ReadMe.txt

    Twist1.exe is run in x86 windows.

    32비트 윈도우에서만 실행 가능하다


    ExeInfo PE

    알려지지 않은 프로그램으로 패킹되어있는 것 같다.

    디버깅을 통해 하나씩 살펴보자.


    Analysis (Packed)

    윈도우 xp에서 프로그램을 실행시키도록 해보았다.

    우선 처음에 실행하다보면 아래와 같은 에러가 발생한다.

    (사진)

    안티 디버깅이 탑재되있는 듯 하다.

    하나씩 스텝을 밟아보면,
    sub_40709F, sub_4070C7 함수를 통해 디버깅을 탐지하는 것을 알 수 있다.

    sub_40709F 함수를 호출하기 전에 2번쨰 매개변수인 a2에 대해 알아보자면,

    .text:00407077 mov eax, large fs:30h
    https://sanseolab.tistory.com/47

    위 fs:[30]은 PEB 테이블의 주소를 의미하는데,
    Process Environment Block으로, 윈도우에서 프로제스 정보를 담고있는 구조체라고 보면 된다.

    그 주소에다가 0x28과 0x30을 XOR한 0x18을 더하는 것을 알 수 있다.

    0x18은 어떤 필드를 가리키는건 windbg로 확인해보면,

    ProcessHeap 필드를 가리키고 있었다.

    Pseudo Code로 나타내면 아래와 같았다.

    sub_40709F

    이제 매개변수 a2는 &NtCurrentPeb()->ProcessHeap라는 것을 알았다.
    하나씩 살펴보면,

    몇가지 분기문이 있는데,
    여기서 .text:004070BC call loc_407300을 살펴보면,

    retn도 없이 그냥 구렁텅이로 빠지는 것을 볼 수 있다.

    Pseudo Code로 나타내면 아래와 같다.

    https://m.blog.naver.com/719121812/20177549572

    +0xC는 윈도우xp에서의 Flags 필드 오프셋, +0x40은 윈도우7에서의 Flags 필드 오프셋을 의미한다.
    Flags가 2가 아니면 구렁텅이로 빠지는데,

    여기서 2는
    https://h3rm1t.tistory.com/7
    https://learn.microsoft.com/ko-kr/windows-hardware/drivers/ddi/ntifs/nf-ntifs-rtlcreateheap

    HEAP_GROWABLE 을 의미한다.

    그러나 디버깅이 되어있어 실제로 값을 확인해보면,
    2가 아닌

    ECX값이 0x50000062이라는 것을 알 수 있다.

    HEAP_GROWABLE (0x02)
    HEAP_TAIL_CHECKING_ENABLED (0x20)
    HEAP_FREE_CHECKING_ENABLED (0x40)
    HEAP_SKIP_VALIDATION_CHECKS (0x10000000)
    HEAP_VALIDATE_PARAMETERS_ENABLED (0x40000000)

    위 플래그가 모두 활성화되었다고 보면 된다.

    >>> hex(0x2 | 0x20 | 0x40 | 0x10000000 | 0x40000000)
    '0x50000062'

    아래와 같이 0x004070AC 지점에 브레이크포인트를 걸어 0x004070C1로 EIP를 수정해서 우회하면 된다.

    sub_4070C7

    점프를 여러번하여 마지막에는 0x004070CB로 점프한다.

    Pseudo Code로 나타내면 아래와 같은데,

    이번에는 ForceFlags 필드값을 통해 디버깅을 체크하고 있었다.

    0x04070DD 지점에 브레이크포인트를 걸고,
    걸렸을때 EIP를 0x4070F0으로 수정하면 우회할 수 있다.

    이제 쭉쭉 따라가보면,

    https://www.vergiliusproject.com/kernels/x86/Windows%20XP/SP3/_LDR_DATA_TABLE_ENTRY

    +0x10 오프셋은 InInitializationOrderLinks을 의미한다.

    Ldr을 통해 프로세스가 디버깅 중인지 확인할 수 있는데,
    디버깅 중인 프로세스는 힙 메모리 영역에 자신이 디버깅 수행 중인 프로세스라는 표시하기 위해 사용되지 않은 부분에는 0xFEEEFEEE이나 0xABABABAB로 채운다.

    00407139 지점에 브레이크포인트를 걸고,
    걸렸을때 EIP를 0x40715D로 수정하면 우회할 수 있다.

    여기까지 우회했다면 0x407179로 점프된 것을 알 수 있는데,
    분기문에 의해 반복이 여러번 진행되므로 0x4071A4에 브레이크를 걸고 다시 확인해보자.

    0x4071BD에서 멈쳤을때
    0x4071AF, 0x4071B6에 있는 명령어에 의해 0x4071BD의 명령어가 바껴
    jmp문이 생긴 것을 볼 수 있다.

    OEP는 0x40157C였다.

    Scylla 프로그램으로 덤프해서 언패킹된 바이너리를 구할 수 있다.
    OEP를 0x40157C로 수정하고, IAT AutoSerach, Get Imports, Dump, FixDump 차례대로 누르면 된다.

    그러면 Twist1_dump_SCY.exe 실행 파일이 생긴다.
    이제 이 실행파일로 IDA를 통해 다시 분석해보자.

    할려고 했으나 무슨일에선지 사용자 인풋값을 확인하는 sub_401240 함수가 이상하게 나온다.

    따라서 사용자 인풋값이 맞는지 아닌지 Wrong 출력 문구가 뜨지 않는다.


    결국 디버깅해서 해결하기로 하였다.

    https://doongdangdoongdangdong.tistory.com/201

    OllyDumpEx 프로그램으로 덤프떠서 해결!

    실행 중인 Twist1 프로세스에서 OEP에서 Image Base를 뺀 값으로, Entry Point를 0x157c로 설정하고 Dump 버튼을 누르면 된다.

    Anlaysis 2 (Unpacked)

    언팩된 바이너리로 다시한번 분석해보자.

    main

    최종적으로 sub_401240 함수에서 사용자 인풋값이 맞는지 아닌지 확인하고 있었다.

    sub_401160

    nop 명령어가 여러번 나오고 마지막에 retn 명령어밖에 없다.
    무의미한 코드이다.

    sub_401000

    dword_40B968에는 kernel32.dll의 base 주소,
    dword_40B964에는 ntdll.dll의 base 주소가 각각 들어간다.

    sub_401090

    sub_401090 함수가 끝나고 복귀했을떄의 0040128A 지점에 브레이크포인트를 걸어 eax를 확인해보면,

    sub_401090함수는 kernel32_SetUnhandledExceptionFilter 주소를 반환한다는 것을 알 수 있었다.
    이렇게 반환된 주소는 dword_40B960에 저장한다.

    그리고 sub_401140을 매개변수 a1으로 지정하여, SetUnhandledExceptionFilter을 호출한다.

    sub_4011D0

    “Reversing.Kr CrackMe” 문구를 출력한다.

    sub_401200

    “Input: ” 문구를 출력한다.

    sub_401220

    사용자 인풋을 dword_40B970 저장한다.

    sub_401240

    사용자 인풋 중 10바이트까지만 v7에 저장한다.

    https://ruinick.tistory.com/27
    https://jeep-shoes.tistory.com/49
    https://jeongzzang.com/156
    https://learn.microsoft.com/ko-kr/windows/win32/procthread/zwqueryinformationprocess

    NTSTATUS WINAPI ZwQueryInformationProcess(
      _In_      HANDLE           ProcessHandle,
      _In_      PROCESSINFOCLASS ProcessInformationClass,
      _Out_     PVOID            ProcessInformation,
      _In_      ULONG            ProcessInformationLength,
      _Out_opt_ PULONG           ReturnLength
    );

    ZwQueryInformationProcess 함수에서 디버깅을 확인한다.
    함수의 두번째 멤버인 ProcessInformationClass값이 ProcessDebugPort을 의미하는 7이면, 세번째에 있는 output인 ProcessInformation이 가르키는 주소에 0xFFFFFFFF값이 나온다고 하는데,

    디버깅중이기에 실제로 dword_409180에 그 값이 저장되있는 것을 확인할 수 있었다.

    따라서, 00407299에 브레이크포인트를 걸어
    그 지점에 걸렸을때, dword_409180값을 0으로 수정해서 우회하도록 한다.

    004072C4 브레이크포인트걸고 계속 연구… TO BE CONTINUED>>>>

    https://nightohl.tistory.com/entry/NtQueryInformationProcess-ProcessDebugObjectHandle-%EC%9A%B0%ED%9A%8C%EB%B0%A9%EB%B2%95

    여기까지 넘어갔다면 이제는 ZwQueryInformationProcess 함수에 2번쨰 인자값인 ProcessInformationClass에 ProcessDebugObjectHandle을 의미하는 0x1e를 넘겨 디버깅을 한번 더 확인한다.

    우회하는 방법은 004072C4에 브레이크포인트를 걸고,
    걸렸을때 ZwQueryInformationProcess 리턴값이 들어있는 eax를 0xC0000353 (STATUS_PORT_NOT_SET)으로 수정해주면 된다.

    0x409150으로부터 메모리를 1바이트를 읽어와 0xB8인지 확인한다.

    0x409150를 확인해보면,

    opcode를 확인하는 것으로 보인다.
    별다른 우회없이 그냥 넘어갈 수 있다.

    https://ddatg.tistory.com/80

    넘어갔다면 이제는 ZwQueryInformationProcess 함수에 2번쨰 인자값인 ProcessInformationClass에 ProcessDebugFlags을 의미하는 0x1f를 넘겨 디버깅을 한번 더 확인한다.

    디버깅이 되어있다면, 3번째 인자(dword_409130, DebugFlags)에 0이 들어가기 때문에
    1로 수정한다.

    따라서 우회하는 방법은 00407459 지점에 브레이크포인트를 걸고,

    위와 같이 0x409130에 1로 수정해주면 된다.

    byte_40B991에는 사용자 인풋의 7번쨰 글자,
    byte_40B990에는 1번쨰 글자가 들어간다.

    이를테면, 사용자 인풋이 “ABCDEFG…”라면,
    byte_40B991에는 ‘G’,
    byte_40B990에는 ‘A’ 글자가 각각 들어간다.

    다시 이전 함수로 복귀해서 확인해보면,

    opcode를 수정하는 것을 볼 수 있다.

    수정했을때의 전과 후를 비교해보면, 아래와 같다.

    위처럼 코드가 수정되었다.

    byte_40B991에 있는 1바이트와 0x36을 XOR한 값이 byte_40C450에 저장된다.

    byte_40B991에는 사용자 인풋의 7번째 글자를 의미하고,

    이를테면, 사용자 인풋이 “ABCDEFG…”라면,
    byte_40C450에는 0x47(‘G’)과 0x36을 XOR한 0x71이 들어간다.

    EIP를 00407497여기까지 멈추고, TO BE CONTINUE>>>

    dword_40B964에는 kernel32.dll의 base 주소가 들어가있고,
    dword_409200에는 “GetCurrentThread” 문자열이 들어간다.

    .text:004074C2 mov dword ptr ds:loc_407471, 15975623h
    .text:004074CC mov dword ptr ds:loc_407471+4, 1ABCD562h
    .text:004074D6 mov dword ptr ds:loc_40749A, 1A25CFDAh
    .text:004074E0 mov dword ptr ds:loc_40748E, 9CA565AAh

    추가로, 위 4가지의 mov 명령어에 의해
    loc_407471, loc_407471+4, loc_40749A, loc_40748E 주소에 있는 opcode가 수정된다.

    그리고 sub_401090 호출을 통해 kernel32.dll의 GetCurrentThread 함수 주소를 가져온다.

    dword_409200에는 “GetThreadContext” 문자열이 들어가고,

    .text:00407522 mov dword ptr ds:loc_4074EF, 15975623h .text:0040752C mov dword ptr ds:loc_4074F5, 1ABCD562h .text:00407536 mov dword ptr ds:loc_407400+4, 1A25CFDAh .text:00407540 mov dword ptr ds:loc_4074AE, 9CA565AAh

    마찬가지로 위 4가지의 mov 명령어에 의해
    loc_4074EF, loc_4074F5, loc_407400+4, loc_4074AE 주소에 있는 opcode가 수정된다.

    dword_409240에는 kernel32_GetCurrentThread 주소가 저장된다.

    그리고 dword_40B964에 있는 kernel32.dll의 base 주소를 eax로 가져오면서,
    sub_401090 호출을 통해 kernel32.dll의 GetThreadContext 함수 주소를 가져온다.

    https://ruinick.tistory.com/53
    https://www.vergiliusproject.com/kernels/x86/Windows%20XP/SP3/_CONTEXT
    https://codemachine.com/downloads/win71/winnt.h

    CONTEXT ct; 
    ct.ContextFlags = (CONTEXT_CONTROL | CONTEXT_INTEGER | CONTEXT_SEGMENTS | CONTEXT_FLOATING_POINT | CONTEXT_DEBUG_REGISTERS); //0x1001F
    
    GetThreadContext(GetCurrentThread(), &ct);

    Hardware BreakPoint 기반 안티 디버깅으로 GetThreadContext, GetCurrentThread API를 통해 디버깅을 잡아낸다.

    Dr0 ~ Dr3 : Hardware BP를 설정할 선형 주소
    Dr4 ~ Dr5 : Reserved(예약됨)
    Dr6 : Debug Status
    Dr7 : Debug Control

    위 6가지를 모두 체크한다.

    dword_4092A4에는 Dr0,
    dword_4092A8에는 Dr1,
    dword_4092AC에는 Dr2,
    dword_4092B0에는 Dr3,
    dword_4092B4에는 Dr6,
    dword_4092B8에는 Dr7 필드값에 해당된다.

    따라서 우회하려면, 00407580 지점에 브레이크포인트를 걸고
    걸렸을때 위 6가지의 dword_4092A4~dword_4092B8값을 모두 0으로 수정해줘야 한다.

    이제 이 프로그램에 있는 모든 안티디버깅을 우회하였다!

    사용자 인풋값을 검증하는 요소로 넘어가는데 한번 확인해보자.

    byte_40C450은 0x36이여야 된다.

    사용자 인풋의 7번째 글자인 byte_40B991과 0x36을 XOR한 값이 byte_40C450로 들어가므로,
    사용자 인풋의 7번째 글자는 0x36과 0x36을 XOR한 값인 0x00 (=NULL),
    즉 이를 통해 사용자 인풋의 글자수는 6자리여야 된다는 것을 알 수 있다.

    loc_40762B 함수를 호출하여 사용자 인풋값을 마저 검증한다.

    mov al, byte_40B990에 의해
    매개변수인 a1(al 레지스터)에는 사용자 인풋의 1번째 글자가 들어가는데,
    ror al, 6에 의해
    오른쪽으로 6만큼 회전하고,
    mov byte_40B000, al에 의해
    그 회전한 값을 byte_40B000에 저장한다.

    이를 테면, 사용자 인풋의 첫번째 글자가 “A”, 0x41이였다면,
    6만큼 회전해서 5라는 값이 나온다.

    매개변수인 a1에는 이제 사용자인풋의 1번쨰 글자와 6만큼 ROR한 값이 들어간다.

    byte_40B001에는 a1 값과 4만큼 ROL한 값이 들어가고,
    byte_40B002에는 방금 ROL 연산한 값에 0x34를 XOR한 값이 들어간다.

    .text:00407700 mov cl, byte_40B000에 의해

    따라가보면, cl 레지스터값을 의미하는 a3에는 byte_40B000 값이 들어간다,
    그러니까 사용자 인풋의 1번째 글자에 6만큼 ROR한 값이 들어간다는 의미이다.

    이 값이 0x49여야하므로, 0x49 값에 6만큼 ROL 연산을 하면,
    첫번째 글자는 0x52, 즉 R이여야 한다.

    이전에 .text:0040762B xor edx, edx 명령어에 의해
    edx는 0 값이 들어가고, 이 값은 dword_4076CC에 저장된다.
    이전에 sub_407619 함수 수행에 의해
    eax는 첫번째 글자값과 6만큼 ROR, 4만큼 ROL, 0x34 XOR 한 값이 들어간다.

    .text:004076DF mov dword ptr ds:sub_4076D0, eax
    근데 무의미한 코드로, 또 opcode를 eax값으로 수정한다.

    .text:004076E4 xor eax, eax
    .text:004076E6 xor ecx, ecx
    에 의해 eax와 ecx는 0 값으로 만들어버리고, ecx 역시 0이다.

    .text:004076E9 mov al, byte ptr dword_40B970
    .text:004076EE mov dl, byte ptr dword_40B970+1
    .text:004076F4 mov cl, byte ptr dword_40B970+2

    al에는 사용자 인풋의 1번째 글자,
    dl에는 2번째, cl에는 3번째 글자가 들어간다.

    그리고 loc_40771B 주소로 점프한다.

    각각 dh 레지스터에는 사용자 인풋의 4번째 글자값,
    bh 레지스터에는 5번째 글자값,
    bl 레지스터에는 6번째 글자값이 들어간다.

    byte_40B970에는 원래는 1번째 글자값이 들어있었는데,
    0x12와 XOR 연산한 값이 들어가게 된다.

    그리고 loc_407750 주소로 점프한다.

    각각 byte_40CCE0에는 사용자인풋의 3번쨰 글자값,
    byte_40CCE4에는 6번째
    byte_40CCE8에는 1번째,
    byte_40CCECE에는 2번째,
    byte_40CCF0에는 5번째,
    byte_40CCF4에는 4번째 글자값이 들어간다.

    그리고 al 레지스터에 사용자인풋의 3번쨰 글자값과 0x77을 XOR한 값이 들어가고,
    loc_4077A3로 점프한다.

    사용자인풋의 3번쨰 글자값과 0x77을 XOR한 값인 al이 0x35여야 한다.

    따라서 0x77과 0x35를 XOR 연산한 값인 0x42,
    즉 3번째 글자는 “B”여야 한다.

    근데 이러나저러나, sub_407800로 이동하기 때문에 페이크인 것 같다…|
    페이크인줄 알았는데, 조건이 맞아야 jnz에 의해 분기되지 않고, call로 넘어가 다시 검증할 수 있는 함수로 복귀할 수 있기 때문에 페이크가 아니다.

    dword_40CD80에 2번째 글자값이 들어가고, ecx 또한 그렇다.

    .text:004077AC xor cl, 20h
    복귀하고나서 그렇게 2번째 글자값과 0x20을 XOR한 값이 cl 레지스터에 들어간다.

    사용자인풋의 2번쨰 글자값과 0x20을 XOR한 값인 cl이 0x69여야 한다.

    따라서 0x20과 0x69를 XOR 연산한 값인 0x49,
    즉 2번째 글자는 “I”여야 한다.

    dword_40CD30에는 5번째 글자값이 들어가고,
    byte_40CCE0에는 숫자 2가 들어가고,
    dl 레지스터에는 첫번째 글자값이 들어간다.


    페이크 코드가 들어있다.

    1번째 글자값과 0x10을 XOR한 값이 0x43인지 확인하는데,
    이 조건이 만족하지 않아야 한다.

    즉, .text:0040781A jnz short loc_407824 로 분기해야 다음 검증을 진행할 수 있다.

    이후 sub_407838을 호출하는 것을 볼 수 있는데,
    dl 레지스터값에는 사용자 인풋의 4번째 글자값,
    al 또한 4번째,
    byte_40C400 또한 4번째 글자값이 들어간다.

    그렇게 점프를 여러번 거치다보면,

    byte_40C401에 사용자 인풋의 4번째 글자값이 들어가고,
    dword_40B084에 1값이 들어간다.
    그리고 점프를 또 거치다보면,

    byte_40CFF4에는 사용자 인풋의 6번째 글자값이 들어간다.

    그렇게 여러번 루프를 거치고나면,
    .text:004078BF jmp short loc_4078DB에 의해,

    004078DB 주소로 점프한다.
    하나씩 call하는 함수에 대해 살펴보면,

    sub_4078FD
    edx 레지스터값을 0으로 초기화한다.

    sub_40790E
    dl 레지스터값에 사용자의 4번째 인풋값과 0x21을 XOR 연산한 값이 들어간다.

    그렇게 XOR 연산된 값인 dl 레지스터값이 0x64여야 된다.

    따라서 0x21과 0x64를 XOR 연산한 값인 0x45,
    즉 4번째 글자는 “E”여야 된다.
    그러면 loc_409DCE로 분기할 수 있다.

    dl 레지스터값에는 사용자 인풋의 5번째 글자값이 들어가고,
    eax, edi, ecx 레지스터값 모두 0으로 초기화한다.

    call하는 3가지 함수가 있는데, 또 살펴보면,

    sub_407E10
    무의미한 코드

    sub_407E0C
    무의미한 코드2

    sub_407E02
    사용자의 인풋 5번째 글자값이 들어있는 dl 레지스터값에다가
    0x46을 XOR 연산한 값이 들어간다.

    복귀하면, 이제 loc_407DF4로 점프하고 또 점프, 여러번 점프한다.

    도착하면 sub_407E18 함수를 호출하는 것을 볼 수 있다.

    sub_407E02 함수에 의해 반환된 값이 사용자의 인풋 5번째 글자값과 0x46을 XOR한 값이므로, al 값이 그렇게 된다.

    그렇게 XOR 연산된 값인 al 레지스터값이 8이여야 된다.

    따라서 0x46과 0x8 XOR 연산한 값인 0x4e,
    즉 5번째 글자는 “N”이여야 된다.

    retn에 의해 이제 loc_4078A6 주소로 복귀한다.

    byte_40CFF4에는 사용자 인풋의 6번째 값이 들어가있는데,
    그 값과 4만큼 rol 회전한 값으로 byte_40CFF4에 다시 저장한다.
    그리고 loc_4078C1으로 점프, 또 점프하고

    .text:00407F60 call loc_407F6E
    call하는 곳으로 가보면,

    사용자 인풋의 6번째 값과 4만큼 rol 회전한 값이 들어있는 byte_40CFF4가 0x14인지 확인한다.

    따라서 0x14을 4만큼 ROR 회전한 값인 0x41,
    즉 6번째 글자는 “A”이여야 된다.

    조건을 만족시키고 step을 밟아 넘어가다보면,

    eax는 1로 set되고,

    이제 0x4012CB로 복귀하여 검증 결과를 보여준다!

    긴 여정이었다…….

    solve.py

    def rol(n, m):
        shift = n << m
        shift &= 255
        src = n >> (8 - m)
        return shift | src
        
    def ror(n, m):
        shift = n >> m
        src = n << (8 - m)
        src &= 255
        return shift | src
    
    glyph1 = chr(rol(0x49, 6))
    glyph2 = chr(0x20 ^ 0x69)
    glyph3 = chr(0x77 ^ 0x35)
    glyph4 = chr(0x21 ^ 0x64)
    glyph5 = chr(0x46 ^ 0x8)
    glyph6 = chr(ror(0x14, 0x4))
    
    key = glyph1 + glyph2 + glyph3 + glyph4 + glyph5 + glyph6
    
    print(f"key: {key}")

    Result

    PS C:\Users\Seo Hyun-gyu\Desktop> python3 .\solve_twist1.py
    key: RIBENA

    답글 남기기

    이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다