security/리버싱핵심원리

TLS 콜백 함수

민사민서 2023. 4. 5. 19:01

TLS (Thread Local Storage)

- 스레드별로 가지는 독립된(고유한) 데이터 저장 공간

- 각각의 스레드는 고유한 스택을 갖기 때문에 스택 변수(지역 변수)는 스레드 별로 고유하다. 그러나 정적 변수와 전역 변수의 경우에는 프로세스 내의 모든 스레드에 의해서 공유된다 (= 공유 메모리 영역에 놓인다)

- 정적(static) 변수, 전역(global) 변수를 각각의 스레드에게 독립적으로 만들어 주고 싶을 때 사용한다

- 같은 문장(context)을 실행하고 있지만 실제로는 스레드 별로 다른 주소 공간을 상대로 작업하게 됨

 

TLS 콜백 함수

- 프로세스의 스레드가 생성/종료될 때마다 자동으로 호출되는 콜백 함수

- 쓰레드 전용으로 쓰이는 'TLS'특정 이벤트에 의해 호출되는 'Callback 함수'의 특성이 합쳐졌다고 볼 수 있다

- 프로세스의 메인 스레드가 생성될 때에도 콜백 함수가 호출되므로, EP 코드보다 먼저 호출된다

- 안티 디버깅 기법으로 사용된다

typedef VOID (NTAPI *PIMAGE_TLS_CALLBACK)(PVOID DllHandle, DWORD dwReason, PVOID Reserved);

* NTAPI (Native API) : Windows NT kernel-mode 함수들의 호출 규약을 나타내기 위한 매크로, WINAPI, __stdcall 매크로와 유사하다

* DllHandle: TLS 콜백 함수를 포함하는 DLL의 핸들

* dwReason: TLS 콜백 함수가 호출된 이유, DLL_PROCESS_ATTACH(1), DLL_PROCESS_DETACH(0), DLL_THREAD_ATTACH(2), DLL_THREAD_DETACH(3) 4가지의 값을 가질 수 있음

 

HelloTLS.exe 분석

- TLS 기능을 사용하도록 프로그래밍 시, IMAGE_NT_HEADER.DataDirectory[9]이 세팅됨

- RAW=7910 (RVA=9310)에는 IMAGE_TLS_DIRECTORY 구조체가 존재한다

* AddressOfCallBacks 멤버변수에는 TLS 콜백함수 주소 배열의 VA가 담겨있다

- VA=408114, RVA=8114, RAW=6714에 가보면 TLS 콜백 함수 주소 배열이 존재한다

* TLS 콜백함수가 0x401000 하나 등록되어 있다

- VA=0x401000 함수를 Ollydbg로 봐보면 아래와 같다

cf) kernel32!IsDebuggerPresent API 내부 구현은 아래와 같다. FS:[18]로 TEB를 구하고, [EAX+30]으로 PEB를 구하고, [EAX+2]로 BeingDebugged 멤버의 값을 확인한다

 

TlsTest.exe

#include <windows.h>

#pragma comment(linker, "/INCLUDE:__tls_used")

void print_console(char* szMsg)
{
    HANDLE hStdout = GetStdHandle(STD_OUTPUT_HANDLE);

    WriteConsoleA(hStdout, szMsg, strlen(szMsg), NULL, NULL);
}

void NTAPI TLS_CALLBACK1(PVOID DllHandle, DWORD Reason, PVOID Reserved)
{
    char szMsg[80] = {0,};
    wsprintfA(szMsg, "TLS_CALLBACK1() : DllHandle = %X, Reason = %d\n", DllHandle, Reason);
    print_console(szMsg);
}

void NTAPI TLS_CALLBACK2(PVOID DllHandle, DWORD Reason, PVOID Reserved)
{
    char szMsg[80] = {0,};
    wsprintfA(szMsg, "TLS_CALLBACK2() : DllHandle = %X, Reason = %d\n", DllHandle, Reason);
    print_console(szMsg);
}

#pragma data_seg(".CRT$XLX")
    PIMAGE_TLS_CALLBACK pTLS_CALLBACKs[] = { TLS_CALLBACK1, TLS_CALLBACK2, 0 };
#pragma data_seg()

DWORD WINAPI ThreadProc(LPVOID lParam)
{
    print_console("ThreadProc() start\n");

    print_console("ThreadProc() end\n");

    return 0;
}

int main(void)
{
    HANDLE hThread = NULL;

    print_console("main() start\n");

    hThread = CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL);
    WaitForSingleObject(hThread, 60*1000);
    CloseHandle(hThread);

    print_console("main() end\n");

    return 0;
}

- #pragma comment(linker, "/INCLUDE:__tls_used")를 통해 링커로 하여금 "__tls_used" 심볼을 포함시키도록 하며

- #pragma data_seg(".CRT$XLX") 를 통해 .CRT$XLX 섹션에 (this section name is convention for Microsoft Visual Stuido compiler&linker when working with TLS callbacks) 콜백 함수의 배열을 올바르게 추가한다

- TLS Callback은 메인스레드보다 먼저 호출되므로 printf()보다 WriteConsole() Low-Level API를 사용하자

 

TLS 콜백함수 분석

- System startup breakpoint (in ntdll.dll)에서 멈추도록 하여 TLS Callback 주소에 bp 걸고 달리거나

- 아예 TLS callback option에 체크하여 분석

 

HXD를 이용한 TLS 콜백 함수 추가

1. 마지막 섹션(.rsrc라고 하자)의 크기를 0x200 확장시킴 // File Alignment 고려

2. .rsrc의 섹션 헤더 수정

    - Size of Raw Data + 0x200

    - Characteristics에 MEM_SCN_CNT_CODE(0x20), MEM_SCN_CNT_EXECUTE/WRITE(0xA0000000) 추가

3. IMAGE_OPTIONAL_HEADER.DataDirectory[9]의 RVA, Size 세팅

4. .rsrc의 확장시킨 영역에 IMAGE_TLS_DIRECTORY 구조체(size 0x18) 세팅, TLS 함수주소 배열 세팅, TLS 함수 코드 세팅