The program is a note editor running inside a x86 64-bit unicorn engine instance.
The start script is provided (see start.py)
Print the flag flag2.tzt
, located in 0xFFFFFFFF81000000 + 0x5000
KERNEL_ADDRESS = 0xFFFFFFFF81000000
...
def start_userland():
...
mu.mem_write(KERNEL_ADDRESS + 0x5000, flag2)
The start script adds a few hooks in start_kernel
:
# Handle kernel code
mu.hook_add(UC_HOOK_CODE, handle_kernel, None, KERNEL_ADDRESS, KERNEL_ADDRESS+MAPPING_SIZE)
# Handle IN instruction
mu.hook_add(UC_HOOK_INSN, handle_kernel_in, None, 1, 0, UC_X86_INS_IN)
# Handle OUT instruction
mu.hook_add(UC_HOOK_INSN, handle_kernel_out, None, 1, 0, UC_X86_INS_OUT)
# Handle interrupts
mu.hook_add(UC_HOOK_INTR, handle_kernel_interrupt)
# Handle invalid memory access
mu.hook_add(UC_HOOK_MEM_READ_UNMAPPED | UC_HOOK_MEM_WRITE_UNMAPPED | UC_HOOK_MEM_FETCH_UNMAPPED, handle_kernel_invalid)
The handle_kernel_interrupt
callback checks the interrupt number and rax. If rax == 0, mem_protect
is called. It can be used to change permission to read the flag.
def handle_kernel_interrupt(uc, intno, data):
if intno == 0x70:
rax = uc.reg_read(UC_X86_REG_RAX)
if rax == 0:
rdi = uc.reg_read(UC_X86_REG_RDI)
rsi = uc.reg_read(UC_X86_REG_RSI)
rdx = uc.reg_read(UC_X86_REG_RDX)
# mem_protect(addr, size, perms)
uc.mem_protect(rdi, rsi, rdx)
elif rax == 7:
rdi = uc.reg_read(UC_X86_REG_RDI)
rsi = uc.reg_read(UC_X86_REG_RSI)
rdx = uc.reg_read(UC_X86_REG_RDX)
# mem_read(addr, size)
buf = str(eval(str(uc.mem_read(rdi, rdx))))
uc.mem_write(rsi, buf)
uc.reg_write(UC_X86_REG_RAX, len(buf))
The custom kernel defines 3 system calls: read(0)
, write(1)
and mprotect(10)
.
Since there is a mov eax, 0; int 0x70; iretd
in mprotect
, uc.mem_protect
will be called in handle_kernel_interrupt
In userland
, show_note
has an indirect call using rax, and the note content is used for rdi, rsi, rdx, rcx and rdx (in order)
this can be used to do syscalls by putting the address of __syscall
in rax. Note that the registers are swapped in __syscall
:
There is a check in read_line
that prevents us from inputting the mprotect
syscall number 10. But we can use this same vulnerability to use do_read
to write it instead.
There is a check in the custom kernel's write syscall that prevents us from printing the flag.
Once mprotect
is called to enable writing to the kernel space, we can write a custom syscall based on this but without the check, which is simply:
mov rcx, rdx
mov rax, rcx
mov dx, 0x3f8
rep outsb
iret
- Call
do_read
with address ofnotes[0]
as*buf
and 0x28 ascount
. - Inside
do_read
's input, write notes[0] to domprotect
syscall to permit writing kernel code. - Use the
read
syscall to write custom write syscall without the check inside kernel space. - Call the custom write syscall to print the flag
Is in expoit.py