TL;DR
problem.exe (32-bit Windows PE)
obfus.h로 난독화로 가상화된 산술 연산, 흐름 교란, 문자열 은닉
Analysis
Obfh_VirtualMachine호출과 각종 FPU 연산이 반복적으로 등장.fcn.0040145d에서 입력 문자열을%255s로 읽은 후0x401805함수(이름을 임의로 validator)에서 복잡한 계산을 수행.- validator 내부에는 다음 패턴이 반복된다.
- 입력 바이트를 로드 여러
imul,fild,fstpObfh_VirtualMachinecmp eax, imm8.
- 입력 바이트를 로드 여러
cmp인근의add eax, <offset>등으로 입력 인덱스를 역추적 가능.
가상화 우회 전략
obfus.h의 가상화된 수식을 직접 복호화하는 대신 에뮬레이션으로 연산을 그대로 실행하여 결과만 관찰한다.
Unicorn
- PE 전체(.text, .data, .obfh)를 Unicorn 메모리에 로드.
- 스택/입력 버퍼 구성
- 스택: 0x900000 영역에 0x20000바이트 맵핑.
- 입력 문자열: 0x800000에 할당, 인자로 전달되도록
ESP조작.
- validator 시작 주소
0x401805에서0x4028a4까지 에뮬레이션. cmp직전의EAX값을 후킹하여 기록.
애뮬레이팅 내용
.text:00401804 ; [00000001 BYTES: COLLAPSED FUNCTION nullsub_1].text:00401805 ; ---------------------------------------------------------------------------.text:00401805 push ebp.text:00401806 mov ebp, esp.text:00401808 sub esp, 0C8h.text:0040180E nop
...
.text:00402872 sub esp, 8.text:00402878 fstp qword ptr [esp+0].text:0040287C call sub_4032F1.text:00402881 add esp, 8.text:00402884 and eax, 0FFh.text:0040288A cmp eax, 36h ; '6'.text:0040288D jz loc_40289D.text:00402893 mov eax, 0.text:00402898 jmp locret_4028A2.text:0040289D ; ---------------------------------------------------------------------------.text:0040289D.text:0040289D loc_40289D: ; CODE XREF: .text:0040288D↑j.text:0040289D mov eax, 1.text:004028A2.text:004028A2 locret_4028A2: ; CODE XREF: .text:004018B3↑j.text:004028A2 ; .text:00401959↑j ....text:004028A2 leave.text:004028A3 retn.text:004028A4문자 추출 방법
- 0~255 단일 바이트를 25자리 모두에 채운 입력을 실행.
- 25개의
cmp결과를 수집하여 “문자 비교 결과” 매핑 구성. - 실제 상수 테이블과 매핑을 역추적하면 각 위치의 문자 결정.
- 모든 위치에서 유일하게 매칭되는 문자가 존재.
import structfrom typing import Dict, List, Tuple
from unicorn import Uc, UC_ARCH_X86, UC_MODE_32, UC_HOOK_CODE, UcErrorfrom unicorn.x86_const import UC_X86_REG_EAX, UC_X86_REG_EBP, UC_X86_REG_EIP, UC_X86_REG_ESPimport pefile
PE_PATH = "problem.exe"
CMP_INFO: List[Tuple[int, int, int]] = [ (0x4018A2, 0x98, 23), (0x40194B, 0x30, 15), (0x4019F1, 0xBC, 16), (0x401A9A, 0xAE, 9), (0x401B43, 0xE7, 20), (0x401BEC, 0x6D, 5), (0x401C92, 0x48, 24), (0x401D38, 0xBA, 17), (0x401DE1, 0x30, 2), (0x401E87, 0xA8, 3), (0x401F30, 0xBB, 4), (0x401FD9, 0x8D, 22), (0x402082, 0x0E, 11), (0x402128, 0x05, 8), (0x4021CE, 0x2F, 12), (0x402272, 0x1E, 1), (0x40231E, 0xC4, 7), (0x4023CD, 0x25, 13), (0x402479, 0xCD, 21), (0x402528, 0x79, 19), (0x4025D1, 0xF8, 0), (0x402680, 0x17, 10), (0x40272C, 0xAC, 18), (0x4027DB, 0xB1, 14), (0x40288A, 0x36, 6),]
def load_binary() -> Tuple[Uc, int, bytes, List[pefile.SectionStructure]]: pe = pefile.PE(PE_PATH) base = pe.OPTIONAL_HEADER.ImageBase sections = list(pe.sections)
with open(PE_PATH, "rb") as f: blob = f.read()
max_va = 0 for sec in sections: va = base + sec.VirtualAddress size = sec.Misc_VirtualSize limit = va + ((size + 0xFFF) & ~0xFFF) max_va = max(max_va, limit)
mem_size = (max_va - base) + 0x1000
uc = Uc(UC_ARCH_X86, UC_MODE_32) uc.mem_map(base, mem_size)
for sec in sections: va = base + sec.VirtualAddress raw = sec.PointerToRawData size = sec.SizeOfRawData uc.mem_write(va, blob[raw:raw + size])
return uc, base, blob, sections
def prepare_context(uc: Uc, base: int, flag: bytes) -> int: stack_top = 0x900000 stack_size = 0x20000 uc.mem_map(stack_top - stack_size, stack_size)
inp_addr = 0x800000 uc.mem_map(inp_addr, 0x1000) uc.mem_write(inp_addr, flag + b"\x00")
esp = stack_top - 0x10 uc.mem_write(esp, struct.pack("<I", 0xDEADBEEF)) uc.mem_write(esp + 4, struct.pack("<I", inp_addr)) uc.reg_write(UC_X86_REG_ESP, esp) uc.reg_write(UC_X86_REG_EBP, 0) uc.reg_write(UC_X86_REG_EIP, 0x401805)
return inp_addr
def run_with_flag(flag: bytes, force_expected: bool = False) -> Dict[int, int]: uc, base, _, _ = load_binary() prepare_context(uc, base, flag)
cmp_map = {addr: expected for addr, expected, _ in CMP_INFO} observed: Dict[int, int] = {}
def hook(emu: Uc, address: int, size: int, user_data: object) -> None: if address in cmp_map and address not in observed: eax = emu.reg_read(UC_X86_REG_EAX) & 0xFF observed[address] = eax if force_expected: emu.reg_write(UC_X86_REG_EAX, cmp_map[address])
uc.hook_add(UC_HOOK_CODE, hook) try: uc.emu_start(0x401805, 0x4028A4) except UcError: pass
return observed
def brute_force_flag() -> str: cmp_order = [addr for addr, _, _ in CMP_INFO] mapping: List[Dict[int, int]] = [dict() for _ in CMP_INFO]
for byte in range(256): flag = bytes([byte]) * 25 observed = run_with_flag(flag, force_expected=True) values = [observed.get(addr, None) for addr in cmp_order] if None in values: raise RuntimeError("Failed to record all cmp values.") for idx, val in enumerate(values): mapping[idx][val] = byte
recovered = ["?"] * 25 for idx, (addr, expected, pos) in enumerate(CMP_INFO): guess = mapping[idx].get(expected) if guess is None: raise RuntimeError(f"No byte matched cmp at {addr:#x}.") recovered[pos] = chr(guess)
return "".join(recovered)
def main() -> None: flag = brute_force_flag() print("Recovered flag:", flag)
observed = run_with_flag(flag.encode(), force_expected=False) for addr, expected, _ in CMP_INFO: if observed.get(addr) != expected: raise RuntimeError("Validation failed.") print("Validation passed.")
if __name__ == "__main__": main()복원된 플래그
python3 solve.pyRecovered flag: hspace{d8a928b2043db77e3}Validation passed.길이 25바이트로 문제 조건을 만족한다.