PEPassword

A packed PE unpacked first, then the password-check and decryption paths traced in the original binary.

2026.08.18 Reversing original post

(더 자세한 설명은 https://maple19out.tistory.com/10 참고)

파일을 언팩하고 열어보니 original 파일과 packed file 이 있다.

Figure 1

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

Figure 2

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

Figure 3

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

Figure 4

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

Figure 5

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

Figure 6

패치함!

Figure 7

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

Figure 8

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

Figure 9

헥스 에디터로 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 와 함께 쓸 것이다.

Figure 10

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

Figure 11