Reversing/Hooking

[Hooking ] 5. TLS + TLS CallBack

D3D 후킹 관련 코드를 찾다보니 아래와 같은 예제 코드를 볼 수 있었다.

BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved )
{
    switch( fdwReason )
    {
        case DLL_PROCESS_ATTACH:
        {
            DisableThreadLibraryCalls(hinstDLL);
            StartD3DHooks();
                return true
            break
        }
        case DLL_PROCESS_DETACH:
        {
            MessageBox(NULL,L"detach dll!", L"ok", MB_OK);
            break
        }
    }
    return TRUE;
}

일단 예제에서 소개하는 코드를 보면, DisableThreadLibraryCalls() 라는 함수가 호출된다. TLSCallBack을 방지하여 디버거 검출 코드로부터 숨는 용도로 쓰인다는데, TLS Callback이 뭔지 알아봤다.


TLS CallBack 함수

TLS (Thread Local Storage)는 쓰레드별로 독립되어 쓰이는 메모리 공간을 말하는데, 하나의 프로세스에서 모든 쓰레드는 가상 메모리 공간을 공유하여 전역변수나 정적(static)변수는 공유 메모리 영역에 놓여서 쓰인다.

그러나 함수 안에서 쓰이는 지역변수는 그 함수를 실행하는 각 쓰레드의 전용 메모리 공간에 놓이는데, 전역변수나 정적변수를 지역변수처럼 전용 메모리 공간에 두고 사용하고 싶을때 TLS 함수를 활용한다.

Callback 함수는 다른함수의 인자로써 쓰이거나, 특정 이벤트에 의해 호출되는 함수를 말하는데, 쓰레드 전용으로 쓰이는 TLS와 Callback 함수의 특성이 합쳐져 TLS Callback 함수는 쓰레드의 본격적인 코드 실행, 즉 Entry Point가 실행되기 전에 호출되는 특정을 가지게 된다.

#include <stdio.h>
#include "windows.h"

//linker spec
#ifdef _M_IX86
#pragma comment (linker, "/INCLUDE:__tls_used")
#else
#pragma comment (linker, "/INCLUDE:_tls_used")
#endif
EXTERN_C
#ifdef _M_X64
#pragma const_seg (".CRT$XLB")
const
#else
#pragma data_seg (".CRT$XLB")
#endif
//end linker

void NTAPI __stdcall tls_callback(PVOID, DWORD dwReason, PVOID Reserved)
{
    printf("TLS Function Called\n");
}
//tls import
PIMAGE_TLS_CALLBACK p_thread_callback = tls_callback;
#pragma data_seg ()
#pragma const_seg ()
//end 

int main() {
    printf("main function called\n");
}
TLS Function Called
main function called

위 코드를 컴파일해서 실행하면 main 함수가 실행되기 전에 TLS 함수가 먼저 호출되는걸 확인할 수 있었다.

ntdll.dll 에서 현재 프로세스에 로드되는 각 모듈들의 Entrypoint로 점프하는 구문이 있는데, tlscallback이 선언된 프로그램에서 해당 구문을 실행하면

call 어셈이 실행된 다음 tls_callback으로 먼저 점프하는게 보인다.

여긴 아까 내가 만들어둔 tls 함수 실행구문.

 

 

해당 구문이 지나고 나서야 call esi를 통해 프로그램의 entrypoint로 진입하고 main 함수 실행 루틴까지 진행되는걸 볼 수 있다.

이렇게 EntryPoint 이전에 먼저 실행된다는 특성때문에 TLS Callback 함수는 디버깅 방지에 주로 사용된다고 한다. 이게 어떻게 활용되는지 확인을 좀만 더 해보자.


Injection과 TLS CallBack

아까 만든 TLS callback 을 사용하는 프로세스에 dll injection을 진행하여 내가 만든 dll을 로드시키고, dllmain 실행 시점에 어떤 변화가 일어나는지 확인해보려고 한다.

int main() {
    char a = 0;
    while (1)
    {
        printf("main function called\n");
        scanf("%c",&a);
    }
}

별다른 동작이 없다면 main funtion called 라는 문자열이 반복해서 출력되는 코드이다.

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
          printf("\n****this is dll****\n");
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

여기에 인젝션될 DLL은 인젝션에 성공했다면 간단한 문자열만 출력하는 코드이다. 인젝터를 실행해 인젝션을 진행하면

반복문이 돌다가 특정 시점에 TLS 콜백함수가 실행되고, 인젝션된 DLL 함수의 동작이 진행된 후 TLS 함수가 한번 더 실행된다. 이러한 동작을 보이는 이유는? 그리고 그 시점은 어디인가.

DLL이 로드되면 프로세스에서 인터럽트가 발생하면서 새로 로드된 dll의 DLL main 함수가 실행되어야 하는데, 동일 프로세스에서 새로운 쓰레드가 생성되는 개념이기 때문에, dll의 EntryPoint 가 실행되기 전에 TLS CallBack함수가 먼저 실행된다.

따라서, DLL 인젝션이 발생하여 새로운 dll이 로드되는 경우, TLS 콜백함수가 등록된 다음이라면 새로운 쓰레드가 생성 될 때마다 해당 함수가 실행되는 셈.

두번 실행되는 이유는 TLS Callback 함수는 DLL_THREAD_ATTACH 와 DLL_THREAD_DETACH 알림 실행시에 호출되는데, 이는 쓰레드 생성과 해제시에 각각 발생하는 알림 이벤트이기 때문. 두 번의 알림이 오니까 두번 호출된다.


DisableThreadLibraryCalls()

DisableThreadLibraryCalls함수는 이런 tls 콜백함수의 실행으로부터 벗어나기 위해 실행되는 함수인데, 이름 그대로 쓰레드 라이브러리의 함수들을 비활성화 하는식으로 동작한다.

이 함수가 호출되면 DLL_THREAD_ATTACH 와 DLL_THREAD_DETACH 알림이 비활성화되어 DLL이 로드되고 쓰레드가 생성되어도 저 알림이 발생하지 않아 TLS_CallBack 함수가 호출되지 않는 것.

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
        DisableThreadLibraryCalls(hModule);
          printf("\n****this is dll****\n");
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

그러나 이 함수를 호출하는 코드를 작성하고 DLL을 만들어도, 여전히 TLS 함수는 잘만 호출되는걸로 보인다. 내가 잘못 알았거나, 코드를 잘못 적은듯.


정리

어찌됐든 TLS Callback은 안티 디버깅이나 DLL 인젝션방지 등에 활용하기 딱 좋은것 같다. 함수 후킹을 위해선 필연적으로 DLL Injection이 이뤄져야 하는데, 이 경우에 쓰레드가 생성되기 때문에 TLS Callback이 실행될 수 밖에 없기 때문. TLS 콜백함수가 엔트리포인트보다 우선되어 실행되니까 여러 안티 디버깅 기법들을 잔뜩 집어넣어볼 수 있다.

따라서 이를 우회하는 방법을 생각해봐야겠다. 쓰레드가 생성되어서 Callback을 호출하는 그 사이 시점 어딘가에서 조작이 가능할지도.