Window Message를 이용하는 실행 파일 분석

Window Message를 이용하는 실행 파일 분석

GUI 윈도우 애플리케이션은 사용자 입력, 시스템 이벤트, 등으로 인해 발생하는 윈도우 메시지(Windows Message)를 처리하며 이를 기반으로 동작한다. 이러한 메시지는 선형적으로 발생하지 않기 때문에 일반적인 실행 코드와는 실행 흐름이 다르다.

이 글에서는 윈도우 메시지를 이용하여 동작하는 PE 파일에 대한 분석 방법에 대해 다룬다.

Windows Message 흐름도

메시지 루프(Message Loop) 는 윈도우 프로그램에서 메시지를 받아들이고 처리하는 루틴이며, GUI 프로그램에 반드시 포함되어야 한다.
아래 이미지는 System Message Queue 에 쌓인 메시지가 애플리케이션에 전달되어 어떻게 처리되는지를 보여준다.

윈도우 메시지 처리 흐름도

System Message Queue 에 쌓인 메시지는 각 애플리케이션의 Thread Message Queue 로 전달된다. 애플리케이션에 구현된 Message Loop 코드는 Thread Message Queue 의 메시지를 꺼내, 적절한 Window 로 전달된다.

각 Window 는 생성 시 Window Class 구조체를 인자로 전달해야 하는데, Window Class 의 멤버 변수 중에는 전달된 메시지를 처리하는 Procedure 주소가 존재한다. Window 에 메시지가 전달되면 지정한 Procedure 에서 메시지 정보를 받아 적절하게 처리한다.

Procedure 가 메시지를 적절하게 처리한 후, 기본적으로 Window 에 구현된 Procedure 에서 메시지를 처리한다.

Window Message Loop

일반적인 메시지 루프는 다음과 같이 구성된다.

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
    MSG msg;
    BOOL bRet;

    while(1)
    {
        bRet = GetMessage(&msg, NULL, 0, 0);

        if (bRet > 0)  // (bRet > 0 이면 메시지가 처리되어야 한다.)
        {
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
        else if (bRet < 0)  // (bRet == -1 은 에러를 뜻한다.)
        {
            // 에러에 대한 처리나 로그 남김이나 프로그램 종료 등의 구문.
            // ...
        }
        else  // (bRet == 0 은 프로그램 종료를 의미한다.)
        {
            break;
        }
    }
    return msg.wParam;
}

GetMessage() 는 메시지 큐에서 메시지를 읽어 온다.

GetMessage() 대신 PeekMessage() 또는 WaitMessage() 를 이용할 수도 있다. PeekMessage()GetMessage() 와 달리 메시지가 없어도 block 상태가 되지 않고 0을 리턴한다. 따라서 메시지가 없는 동안 다른 작업을 실행할 수 있다. WaitMessage() 는 메시지가 없는 동안 스레드를 Sleep 시킨다.

TranslateMessage() 는 가상키 입력을 문자열로 변환시키는 역할을 한다. 이 함수는 필수는 아니지만 없으면 문제가 발생할 여지가 있다.
DispatchMessage() 은 필수로 호출되어야 하며, 메시지를 적절한 윈도우의 프로시저에 메시지를 전달한다.

Window 생성

앞서 Message Loop 코드에 의해 큐에서 읽어온 메시지는 적절한 Window 에 전달될 것이다. 하지만 그전에 애플리케이션은 메시지가 전달될 Window 를 생성해야 한다.

먼저, RegisterClass() API 를 호출하여 윈도우 클래스를 생성한다. 해당 API 는 WNDCLASS 구조체를 인자로 받는다. WNDCLASS 구조체에서 중요한 필드는 lpfnWndProc 이다. lpfnWndProc 필드는 윈도우에 전달되는 메시지를 처리하는 루틴의 시작 주소를 가리킨다.

ATOM RegisterClassA(
  [in] const WNDCLASSA *lpWndClass
);
typedef struct tagWNDCLASSA {
  UINT      style;
  WNDPROC   lpfnWndProc;              // 윈도우에 전달되는 메시지를 처리하는 코드.
  int       cbClsExtra;
  int       cbWndExtra;
  HINSTANCE hInstance;
  HICON     hIcon;
  HCURSOR   hCursor;
  HBRUSH    hbrBackground;
  LPCSTR    lpszMenuName;
  LPCSTR    lpszClassName;
} WNDCLASSA, *PWNDCLASSA, *NPWNDCLASSA, *LPWNDCLASSA;

typedef struct tagWNDCLASSEXA {
  UINT      cbSize;
  UINT      style;
  WNDPROC   lpfnWndProc;              // 윈도우에 전달되는 메시지를 처리하는 코드.
  int       cbClsExtra;
  int       cbWndExtra;
  HINSTANCE hInstance;
  HICON     hIcon;
  HCURSOR   hCursor;
  HBRUSH    hbrBackground;
  LPCSTR    lpszMenuName;
  LPCSTR    lpszClassName;
  HICON     hIconSm;
} WNDCLASSEXA, *PWNDCLASSEXA, *NPWNDCLASSEXA, *LPWNDCLASSEXA;

이후 CreateWindow() 에 앞서 생성한 윈도우 클래스 구조체의 ClassName을 전달하여 윈도우를 생성할 수 있다. 이렇게 생성된 윈도우에 윈도우 메시지가 전달되면, lpfnWndProc 필드에 지정한 루틴에서 메시지를 처리한다.

void CreateWindowA(
  [in, optional]  lpClassName,
  [in, optional]  lpWindowName,
  [in]            dwStyle,
  [in]            x,
  [in]            y,
  [in]            nWidth,
  [in]            nHeight,
  [in, optional]  hWndParent,
  [in, optional]  hMenu,
  [in, optional]  hInstance,
  [in, optional]  lpParam
);

HWND CreateWindowExW(
  [in]           DWORD     dwExStyle,
  [in, optional] LPCWSTR   lpClassName,
  [in, optional] LPCWSTR   lpWindowName,
  [in]           DWORD     dwStyle,
  [in]           int       X,
  [in]           int       Y,
  [in]           int       nWidth,
  [in]           int       nHeight,
  [in, optional] HWND      hWndParent,
  [in, optional] HMENU     hMenu,
  [in, optional] HINSTANCE hInstance,
  [in, optional] LPVOID    lpParam
);

생성한 윈도우에 메시지가 전달되면, RegisterClass() 호출 시 등록했던 lpfnWndProc 필드 주소의 프로시저가 실행된다. lpfnWndProc 는 다음과 같은 인자를 가진다.

LRESULT Wndproc(
  HWND unnamedParam1,     // 윈도우 핸들.
  UINT unnamedParam2,     // 프로시저에 전달된 메시지.
  WPARAM unnamedParam3,   // 추가 메시지 정보.
  LPARAM unnamedParam4    // 추가 메시지 정보.
)

가장 중요한 인자는 프로시저에 전달된 메시지의 타입을 지정하는 두 번째 인자다. 윈도우 프로시저는 해당 값을 비교하여 전달된 메시지 타입을 인지하고, 적절하게 처리하는 코드를 구현해야 한다. 세 번째, 네 번째 인자에는 메시지와 함께 전달되는 추가 정보가 저장될 수 있다.

Windows 메시지를 이용하는 파일 분석

앞선 글을 읽어보면, 결국 윈도우 메시지를 이용하는 실행 파일을 분석하는 데에 가장 중요한 부분은 메시지를 처리하는 Procedure 코드이다. Procedure 의 주소는 RegisterClass() 가 호출될 때 전달되는 WNDCLASS 구조체에 저장된다. 아래 코드에서는 0x41d59f 주소의 코드가 Procedure 로 지정된 것을 확인할 수 있다.

RegisterClass()lpfnWndProc 설정 코드

0x41d59f 주소 함수는 2번째 인자 값을 검증하여 분기하는 코드가 다수 존재한다. Wndproc 의 2번째 인자는 메시지 타입을 의미하며, 따라서 해당 함수는 전달받은 메시지 타입에 따라 다른 동작을 수행한다는 것을 알 수 있다. 윈도우 메시지 타입에 따른 상수 값은 WinUser.h 파일에 정의됐고, 간단하게 목록만 알고 싶다면 List Of Windows Messages 글을 참고하자.

메시지 프로시저 코드