ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [리버싱 핵심원리 study] 40장 64비트 디버깅
    Reverse Engineering 2021. 2. 8. 18:51

    이번 장에서는 동일한 소스코드를 x86와 x64 환경으로 컴파일했을 때, 어셈블리 코드가 어떻게 달라지는지 비교해본다. 책이 집필될 당시에는 x64dbg가 없어서 그런지 64bit 프로그램을 디버깅할 때 WinDbg를 이용한다. 단순 실행파일의 디버깅이 목적이라면 x64dbg가 편리할 것 같지만 이후에 커널 디버깅 등을 할 때는 WinDbg를 이용하므로, 익숙해질 겸 이번 포스팅에서도 책에서와 같이 WinDbg를 이용해보겠다.

     

    1. 소스코드(WOW64Test.cpp)

    #include "stdio.h"
    #include "windows.h"
    #include "Shlobj.h"
    #include "tchar.h"
    #pragma comment(lib, "Shell32.lib")
    
    int _tmain(int argc, TCHAR* argv[])
    {
        HKEY    hKey                = NULL;
        HANDLE  hFile               = INVALID_HANDLE_VALUE;
        TCHAR   szPath[MAX_PATH]    = {0,};
    
        ////////////////
        // system32 folder
        if( GetSystemDirectory(szPath, MAX_PATH) )
        {
            _tprintf(L"1) system32 path = %s\n", szPath);
        }
    
        ////////////////
        // File size
        _tcscat_s(szPath, L"\\kernel32.dll");
        hFile = CreateFile(szPath, GENERIC_READ, FILE_SHARE_READ, NULL, 
                           OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
        if( hFile != INVALID_HANDLE_VALUE )
        {
            _tprintf(L"2) File size of \"%s\" = %d\n", 
            szPath, GetFileSize(hFile, NULL));
            CloseHandle(hFile);
        }
    
        ////////////////
        // Program Files
        if( SHGetSpecialFolderPath(NULL, szPath, 
                                   CSIDL_PROGRAM_FILES, FALSE) )
        {
            _tprintf(L"3) Program Files path = %s\n", szPath);
        }
    
        ////////////////
        // Registry
        if( ERROR_SUCCESS == RegCreateKey(HKEY_LOCAL_MACHINE, 
                                          L"SOFTWARE\\ReverseCore", &hKey) )
        {
            RegCloseKey(hKey);
            _tprintf(L"4) Create Registry Key : HKLM\\SOFTWARE\\ReverseCore\n");
        }
    
        return 0;
    }

     

    위 소스코드에서는 GetSystemDirectory(), CreateFile(), SHGetSpecialFolderPath(), RegCreateKey() API 등을 이용하며 각종 정보를 콘솔에 출력해주는 역할을 한다. 위 소스코드를 Visual C++ x86 / x64로 컴파일했을 때 어떤 차이가 있는지 살펴보자.

     

    2. PE32 : WOW64Test_x86(32bit)

    OllyDbg를 이용해 WOW64Test_x86.exe를 열어보자.

     

    EP 코드

    초기에는 Stub Code들이 나온다. JMP 명령어를 실행시켜 0040128F로 이동해보자.

     

    Startup 코드

    main 함수를 찾기에 앞서 다음과 같은 정보에 주목하자.

     

    (1) WOW64Test_x86.exe는 콘솔 기반의 프로그램으로, GetCommandLine() API를 호출하여 argc, argv[] 값을 처리한다. 해당 부분에 BP를 설치하면 리턴 주소를 확인해서 main 함수를 찾아낼 수 있다.

    (2) main 함수 내부에서 GetSystemDirectory(), GetFileSize(), CreateFile() 등의 API를 호출한다.

    (3) main 함수 내부에서 문자열을 출력하는 코드가 존재한다.

     

    (2)를 이용해 GetSystemDirectory() API가 호출되는 곳을 search for all intermodular call을 이용해 찾아보자.

     

    GetSystemDirectory API 호출

    위와 같이 한 곳에서 호출되는 것을 확인할 수 있다. 해당 부분으로 이동해보자.

     

    main 함수

    main 함수를 코드임을 확인할 수 있다.

     

    3. PE32+ : WOW64Test_x64.exe(64bit)

    64bit 환경에서의 디버깅을 위해 WinDbg를 이용한다. 분석하기에 앞서 간단한 명령어를 알아보자.

     

    명령어 설명 활용
    u Unassemble u : 다음 명령어 표시
    u address : 주소 이후의 명령어 표시
    u L10 : 명령어 10줄 표시
    ub : 이전 명령어 표시
    t Trace[F11] Step Into
    p Pass[F10] Step Over
    g Go(Run) g : 실행
    g address : 주소까지 실행
    d Dump d address : 주소 내용 표시
    db address : byte
    dd address : dword
    dq address : qword
    r Register r : 레지스터 표시
    r register : 지정된 레지스터만 표시
    bp Break Point bp : Break Point(BP) 설정
    bl : BP List 목록 표시
    bc : BP Clear(BP 삭제)
    lm Loaded Module lm : 디버기 프로세스에 로딩된 모듈(라이브러리) 표시
    dt Display Type dt struct name : 구조체 멤버 표시
    dt struct name address : 주소를 구조체에 매핑시켜 표시
    !dh Display PE Header !dh loaded address : PE Viewer

     

    WinDbg는 microsoft 공식 홈페이지에서 다운로드할 수 있다.

    docs.microsoft.com/ko-kr/windows-hardware/drivers/debugger/windbg-install-preview

     

    WinDbg Preview - Installation - Windows drivers

    This section describes how to install the WinDbg Preview debugger.

    docs.microsoft.com

     

    WinDbg를 실행시킨 후, Launch Executable을 통해 WOW64Test_x64.exe를 열어보자.

    WinDbg 실행

    프로그램을 열면 아래와 같이 시스템 중단점(ntdll.dll)에서 멈춘 것을 확인할 수 있다.

    System Break Point

     

    명령어 창에 !dh WOW64Test_x64를 입력하여 PE 헤더를 확인해보자.

    EP

    위와 같이 EP가 142C임을 확인할 수 있다. 이제 g WOW64Test_x64+142c를 입력하여 Entry Point로 이동해보자.

     

    EP

    WinDbg는 기본적으로 한 줄만 표시하므로 u eip L10을 통해 0x10줄을 출력하도록 하자.

     

    EP

    PE32와 유사한 구조를 나타내는 것을 확인할 수 있다. (CALL + JMP) 동그라미 표시한 jmp 명령어를 따라간 후 u eip L10을 입력하여 Startup 코드를 살펴보면 아래와 같다.

     

    Startup 코드

    main 함수를 찾기 위해 kernel32.dll의 GetCommandLineW() API가 호출되는 부분에 BP를 설정해야 한다. 우선 정확한 API 이름을 확인하기 위해 x kernel32!* 명령어를 입력하여 kernel32.dll의 모든 API 목록을 확인한다.

     

    GetCommandLineWStub() API

    위와 같이 GetCommandLineWStub() API가 있는 것을 확인할 수 있다. bp kernel32!GetCommandLineWStub을 통해 해당 지점에 bp를 설정한 후 g(실행)을 하면 해당 API 부분에서 멈추는 것을 확인할 수 있다.

     

    BP 적중

    이제 dq rsp 명령어를 통해 스택에 저장된 리턴 주소를 확인하자.

     

    리턴 주소

    g 00000001`40001381 명령어를 입력하여 주소를 따라가 보자.

     

    GetCommandLine() API 수행 직후

    코드의 첫 번째 명령어 mov를 통해 GetCommandLine()의 return 값을 적당한 위치에 저장한다. 이후에는 Command Line의 문자열을 쪼개어 argc, argv 파라미터를 세팅하는 작업을 한다. 아래쪽 동그라미의 mov는 main 함수의 파라미터인 rcx, rdx, r8 레지스터를 세팅하는 작업이고, 마지막의 call 명령어가 main 함수의 시작 부분이다.

    g 00000001`400013ea를 입력한 후 r 명령어를 통해 마지막 call 명령어가 수행된 후 레지스터 값을 살펴보자.

     

    main 함수 파라미터

    각 레지스터(파라미터)의 값은 다음과 같은 의미를 갖는다.

     

    rcx -> argc (프로그램 실행 말고는 따로 인자가 없으므로 1)

    rdx -> argv[] 배열 시작 주소

    r8 -> '포인터 배열' (스택 영역을 가리킴. 시스템 환경 변수 문자열의 배열)

     

    Step in을 한 후, u eip L40을 통해 main 함수 내부를 살펴보면 다음과 같다.

    main 함수 내부

     

    4. Comment

    x86과 x64 환경에서 컴파일된 바이너리는 다르면서도 크게 보면 비슷하다는 점을 알 수 있었다. 손에 익힐 겸 WinDbg를 이용해보았는데 커널 디버깅을 하지 않는 이상은 x64dbg를 이용할 것 같다.

     

    반응형

    댓글

Designed by Tistory.