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

news2024/11/25 13:00:09

附件下载链接

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。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/27890.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【架构设计】作为架构师你应该掌握的画图技术

1.前言 大家知道&#xff0c;架构的过程其实就是建模的过程&#xff0c;那自然离不开架构图。那么&#xff0c;我们先来看几个问题。 &#xff08;1&#xff09;什么是架构图&#xff1f; 架构图 架构 图&#xff0c;用图的形式把系统架构展示出来&#xff0c;配上简单的文…

Rust学习笔记——安装、创建、编译、输入输出

目录 一.安装 二.创建 三.编译 四.输入输出 &#xff08;一&#xff09;.输出hello world &#xff08;二&#xff09;.输入 一.安装 Rust Programming Language (rust-lang.org)&#xff0c;这是Rust官网。 直接下载自己对应系统版本即可&#xff0c;小编是linux版。 下…

ARP协议map4(3层网络层的协议)

数据来源 一、广播与广播域概述 1、广播域广播域 广播&#xff1a;将广播地址做为目标地址的数据帧 广播域&#xff1a;网络中能接收到同一个广播所有节点的集合&#xff08;广播域越小越好&#xff0c;这样通信效率更高&#xff09; 下图每个圈都是一个广播域&#xff0c;说…

通关算法题之 ⌈数组⌋ 上

滑动窗口 3. 无重复字符的最长子串 给定一个字符串 s &#xff0c;请你找出其中不含有重复字符的 最长子串 的长度。 输入: s "abcabcbb" 输出: 3 这就是变简单了&#xff0c;连 need 和 valid 都不需要&#xff0c;而且更新窗口内数据也只需要简单的更新计数器 …

(续)SSM整合之SSM整合笔记(ContextLoaderListener)(P179-188)

一 准备工作 1 新建模块ssm com.atguigu.ssm 2 导入依赖 <packaging>war</packaging><properties><spring.version>5.3.1</spring.version> </properties><dependencies><dependency><groupId>org.springframew…

Day10--初步实现自定义tabBar的效果

承接上文配置信息之后。 1.添加tabBar代码文件 我的操作&#xff1a; 1》在文件区新建一个custom-tab-bar文件夹 2》并在其中新建一个index组件 3》文件区展示图 4》最终的效果图&#xff1a; **************************************************************************…

界面组件DevExpress Reporting v22.1亮点 - 报表设计器功能全面升级

DevExpress Reporting是.NET Framework下功能完善的报表平台&#xff0c;它附带了易于使用的Visual Studio报表设计器和丰富的报表控件集&#xff0c;包括数据透视表、图表&#xff0c;因此您可以构建无与伦比、信息清晰的报表。 DevExpress Reporting v22.1版本已正式发布&am…

数据结构之二叉树(前提知识)

文章目录前言**一、树****二、树的相关概念****节点的度****叶节点****分支节点****子节点****父节点****兄弟节点****树的度****节点的层****树的高度****祖先****子孙****森林****三、树的表示****孩子表示法****左孩子右兄弟法****双亲表示法****四、树在实际中的应用****总…

iPhone/iPad屏幕投屏镜像到PC或Mac上面教程分享

AirServer是一款Mac应用程序&#xff0c;可将AirPlay / AirTunes的音频&#xff0c;视频&#xff0c;照片&#xff0c;幻灯片和镜像接收功能添加到Mac电脑。它可以实现将iPhone手机或Mac电脑上的媒体文件以及其他操作投射到Mac电脑上。使用AirServer&#xff0c;可以从Mac&…

如何利用DGL官方库中的rgcn链接预测代码跑自己的数据集(如何在DGL库的链接预测数据集模块定义自己的数据集类)

最近在忙我的省创&#xff0c;是有关于知识图谱的&#xff0c;其中有一个内容是使用rgcn的链接预测方法跑自己的数据集&#xff0c;我是用的dgl库中给出的在pytorch环境下实现rgcn的链接预测的代码&#xff0c;相关链接贴在这里&#xff1a; dgl库中关于rgcn的介绍文档 dgl库…

联盟快应用-如何进行测试?

官方文档&#xff1a;快应用-无需安装&#xff0c;即点即用-厂商联盟官方网站 什么是快应用&#xff1f; 可以简单理解为是另一种小程序。 快应用是一种新的应用形态&#xff0c;以往的手机端应用主要有两种方式&#xff1a;网页、原生应用&#xff1b;网页无需安装&#xff…

阻塞车间调度

阻塞车间调度 当前机器上的作业处理必须保留在该机器上&#xff0c;直到下一台机器可用于处理为止。也就是说如果该作业要执行的下一个工序的机器被使用&#xff0c;则该机器必须被占用。 n个作业必须在m个机器f个工厂上进行处理&#xff0c;在每一个工厂中连续机器之间没有缓…

Android11 framework Handler

Android11 framework Handler引言Handler工作流程MessageQueue主要函数Looper主要函数思考1.一个线程有几个handler&#xff0c;有几个looper2.为什么handler会有内存泄漏3.如果想要在子线程new Handler怎么做&#xff1f;4.子线程中的loop如果消息队列中没有消息处理的时候怎么…

深入底层学git:目录中包含的秘密

1.Git简介 Git具有最优的存储能力&#xff0c;在没有远端git服务器的情况下&#xff0c;git本地就可以独立作为版本管控系统&#xff0c;这其中.git裸仓库中起了关键作用&#xff0c;那么我们一起来看看.git下都放了哪些文件。 打开Git Bash&#xff0c;切换到项目目录&#x…

王道考研——操作系统(第二章 进程管理)(进程;线程)

一、进程的概念、组成、特征 进程的概念 进程的组成——PCB 进程的组成——程序段、数据段 知识滚雪球&#xff1a;程序是如何运行的&#xff1f; 进程的组成 进程的特征 知识回顾与重要考点 二、进程的状态与转换 进程的状态——创建态、就绪态 进程的状态——运行态 进程的…

刷题日记【第十二篇】-笔试必刷题【洗牌+MP3光标位置+年终奖+迷宫问题】

洗牌【编程题】 import java.util.*;public class Main {// 左: i --> 2*i;// 右: in --> 2*i 1;private static void playCard(int[] cards, int n, int k ) {for (int i 0; i < k; i) {//一次洗牌的顺序int[] newCards new int[cards.length];//遍历编号为0-n-1…

【Servlet】2:认识一下Web服务器——Tomcat

目录 第三章 | Tomcat 认识与配置 | 章节概述 | HTTP服务器概述 | Tomcat 安装与配置 | Tomcat 的目录结构、端口号 第四章 | Tomcat 基本使用 | 章节概述 | 本地Tomcat 静态资源网站访问 | IDEATomcat 静态资源网站访问 | IDEA中最基础web项目的目录结构 本文章属于后…

从零开始操作系统-07:APIC

这一节主要主要是APIC。 所需要的文件在Github&#xff1a;https://github.com/yongkangluo/Ubuntu20.04OS/tree/main/Files/Lec7-ExternalInterrupt 历史方法&#xff1a;PIC&#xff08;Programmable Interrupt Controller&#xff09; Intel 8259&#xff1a; APIC&#…

小侃设计模式(十三)-策略模式

1.概述 策略模式&#xff08;Strategy Pattern&#xff09;是一种比较简单的模式&#xff0c;它定义了算法家族&#xff0c;分别封装起来&#xff0c;让它们之间可以互相替换&#xff0c;此模式让算法的变化&#xff0c;不会影响到使用算法的客户。策略模式具有较强的实用性&a…

ARM学习扫盲篇(一):CPSRSPSR、LcacheDcache、w/parityw/ECC

1、CPSR&SPSR CPSR—程序状态寄存器(current program status register) SPSR—程序状态保存寄存器&#xff08;saved program status register&#xff09; Icache&Dcache icache用来缓存指令&#xff1b; dcache用来缓存数据&#xff0c;dcache用的前提是mmu要启动…