2022CTF培训(二)Hook进阶&反调试

附件下载链接

Hook进阶

更精准的 Inline Hook

要求

  • 实现防止任务管理器对某进程自身的结束
  • 要求不影响任务管理器结束其它进程的功能

Dll 注入程序编写

提权

主要过程如下:

  • 首先,程序需要调用OpenProcessToken函数打开指定的进程令牌,并获取TOKEN_ADJUST_PRIVILEGES权限的令牌句柄。
  • 再接着调用LookupPrivilegeValue函数,获取本地系统指定特权名称的LUID值,这个LUID值相当于该特权的身份标识号。
  • 接着对进程令牌特权结构体TOKEN_PRIVILEGES进行赋值,设置新特权的数量、特权对应的LUID值以及特权的属性状态。
  • 最后,程序调用AdjustTokenPrivileges函数对进程令牌的特权进行修改,将上面设置好的新特权设置到进程令牌中,这样就完成了进程访问令牌的修改工作。
BOOL UpPriv() {
	HANDLE hToken;
	LUID luid;
	TOKEN_PRIVILEGES tp;
	if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken)) {
		std::cerr << "[-] Failed to open process token." << std::endl;
		return FALSE;
	}
	if (!LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &luid)) {
		std::cerr << "[-] Failed to lookup privilege value." << std::endl;
		return FALSE;
	}
	tp.PrivilegeCount = 1;
	tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
	tp.Privileges[0].Luid = luid;
	if (!AdjustTokenPrivileges(hToken, 0, &tp, sizeof(TOKEN_PRIVILEGES), NULL, NULL)) {
		std::cerr << "[-] Failed to adjust token privileges." << std::endl;
		return FALSE;
	}
	DWORD res = GetLastError();
	if (res == ERROR_SUCCESS) {
		std::cerr << "[+] Adjust token Privileges successufully." << std::endl;
		return TRUE;
	}
	if (res == ERROR_NOT_ALL_ASSIGNED) {
		std::cerr << "[-] The token does not have one or more of the privileges specified in the NewState parameter." << std::endl;
		return FALSE;
	}
	std::cerr << "[-] Unknown result." << std::endl;
	return FALSE;
}
获取任务管理器的进程句柄
  • 首先根据窗口名称获取窗口句柄

    	HWND hwnd = FindWindowExA(NULL, NULL, NULL,"任务管理器");
    	if (hwnd == NULL) {
    		std::cerr << "[-] Failed to find Taskmgr." << std::endl;
    		return 0;
    	}
    
  • 之后根据窗口句柄获得进程句柄

    HANDLE GetProcessFromHWND(HWND hwnd) {
    	DWORD pid;
    	GetWindowThreadProcessId(hwnd, &pid);
    	HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS | PROCESS_CREATE_THREAD, FALSE, pid);
    	if (hProcess == NULL) {
    		std::cerr << "[-] Failed to open process." << std::endl;
    	}
    	return hProcess;
    }
    
Dll注入
  • 调用 VirtualAllocEx 函数在目标进程中申请一段内存,属性为 PAGE_READWRITE,用来存放我们要注入的 Dll 的绝对路径。
  • 调用 WriteProcessMemory 将 Dll 的绝对路径写入到目标进程中使用 VirtualAllocEx 函数申请的空间中。
  • 调用 GetProcAddress 获得 LoadLibraryA 函数的地址。
  • 调用 CreateRemoteThread 在目标进程中创建一个新的 LoadLibraryA 线程,线程的参数设置为使用 VirtualAllocEx 申请的空间中存放的字符串的指针。
BOOL DoInjection(PCHAR DllPath, HANDLE hProcess) {
	DWORD BufSize = strlen(DllPath) + 1;
	LPVOID AllocAddr = VirtualAllocEx(hProcess, NULL, BufSize, MEM_COMMIT, PAGE_READWRITE);
	if (!WriteProcessMemory(hProcess, AllocAddr, DllPath, BufSize, NULL)) {
		std::cerr << "[-] Failed to write process memory." << std::endl;
		return FALSE;
	}
	HMODULE hModule = GetModuleHandle(TEXT("Kernel32"));
	if (hModule == NULL) {
		std::cerr << "[-] Failed to get module handle." << std::endl;
		return FALSE;
	}
	PTHREAD_START_ROUTINE pfnStartAddr = (PTHREAD_START_ROUTINE)GetProcAddress(hModule, "LoadLibraryA");
	if (pfnStartAddr == NULL) {
		std::cerr << "[-] Failed to get process address." << std::endl;
		return FALSE;
	}
	HANDLE hRemoteThread = CreateRemoteThread(hProcess, NULL, 0, pfnStartAddr, AllocAddr, 0, NULL);
	if (hRemoteThread == NULL) {
		std::cerr << "[-] Failed to create remote thread." << std::endl;
		return FALSE;
	}
	std::cerr << "[+] Inject dll successfully." << std::endl;
	return TRUE;
}

后面简单地通过 CreateRemoteThread 是否成功来判定我们注入 Dll 是否成功,实际上这种判断是非常不可靠的。我们可以设计一些进程间通讯的手段,当目标 Dll 成功注入到目标进程中的时候,传递一个消息给我们的注入程序,这种方式更加稳定。

Inline Hook Dll 编写

代码如下:

// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "pch.h"
#include "psapi.h"

CONST LPCTSTR ToProtect = TEXT("C:\\Users\\sky123\\Desktop\\MyDllInject.exe");
TCHAR FileName[MAX_PATH + 1];
BYTE JMP[5], TMP[5];
PVOID FuncAddr;
DWORD newProtect = PAGE_EXECUTE_READWRITE, oldProtect;

VOID HookTerminateProcess() {
    VirtualProtect(FuncAddr, 5, newProtect, &oldProtect);
    memcpy(FuncAddr, JMP, 5);
    VirtualProtect(FuncAddr, 5, oldProtect, &newProtect);
}

VOID UnHook() {
    VirtualProtect(FuncAddr, 5, newProtect, &oldProtect);
    memcpy(FuncAddr, TMP, 5);
    VirtualProtect(FuncAddr, 5, oldProtect, &newProtect);
}

BOOL WINAPI MyTerminateProcess(HANDLE hProcess, UINT uExitCode) {
    GetModuleFileNameEx(hProcess,0, FileName, MAX_PATH);
    if (!lstrcmp(FileName, ToProtect)) {
        MessageBox(0, TEXT("You can't kill me!"), TEXT("INFO"), MB_OK);
        return FALSE;
    } else {
        UnHook();
        BOOL ret = TerminateProcess(hProcess, uExitCode);
        HookTerminateProcess();
        return ret;
    }
}

VOID InitHook() {
    HMODULE hModule = LoadLibrary(L"Kernel32.dll");
    if (hModule == NULL) {
        MessageBox(NULL, L"Failed to load library.", NULL, 0);
        return;
    }
    FuncAddr = (void*)GetProcAddress(hModule, "TerminateProcess");
    if (FuncAddr == NULL) {
        MessageBox(NULL, L"Failed to get process address.", NULL, 0);
        return;
    }
    memcpy(TMP, FuncAddr, 5);
    JMP[0] = 0xE9;
    *(DWORD*)&JMP[1] = (DWORD)MyTerminateProcess - (DWORD)FuncAddr - 5;
}

BOOL APIENTRY DllMain( HMODULE hModule,DWORD  ul_reason_for_call,LPVOID lpReserved) {
    switch (ul_reason_for_call){
    case DLL_PROCESS_ATTACH:
        InitHook();
        HookTerminateProcess();
        break;
    case DLL_THREAD_ATTACH:
        break;
    case DLL_THREAD_DETACH:
        break;
    case DLL_PROCESS_DETACH:
        UnHook();
        break;
    }
    return TRUE;
}

在 MyTerminateProcess 函数中做了判断,如果是需要保护的进程则返回 FALSE,否则脱钩调用 TerminateProcess,然后恢复钩子最后将执行结果返回。

运行效果

使用管理员权限运行注入程序。
发现受保护的进程无法被任务管理器结束,对于其他进程则可以正常结束。
在这里插入图片描述

Detours 库的使用

Detours 介绍

已在 GitHub 上被开源

是微软官方的 Hook 软件包,用于监视和检测 Windows 上的 API 调用(实际上不仅仅限于对 API 函数的调用监控,还可以实现任意函数调用的监控,得益于非常易于使用的调用方式)。

Detours 可以用于拦截 ARM,x86,x64 和 IA64 计算机上的二进制函数。 Detours 最常用于拦截应用程序中的 Win32 API 调用,例如添加调试用的工具、 拦截代码在运行时动作等等。 通过跳转到用户提供的回调函数的无条件来替换目标功能的前几条指令。 来自目标功能的指令将被放置在“蹦床”上, 蹦床的地址位于目标指针中。

Detours 在运行时进行 Hook,目标函数的代码仅仅在内存中被修改(而不是被修改了二进制文件),这使得我们能以很细的粒度进行二进制程序的分析。

Detours 的基本使用方法

一般的调用框架如下:

  • DetourTransactionBegin();
  • DetourUpdateThread(GetCurrentThread());
  • DetourAttach() / DetourDetach()
  • DetourTransactionCommit()

需要遵循以上调用规则,可以保证线程安全等问题。
代码如下:

// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "pch.h"
#include "psapi.h"

CONST LPCTSTR ToProtect = TEXT("C:\\Users\\sky123\\Desktop\\MyDllInject.exe");
TCHAR FileName[MAX_PATH + 1];
static BOOL(WINAPI* OrgTerminateProcess)(HANDLE hProcess, UINT uExitCode) = NULL;

BOOL WINAPI MyTerminateProcess(HANDLE hProcess, UINT uExitCode) {
	GetModuleFileNameEx(hProcess, 0, FileName, MAX_PATH);
	if (!lstrcmp(FileName, ToProtect)) {
		MessageBox(0, TEXT("You can't kill me!"), TEXT("INFO"), MB_OK);
		return FALSE;
	}
	return OrgTerminateProcess(hProcess, uExitCode);
}

BOOL InitHook() {
	DetourTransactionBegin();
	DetourUpdateThread(GetCurrentThread());
	HMODULE hModule = LoadLibrary(L"Kernel32.dll");
	if (hModule == NULL) {
		MessageBox(NULL, L"Failed to load library.", NULL, 0);
		return FALSE;
	}
	OrgTerminateProcess = (BOOL(WINAPI*)(HANDLE, UINT))GetProcAddress(hModule, "TerminateProcess");
	if (OrgTerminateProcess == NULL) {
		MessageBox(NULL, L"Failed to get process address.", NULL, 0);
		return FALSE;
	}
	DetourAttach(&(PVOID&)OrgTerminateProcess, MyTerminateProcess);
	return !DetourTransactionCommit();
}

BOOL DetachHook() {
	DetourTransactionBegin();
	DetourUpdateThread(GetCurrentThread());
	DetourDetach(&(PVOID&)OrgTerminateProcess, MyTerminateProcess);
	return !DetourTransactionCommit();
}

BOOL APIENTRY DllMain(HMODULE hModule, DWORD  ul_reason_for_call, LPVOID lpReserved) {
	switch (ul_reason_for_call)	{
	case DLL_PROCESS_ATTACH:
		if (!InitHook()) {
			MessageBox(NULL, L"Failed to hook.", NULL, 0);
		}
		break;
	case DLL_THREAD_ATTACH:
		break;
	case DLL_THREAD_DETACH:
		break;
	case DLL_PROCESS_DETACH:
		DetachHook();
		break;
	}
	return TRUE;
}

可以实现相同的效果:
在这里插入图片描述
用火绒剑分析,发现 Detours 在函数入口点处的 hook 与前面实现的 inline Hook 相同。
在这里插入图片描述
跟进去发现是 MyTerminateProcess 函数。
在这里插入图片描述
在判断出参数对应的进程不是需要保护的进程的时候会跳转到 OrgTerminateProcess,OrgTerminateProcess 处的反汇编如下。因此 Detours 保证 Hook 多线程的方法是构造一个新的入口点。
在这里插入图片描述

多线程同步问题引入

假设两个线程 A、B 同时调用了被 Hook 的函数,那么有一种可能的执行顺序如下:
在这里插入图片描述
很明显,B 在运行原函数的时候会发生致命错误。

反调试

调用系统 API

使用 Windows API 函数检测调试器是否存在是最简单的反调试技术。Windows 操作系统中提供了这样一些 API ,应用程序可以通过调用这些 API,来检测自己是否正在被调试。

调用 IsDebuggerPresent() API

VOID CheckDebug1() {
    std::cout << "[*] IsDebuggerPresent(): " << (IsDebuggerPresent() ? "True" : "False") << std::endl;;
}

该函数对应的汇编如下:
在这里插入图片描述
其中 fs:[0] 指向的是线程环境块(TEB),在 TEB 0x30 偏移处存储的是进程环境快(PEB)的地址。在 PEB 0x2 偏移处存储的是一字节长度的 BeingDebugged 标志位。因此该函数本质是读取该进程对应 PEB 的 BeingDebugged 标志位并返回。

调用 CheckRemoteDebuggerPresent() API

VOID CheckDebug2() {
    BOOL ret;
    CheckRemoteDebuggerPresent(GetCurrentProcess(), &ret);
    std::cout << "[*] CheckRemoteDebuggerPresent: " << (ret ? "True" : "False") << std::endl;
}

如图,该函数在判断 PRocessId 不是 0 以及检查输出参数指针是否为 NULL 后就调用了 NtQueryInformationProcess 函数。
在这里插入图片描述

调用 NtQueryInformationProcess() API

该函数未导出,并且貌似在 x64 下用不了。

VOID CheckDebug3() {
    HMODULE hModule = LoadLibrary(TEXT("Ntdll.dll"));
    NtQueryInformationProcessPtr NtQueryInformationProcess = (NtQueryInformationProcessPtr)GetProcAddress(hModule, "NtQueryInformationProcess");
    INT debugPort;
    NTSTATUS ret = NtQueryInformationProcess(GetCurrentProcess(), (PROCESSINFOCLASS)0x7, &debugPort, sizeof(debugPort), NULL);
    if (!NT_SUCCESS(ret)) {
        std::cerr << "[-] NtQueryInformationProcess error: " << std::hex << ret << std::endl;
        return;
    }
    std::cout << "[*] NtQueryInformationProcess: " << (debugPort ? "True" : "False") << std::endl;
}

运行效果

  • 正常运行,不调试
    在这里插入图片描述
  • 调试状态:
    在这里插入图片描述
  • OD 调试:
    在这里插入图片描述

检查软断点

  • 进行断点检测,普通调试器往往通过对代码进行修改实现断点
  • 修改代码将会引起程序内存的变化
  • 检测内存是否变化即可知道有无断点

重点检测对象

  • 校验函数
  • 系统 API 首字节

检测方式(代码块校验)

  • 首次运行 TimerProc 时,计算校验和并保存
  • 第二次运行 TimerProc 时,计算校验和是否与首次计算的值相等
  • 如果不相等则表明存在断点
  • 进行反调试操作

检测方式(函数首字节校验)

  • 读取常用系统 API 的首字节,判断其是否为 0xCC
  • 如果是 0xCC,则证明该 API 被下了软件断点
  • 进行反调试操作

关键代码

void CALLBACK TimerProc(
	HWND hWnd,      // handle of CWnd that called SetTimer
	UINT nMsg,      // WM_TIMER
	UINT_PTR nIDEvent,   // timer identification
	DWORD dwTime    // system time
) {

	DWORD pid;
	GetWindowThreadProcessId(hWnd, &pid);
	HANDLE handle = OpenProcess(PROCESS_ALL_ACCESS, false, pid);
	DWORD addr1 = MyGetProcAddress(GetModuleHandleA(("User32.dll")), "MessageBoxW"); //apphelp.dll
	DWORD addr2 = MyGetProcAddress(GetModuleHandleA("User32.dll"), "GetWindowTextW");


	char buf1, buf2;
	char buf3[200] = { 0 };
	SIZE_T size;
#define check_size 200
	ReadProcessMemory(handle, (LPCVOID)addr1, &buf1, 1, &size);	//MessageBoxW首字节
	ReadProcessMemory(handle, (LPCVOID)addr2, &buf2, 1, &size);	//GetWindowTextW首字节
	ReadProcessMemory(handle, (LPCVOID)addr3, &buf3, check_size, &size);	//OnBnClickedButton1函数中抽取200个字节

	int currentSum = 0;
	for (int i = 0; i < check_size; i++) {
		currentSum += buf3[i];
	}
	if (sum) {
		if (currentSum != sum) {
			TerminateProcess(handle, 1);
		}
	}
	else {
		sum = currentSum;
	}

	if ((byte)buf1 == 0xcc || (byte)buf2 == 0xcc) {
		TerminateProcess(handle,1);
	}
	CloseHandle(handle);

}

该部分代码实现了两个功能,一个是对代码块的断点检测,一旦在指定的范围内出现了断点,则会调用 TerminateProcess 自动退出程序;第二个是对两个系统 API 的断点检测,两个 API 的赋值均通过 GetModuleHandleA 再通过自己编写的 GetProcAddress 来获取。

使用自己编写的 GetProcAddress 目的是避免因为高版本的兼容问题而获取到不正确的函数地址,其定义如下:

DWORD MyGetProcAddress(HMODULE hModule, LPCSTR lpProcName)
{
	PIMAGE_EXPORT_DIRECTORY pImageExportDirectory = (PIMAGE_EXPORT_DIRECTORY)((PIMAGE_NT_HEADERS((DWORD)hModule + ((PIMAGE_DOS_HEADER)((DWORD)hModule))->e_lfanew))->OptionalHeader.DataDirectory[0].VirtualAddress + (DWORD)hModule);
	for (int i = 0; i < pImageExportDirectory->NumberOfNames; ++i)
	{
		DWORD dwAdName = *(DWORD*)((DWORD)hModule + pImageExportDirectory->AddressOfNames + i * sizeof(DWORD)) + (DWORD)hModule;
		if (lstrcmpiA((char*)dwAdName, lpProcName) == 0)
		{
			WORD index = *(DWORD*)((DWORD)hModule + pImageExportDirectory->AddressOfNameOrdinals + i * sizeof(WORD));
			DWORD dwFuncRVA = (DWORD)hModule + pImageExportDirectory->AddressOfFunctions + index * sizeof(DWORD);
			return *(DWORD*)dwFuncRVA + (DWORD)hModule;
		}
	}
	return 0;
}

该函数的实现原理即遍历进程模块的导入表查询函数名对应的函数地址。

程序初始化部分代码

该部分代码将校验函数 OnBnClickedButton1 赋值给 addr3 变量,并且开启 Timer,进行定时的断点检测。

addr3 = (DWORD)pointer_cast<void*>(&CMFCApplication1Dlg::OnBnClickedButton1);
SetTimer(1, 100, TimerProc);

调试验证

运行程序,在 MessageBoxW 或 GetWindowTextW 处下断点,程序立刻退出。
在这里插入图片描述
绕过方法:在函数下一条指令处下断点或直接下硬件断点。

在这里插入图片描述
根据弹窗的字符串在汇编界面进行搜索
在这里插入图片描述
找到相关字符串
在这里插入图片描述
进而找到关键代码
在这里插入图片描述
但是由于前面的反调试,在此处代码下断点就会导致进程结束。
绕过方法:

  • 在 TerminateProcess 函数下断点。
  • 通过返回地址回溯调用 TerminateProcess 的位置在这里插入图片描述
    在这里插入图片描述
  • 调用 TerminateProcess 的地方都 patch 掉。
    在这里插入图片描述
  • patch 后的版本可以成功在 MessageBoxW处 下断点。
    在这里插入图片描述

CheckTime

原理

单步跟踪的时间与直接运行的时间相差巨大。

使用方法及代码

  • RDTSC(CPU Cycle)
    x86 CPU 中存在一个名为 TSC(Time Stamp Counter,时间戳计数器)的 64 位寄存器。CPU 对每个时钟周期计数,然后保存到 TSC,RDTSC 指令便是用来将 TSC 的值读入 EDX:EAX 寄存器中。

    u64 RDTSC() {
    	u32 timeH, timeL;
    	__asm {
    		rdtsc
    		mov timeL, eax
    		mov timeH, edx
    	}
    	return  u64(timeH) << 32 | timeL;
    }
    
    bool CheckDebugRDTSC(char input[]) {
    	u64 time = RDTSC();
    	bool nice = false;
    	if (!strcmp(input, flag)) {
    		 nice = true;
    	}
    	if (RDTSC() - time > 0x1000) {
    		exit(-1);
    	}
    	return nice;
    }
    
  • GetTickCount()(最近系统重启时间与当前时间的相差毫秒数)

    bool CheckDebugGetTickCount(char input[]) {
    	u32 time = GetTickCount();
    	bool nice = false;
    	if (!strcmp(input, flag)) {
    		nice = true;
    	}
    	if (GetTickCount() - time > 1000) {
    		exit(-1);
    	}
    	return nice;
    }
    

SEH 异常反调试

SEH(结构化异常处理)可以说是 Win32 操作系统提供的所有功能中使用最广泛而又没有公开的功能之一了,它是操作系统提供的服务,简单的来说,它是一种能让一个线程出现错误的时候操作系统调用用户自定义的回调函数的机制,无论回调函数做了什么事情,它都需要返回一个值,来告诉操作系统接下来是否应该继续查找下一个回调函数、下一步应该做什么。

回调函数形式

LONG NTAPI SEHandler(EXCEPTION_POINTERS *ExceptionInfo);

查看 winnt.h 中对该结构体的定义可以发现:

typedef struct _EXCEPTION_POINTERS {
    PEXCEPTION_RECORD ExceptionRecord;
    PCONTEXT ContextRecord;
} EXCEPTION_POINTERS, *PEXCEPTION_POINTERS;

其中的 ContextRecord 是我们需要关注的内容,里面包括了许多有用的信息,包括产生异常时的线程上下文状态中的所有信息(例如通用寄存器、段选择子、IP 寄存器等等),我们可以通过这些信息方便地控制异常处理的进行。

例如,如果我们需要在异常发生的时候,将 Eip 的值增加 1 后继续执行,那么可以使用如下回调函数:

LONG NTAPI SEHandler(EXCEPTION_POINTERS *ExceptionInfo) {
	ExceptionInfo->ContextRecord->Eip += 1;
	return EXCEPTION_CONTINUE_EXECUTION;
}

该函数在返回的时候返回了 EXCEPTION_CONTINUE_EXECUTION,表示告诉操作系统恢复产生异常线程的执行;与之对应的值是 EXCEPTION_CONTINUE_SEARCH,当回调函数无法处理相应异常时,需要返回这个数值以告诉操作系统继续寻找下一个回调函数,或者是弹出最终的错误提示框,崩溃程序(xxx 程序停止工作)。

SEH 链的构造

往往通过如下操作构造一个 EXCEPTION_REGISTRATION 结构体:

PUSH handler
PUSH FS:[0]

执行完这两条指令之后,栈中会有一个 8 字节的 EXCEPTION_REGISTRATION 结构体,随后往往会有一条像下面这样的指令讲构造好的 EXCEPTION_REGISTRATION 结构体链接到当前的 SEH 链上:

MOV FS:[0],ESP

这个操作会使得线程信息块(TIB)中的第一个 DWORD 指向了新的 EXCEPTION_REGISTRATION 结构。

TEB

TEB(Thread Environment Block,线程环境块)。

系统在此TEB中保存频繁使用的线程相关的数据。位于用户地址空间,在比 PEB 所在地址低的地方。进程中的每个线程都有自己的一个 TEB。一个进程的所有 TEB 都以堆栈的方式,存放在从 0x7FFDE000 开始的线性内存中,每 4KB 为一个完整的 TEB,不过该内存区域是向下扩展的。在用户模式下,当前线程的 TEB 位于独立的 4KB 段,可通过 CPU 的 FS 寄存器来访问该段,一般存储在 FS:[0]。

// 
// Thread Environment Block (TEB) 
// 
typedef struct _TEB { 
	NT_TIB Tib; /* 00h */ 
	PVOID EnvironmentPointer; /* 1Ch */ 
	CLIENT_ID Cid; /* 20h */ 
	PVOID ActiveRpcHandle; /* 28h */ 
......

其中有我们关注的 TIB,其结构如下:

typedef struct _NT_TIB {
    struct _EXCEPTION_REGISTRATION_RECORD *ExceptionList;
    PVOID StackBase;
    PVOID StackLimit;
    PVOID SubSystemTib;
    union {
        PVOID FiberData;
        DWORD Version;
    };
    PVOID FiberData;
    PVOID ArbitraryUserPointer;
    struct _NT_TIB *Self;
} NT_TIB;
typedef NT_TIB *PNT_TIB;

TIB 的第一个参数 ExceptionList 指向的便是 SEH 链表,它很简单,其大致定义如下:

typedef struct _EXCEPTION_REGISTRATION_RECORD{
    PEXCEPTION_REGISTRATION_RECORD Next;
    PEXCEPTION_DISPOSITION Handler;
} EXCEPTION_REGISTRATION_RECORD;

这便是要使用 MOV FS:[0], ESP 进行链表设置的原因。

反调试应用

// SEH.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

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

unsigned int flag;

LONG WINAPI VEHandler(EXCEPTION_POINTERS* pExceptionInfo) {
	puts("[*] VEHandler");
	return EXCEPTION_CONTINUE_SEARCH;
}

LONG WINAPI VCHandler(EXCEPTION_POINTERS* pExceptionInfo) {
	puts("[*] VCHandler");
	pExceptionInfo->ContextRecord->Eax = -1;
	pExceptionInfo->ContextRecord->Eip += 2;
	return EXCEPTION_CONTINUE_SEARCH;
}

LONG WINAPI TopLevelExcepFilter(EXCEPTION_POINTERS* pExceptionInfo) {
	puts("[*] TopLevelExcepFilter");

	return EXCEPTION_CONTINUE_EXECUTION;
}

LONG WINAPI SEHandler(EXCEPTION_POINTERS* pExceptionInfo) {
	puts("[*] SEHandler");
	return EXCEPTION_CONTINUE_SEARCH;
}

int main() {
	AddVectoredExceptionHandler(0, VEHandler);
	AddVectoredContinueHandler(0, VCHandler);
	SetUnhandledExceptionFilter(&TopLevelExcepFilter);
	puts("Input your flag:");
	if (!scanf_s("%4d", &flag, 4)) {
		puts("Invalid flag!");
		return 0;
	}
	__try {
		int result = flag * flag + 1;

		if (result / flag == -1) {
			puts("Congratulations~");
		}
		else {
			puts("Invalid flag!");
		}
	}
	__except (SEHandler(GetExceptionInformation())) {

	}
	system("pause");
	return 0;
}

当输入 input = 0 时,如果是未调试状态,则运行结果如下:
在这里插入图片描述
这些函数依次接管了除零异常,其中 VCHandler 通过将 eip += 2 的方式跳过出错位置修复了异常,并且将存放 result 的 eax 寄存器置为 -1 。
如果是调试状态则,对于 OllyDbg ,该调试器提前会接管异常并使程序退出,从而起到反调试的效果。
但 x32dbg 对这种异常会不做处理,不会影响执行结果。

INT 2D

INT 2D 原为内核模式中用来触发断点异常的指令,也可以在用户模式下触发异常。但程序调试运行时不会触发异常,只是忽略。这种在正常运行与调试运行中表现出的不同,可以很好地应用到反调试技术中。

在调试模式中执行完 INT 2D 指令后,下一条指令的第一个字节将被忽略,后一个字节将会被识别为新的指令继续执行。

SEH 里得到的 INT 2D 异常的 EIP 也是指令 +2 的地址。

得益于上面的特性,单步执行该指令时,调试器并不会在下一条指令处断下来,而达到一种 F9 的效果。

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

char flag[5];

LONG WINAPI SEHandler(PEXCEPTION_POINTERS pExceptionInfo) {
	if (!strcmp(flag, "flag")) {
		pExceptionInfo->ContextRecord->Eip += 29;
	}else {
		pExceptionInfo->ContextRecord->Eip += 1;
	}
	return EXCEPTION_CONTINUE_EXECUTION;
}

int main() {
	puts("Input your flag:");
	if (!scanf_s("%4s", &flag, 5)) {
		puts("Wrong flag!");
		return 0;
	}
	__try {
		__asm {
			INT 0x2D
			_EMIT 0x15
			_EMIT 0x90
		}
		if (flag[0] == 1 && flag[1] == 2 && flag[2] == 3) {
			puts("Congratulations~");
		}else {
			puts("Wrong flag!");
		}
	}
	__except (SEHandler(GetExceptionInformation())) {}
	system("pause");
	return 0;
}

上面的代码中 int 0x2d 会触发异常,在异常接管函数 SEHandler 中,ContextRecord 中的 EIP 指向的位置如下图所示

在 SEHandler 中检验 flag ,根据 flag 是否正确将 EIP 加相应的值从而输出对应的结果。
直接运行效果如下:
在这里插入图片描述
在这里插入图片描述

使用 OllyDbg 调试,单步跟踪到 0x2d 后断不下来。
在 int 0x2d 后面的 nop 下断点,发现可以断在这里:
在这里插入图片描述
继续单步跟踪发现程序执行了主函数中检验 flag 的代码。说明 OllyDbg 先于 SEHandler 接管了异常,并且将 EIP 加 1 处理了异常然后让程序继续执行,从而在调试状态下执行了错误的流程:
在这里插入图片描述

但是在其他位置下断点或者在 nop 处下硬件断点不会影响程序的执行流程,可能与调试器的内部机制有关。

TLS 反调试

hread Local Storage(TLS),即线程本地存储,是 Windows 为解决一个进程中多个线程同时访问全局变量而提供的机制。

所谓 TLS 回调函数是指,每当创建/终止进程时会自动调用执行的函数。有意思的是,创建进程的主线程时也会自动调用回调函数,且其调用先于 EP 代码。反调试技术利用的就是 TLS 回调函数的这一特征。

TLS不是直接进行反调试,而是利用TLS的特性将反调试提前进行。

回调函数的定义

void NTAPI __stdcall TLS_CALLBACK(PVOID DllHandle, DWORD Reason, PVOID Reserved);

通过下面代码通知链接器加入 TLS 回调函数:

// linker spec 通知链接器 PE 文件要创建 TLS 目录,注意 X86 和 X64 的区别
#ifdef _M_IX86
#pragma comment (linker, "/INCLUDE:__tls_used")
#pragma comment (linker, "/INCLUDE:__tls_callback")
#else
#pragma comment (linker, "/INCLUDE:_tls_used")
#pragma comment (linker, "/INCLUDE:_tls_callback")
#endif
// 创建 TLS 段
EXTERN_C
#ifdef _M_X64
#pragma const_seg (".CRT$XLB")
const
#else
#pragma data_seg (".CRT$XLB")
#endif
// end linker

// TLS Import 定义多个回调函数
PIMAGE_TLS_CALLBACK _tls_callback[] = { TLS_CALLBACK, 0 };
#pragma data_seg ()
#pragma const_seg ()
// end 

随后我们可以在 TLS 回调函数中采用如下的方式进行反调试:

extern "C" NTSTATUS NTAPI NtQueryInformationProcess(HANDLE hProcess, ULONG InfoClass, PVOID Buffer, ULONG Length, PULONG ReturnLength);

VOID NTAPI TLS_CALLBACK(PVOID DllHandle, DWORD Reason, PVOID Reversed) {
	HANDLE DebugPort = NULL;
	if (!NtQueryInformationProcess(GetCurrentProcess(), 7, &DebugPort, sizeof(HANDLE), NULL)) {
		if (DebugPort) {
			MessageBoxA(NULL,"TLS_CALLBACK: Debugger detect!","TLS_CALLBACK", MB_ICONSTOP);
			TerminateProcess(GetCurrentProcess(), 1);
		}
	}
}

未公开 API

主要介绍 ZwSetInformationThread() API,它是一个未被公开的 API,效果强大。

如果有调试器挂载在目标进程上,并且传递 0x11 给这个函数的第二个参数,操作系统将会立即迫使所有已挂载的调试器 detach,并且终止进程。
示例代码如下:

#include <iostream>
#include<Windows.h>

typedef enum _THREADINFOCLASS {
	ThreadBasicInformation, // 0 Y N
	ThreadTimes, // 1 Y N
	ThreadPriority, // 2 N Y
	ThreadBasePriority, // 3 N Y
	ThreadAffinityMask, // 4 N Y
	ThreadImpersonationToken, // 5 N Y
	ThreadDescriptorTableEntry, // 6 Y N
	ThreadEnableAlignmentFaultFixup, // 7 N Y
	ThreadEventPair, // 8 N Y
	ThreadQuerySetWin32StartAddress, // 9 Y Y
	ThreadZeroTlsCell, // 10 N Y
	ThreadPerformanceCount, // 11 Y N
	ThreadAmILastThread, // 12 Y N
	ThreadIdealProcessor, // 13 N Y
	ThreadPriorityBoost, // 14 Y Y
	ThreadSetTlsArrayAddress, // 15 N Y
	ThreadIsIoPending, // 16 Y N
	ThreadHideFromDebugger // 17 N Y
} THREAD_INFO_CLASS;

typedef NTSTATUS(NTAPI* pZwSetInformationThread)(
	IN HANDLE ThreadHandle,
	IN THREAD_INFO_CLASS ThreadInformaitonClass,
	IN PVOID ThreadInformation,
	IN ULONG ThreadInformationLength
	);

int main() {
	pZwSetInformationThread ZwSetInformationThread = (pZwSetInformationThread)GetProcAddress(LoadLibrary(TEXT("ntdll.dll")), "ZwSetInformationThread");
	ZwSetInformationThread(GetCurrentThread(), ThreadHideFromDebugger, NULL, NULL);
	std::cout << "Hello, World!" << std::endl;
	return 0;
}

CTF 逆向程序中很喜欢使用这种技巧,pass 的方法很简单,直接将函数(以及参数)的调用(传递)部分全部 NOP 即可,或者将参数 0x11 改为无伤大雅的 0x0。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

_sky123_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值