PEPassword
A packed PE unpacked first, then the password-check and decryption paths traced in the original binary.
(더 자세한 설명은 https://maple19out.tistory.com/10 참고)
파일을 언팩하고 열어보니 original 파일과 packed file 이 있다.

original 파일을 보니 이런게 나온다.

packed.exe 는 아래와 같이 나온다.

닶을 넣어도 딱히 correct 나 wrong 같은 문자가 나오지 않는다. 일단 파일을 열고, 어떤식으로 파일이 실행되는지 분석 하기 위해 symbol 에 가서 exe 파일 부분으로 옮긴다. 그후 밑에 호출 몇개에 bp 를 걸어 헤매다 보면, 윈도우를 호출하는 함수를 볼수있다.

그럼 여기서부터 어쨌든 계산이 시작되는 곳인걸 알수있으니, 천천히 밑에부분들을 분석해보자. 계속 코드를 돌리다 보면, 어떤부분에서 다이얼로그가 끝나게 된다. 그것에 원인인 함수를 찾아보았다.

4091D8 함수 호출 결과 eax가 0xE98F842A인지 비교하고있는 것 같다. 만약 같으면 EndDialog 함수 호출로 다이얼로 그가 끝나게 된다. 일단 jne 부분을 je로 패치하면 더 나아갈수있다!

패치함!

밑에로 쭉 봐보자. 오른쪽에는 어떤식으로 유저의 인풋을 다루는 분석한걸 적어두었다. 한번 관찰을 해보길 바란다. 디코 딩의 핵심 흐름은 0x40921F에서 전개된다. 이전 단계에서는 0x4091DA에 위치한 패스워드 체크 루틴이 두 번 호출되 며, 해당 호출 결과는 EAX와 EBX에 각각 저장된다.

여기 복호화 코드때, edi 레지스터 를 보자. 00401000 이다. 따라가서 덤프창에서 값을 보자. dword 니까 4바이트씩 보 면 된다.

헥스 에디터로 original exe 의 동일 부분을 보자. 이미지 베이스값을 생각해 1000 이다.
>>> hex(0xb6e62e17 ^ 0x14cec81) '0xb7aac296' >>> hex(0x0d0c7e05 ^ 0x57560000) '0x5a5a7e05'
eax 는 edx 값에 따라 바뀐다. 그래서, ebx 값도 필요하다. eax 레지스터의 값은 간단하게 구할 수 있지만, ebx 레지스터 는 역연산으로 계산할 수 없었다. 그래서 코드를 짜서 얻어냄!
def rol32(x: int, r: int) -> int:
r &= 31
return ((x << r) | (x >> (32 - r))) & 0xFFFFFFFF
def ror32(x: int, r: int) -> int:
r &= 31
return ((x >> r) | (x << (32 - r))) & 0xFFFFFFFF
def bytes_to_dword_le(b: bytes) -> int:
if len(b) != 4:
raise ValueError("Need exactly 4 bytes")
return int.from_bytes(b, "little")
def hexbytes_to_dword_le(s: str) -> int:
"""
'81 EC 4C 01' 같은 문자열 -> 0x014CEC81
"""
s = s.replace("0x", "").replace(",", " ").replace("\t", " ").strip()
parts = [p for p in s.split() if p]
if len(parts) != 4:
raise ValueError("Need 4 hex bytes like: '81 EC 4C 01'")
b = bytes(int(p, 16) for p in parts)
return bytes_to_dword_le(b)
def recover_ebx0_from_two_blocks(
plain0: int, cipher0: int, plain1: int, cipher1: int
) -> list[int]:
"""
루프 구조:
[edi] ^= eax
cl = al
ebx = rol(ebx, cl)
eax ^= ebx
cl = bh
eax = ror(eax, cl)
ebx += eax
여기서 첫 2개 DWORD 평문/암호문으로 eax0, eax1을 얻고,
bh(0~255) 브루트포스로 ebx0 후보를 복원한다.
"""
eax0 = (cipher0 ^ plain0) & 0xFFFFFFFF # 1st iteration에서 사용된 eax
eax1 = (cipher1 ^ plain1) & 0xFFFFFFFF # 2nd iteration에서 사용된 eax
al0 = eax0 & 0xFF # cl = al (첫 회전량)
candidates = []
for bh in range(256):
# ror(eax_pre, bh) = eax1 -> eax_pre = rol(eax1, bh)
eax_pre = rol32(eax1, bh)
# eax_pre = (eax0 ^ ebx1) where ebx1 = rol(ebx0, al0)
ebx1 = (eax_pre ^ eax0) & 0xFFFFFFFF
# cl = bh 는 "현재 ebx1 의 BH" 에서 오므로 일치해야 함
if ((ebx1 >> 8) & 0xFF) != bh:
continue
# ebx1 = rol(ebx0, al0) -> ebx0 = ror(ebx1, al0)
ebx0 = ror32(ebx1, al0)
candidates.append(ebx0)
return candidates
if __name__ == "__main__":
plain0 = hexbytes_to_dword_le("81 EC 4C 01")
cipher0 = hexbytes_to_dword_le("17 2E E6 B6")
plain1 = hexbytes_to_dword_le("00 00 56 57")
cipher1 = hexbytes_to_dword_le("05 7E 0C 0D")
cands = recover_ebx0_from_two_blocks(plain0, cipher0, plain1, cipher1)
print("EBX0 candidates:", [hex(x) for x in cands])
돌려보니까 ebx 값이 맞았다. 왠지 모르겠지만 첫 번째 후보 값은 두 번째 반복 구간까지는 조건을 만족했지만, 세 번째 반복 단계에서는 정상적으로 복원되지 않았다. 그래서 두 번째 후보를 위의 eax 와 함께 쓸 것이다.

xor 직전에 넣어서 돌려보니 플래그가 나온다!
