鉴于 LitCTF 耻辱性的大败,加上最近一批课程结课,空出了一些时间,因此开始着手系统学习一下反调试了。
反调试简介
严格意义上的反调试指的是反动态调试。动态调试可以验证静态分析结果和观察程序运行时数据,因此程序开发者通常会设置反调试来检测自己开发的程序是否正在被调试。
不管是什么反调试,都有绕过方法。反调试和反反调试一直处于对抗状态。
Windows 平台反调试
基于 PEB 的反调试
PEB(Process Environment Block,进程环境块)是一个存放进程信息的结构体。
官方文档 没有完整记录这个结构体的信息,需要通过 WinDbg 或者第三方文档查看。完整结构非常长,我们只会涉及其中一少部分变量。
PEB 的完整信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70
| struct _PEB { 0x000 BYTE InheritedAddressSpace; 0x001 BYTE ReadImageFileExecOptions; 0x002 BYTE BeingDebugged; 0x003 BYTE SpareBool; 0x004 void* Mutant; 0x008 void* ImageBaseAddress; 0x00c _PEB_LDR_DATA* Ldr; 0x010 _RTL_USER_PROCESS_PARAMETERS* ProcessParameters; 0x014 void* SubSystemData; 0x018 void* ProcessHeap; 0x01c _RTL_CRITICAL_SECTION* FastPebLock; 0x020 void* FastPebLockRoutine; 0x024 void* FastPebUnlockRoutine; 0x028 DWORD EnvironmentUpdateCount; 0x02c void* KernelCallbackTable; 0x030 DWORD SystemReserved[1]; 0x034 DWORD ExecuteOptions:2; 0x034 DWORD SpareBits:30; 0x038 _PEB_FREE_BLOCK* FreeList; 0x03c DWORD TlsExpansionCounter; 0x040 void* TlsBitmap; 0x044 DWORD TlsBitmapBits[2]; 0x04c void* ReadOnlySharedMemoryBase; 0x050 void* ReadOnlySharedMemoryHeap; 0x054 void** ReadOnlyStaticServerData; 0x058 void* AnsiCodePageData; 0x05c void* OemCodePageData; 0x060 void* UnicodeCaseTableData; 0x064 DWORD NumberOfProcessors; 0x068 DWORD NtGlobalFlag; 0x070 _LARGE_INTEGER CriticalSectionTimeout; 0x078 DWORD HeapSegmentReserve; 0x07c DWORD HeapSegmentCommit; 0x080 DWORD HeapDeCommitTotalFreeThreshold; 0x084 DWORD HeapDeCommitFreeBlockThreshold; 0x088 DWORD NumberOfHeaps; 0x08c DWORD MaximumNumberOfHeaps; 0x090 void** ProcessHeaps; 0x094 void* GdiSharedHandleTable; 0x098 void* ProcessStarterHelper; 0x09c DWORD GdiDCAttributeList; 0x0a0 void* LoaderLock; 0x0a4 DWORD OSMajorVersion; 0x0a8 DWORD OSMinorVersion; 0x0ac WORD OSBuildNumber; 0x0ae WORD OSCSDVersion; 0x0b0 DWORD OSPlatformId; 0x0b4 DWORD ImageSubsystem; 0x0b8 DWORD ImageSubsystemMajorVersion; 0x0bc DWORD ImageSubsystemMinorVersion; 0x0c0 DWORD ImageProcessAffinityMask; 0x0c4 DWORD GdiHandleBuffer[34]; 0x14c void (*PostProcessInitRoutine)(); 0x150 void* TlsExpansionBitmap; 0x154 DWORD TlsExpansionBitmapBits[32]; 0x1d4 DWORD SessionId; 0x1d8 _ULARGE_INTEGER AppCompatFlags; 0x1e0 _ULARGE_INTEGER AppCompatFlagsUser; 0x1e8 void* pShimData; 0x1ec void* AppCompatInfo; 0x1f0 _UNICODE_STRING CSDVersion; 0x1f8 void* ActivationContextData; 0x1fc void* ProcessAssemblyStorageMap; 0x200 void* SystemDefaultActivationContextData; 0x204 void* SystemAssemblyStorageMap; 0x208 DWORD MinimumStackCommit; } PEB, *PPEB;
|
PEB 位于 32 位架构的 FS 段和 64 位架构的 GS 段中,偏移量为 0x30
。这些内存段中还存有 TEB(Thread Environment Block,线程环境块),包含进程中运行线程的各种信息。进程中的每个线程都对应着一个 TEB 结构体。
TEB 的完整信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
| typedef struct _TEB { NT_TIB NtTib; PVOID EnvironmentPointer; CLIENT_ID ClientId; PVOID ActiveRpcHandle; PVOID ThreadLocalStoragePointer; PPEB ProcessEnvironmentBlock; ULONG LastErrorValue; ULONG CountOfOwnedCriticalSections; PVOID CsrClientThread; PVOID Win32ThreadInfo; ULONG User32Reserved[26]; ULONG UserReserved[5]; PVOID WOW32Reserved; ULONG CurrentLocale; ULONG FpSoftwareStatusRegister; VOID * SystemReserved1[54]; LONG ExceptionCode; PACTIVATION_CONTEXT_STACK ActivationContextStackPointer; UCHAR SpareBytes1[36]; ULONG TxFsContext; GDI_TEB_BATCH GdiTebBatch; CLIENT_ID RealClientId; PVOID GdiCachedProcessHandle; ULONG GdiClientPID; ULONG GdiClientTID; PVOID GdiThreadLocalInfo; ULONG Win32ClientInfo[62]; VOID * glDispatchTable[233]; ULONG glReserved1[29]; PVOID glReserved2; PVOID glSectionInfo; PVOID glSection; PVOID glTable; PVOID glCurrentRC; PVOID glContext; ULONG LastStatusValue; UNICODE_STRING StaticUnicodeString; WCHAR StaticUnicodeBuffer[261]; PVOID DeallocationStack; VOID * TlsSlots[64]; LIST_ENTRY TlsLinks; PVOID Vdm; PVOID ReservedForNtRpc; VOID * DbgSsReserved[2]; ULONG HardErrorMode; VOID * Instrumentation[9]; GUID ActivityId; PVOID SubProcessTag; PVOID EtwLocalData; PVOID EtwTraceData; PVOID WinSockData; ULONG GdiBatchCount; UCHAR SpareBool0; UCHAR SpareBool1; UCHAR SpareBool2; UCHAR IdealProcessor; ULONG GuaranteedStackBytes; PVOID ReservedForPerf; PVOID ReservedForOle; ULONG WaitingOnLoaderLock; PVOID SavedPriorityState; ULONG SoftPatchPtr1; PVOID ThreadPoolData; VOID * * TlsExpansionSlots; ULONG ImpersonationLocale; ULONG IsImpersonating; PVOID NlsCache; PVOID pShimData; ULONG HeapVirtualAffinity; PVOID CurrentTransactionHandle; PTEB_ACTIVE_FRAME ActiveFrame; PVOID FlsData; PVOID PreferredLanguages; PVOID UserPrefLanguages; PVOID MergedPrefLanguages; ULONG MuiImpersonation; WORD CrossTebFlags; ULONG SpareCrossTebBits: 16; WORD SameTebFlags; ULONG DbgSafeThunkCall: 1; ULONG DbgInDebugPrint: 1; ULONG DbgHasFiberData: 1; ULONG DbgSkipThreadAttach: 1; ULONG DbgWerInShipAssertCode: 1; ULONG DbgRanProcessInit: 1; ULONG DbgClonedThread: 1; ULONG DbgSuppressDebugMsg: 1; ULONG SpareSameTebBits: 8; PVOID TxnScopeEnterCallback; PVOID TxnScopeExitCallback; PVOID TxnScopeContext; ULONG LockCount; ULONG ProcessRundown; UINT64 LastSwitchTime; UINT64 TotalSwitchOutTime; LARGE_INTEGER WaitReasonBitMap; } TEB, *PTEB;
|
获取 PEB 结构体的两种办法
借助 FS 或 GS 段寄存器所指的 TEB 结构体可以轻松获取 PEB 结构体地址,这是因为 TEB 结构体中的 ProcessEnvironmentBlock
成员是指向 PEB 结构体地址的。于是:
-
直接获取 PEB 地址
1
| MOV EAX, DWORD PTR FS: [0x30]
|
-
先获取 TEB,再通过 TEB.ProcessEnvironmentBlock
获取 PEB 地址
1 2
| MOV EAX, DWORD PTR FS: [0x18] MOV EAX, DWORD PTR DS: [EAX+0x30]
|
调试检测手段
-
BeingDebugged
当进程处于调试状态时,该成员的值会被设置成为 1;在非调试状态下运行时,设置为 0。IsDebuggerPresent
函数可以通过检测这个值来判断程序是否在调试
1 2 3
| mov eax, dword ptr fs:[18] ; 获取TEB地址 mov eax, dword ptr ds:[eax+30] ; 获取PEB地址 movz eax byte ptr ds:[eax+2] ; 获取PEB偏移为2的结构体元素,即BeingDebugged
|
-
ProcessHeap
利用进程堆 (HEAP) 进行反调试。PEB.ProcessHeap
成员是指向默认 HEAP 结构体的指针。
Note
1 2 3 4 5 6 7 8 9 10
| ... +0x018 ProcessHeap : 0x00090000 Void //进程默认堆 ... +0x078 HeapSegmentReserve : 0x100000 //堆的默认保留大小,字节数,1MB +0x07c HeapSegmentCommit : 0x2000 //堆的默认提交大小,8KB (两个内存页,x86 默认内存页 4KB) ... +0x088 NumberOfHeaps : 0x10 //堆的数量 +0x08c MaximumNumberOfHeaps : 0x10 //堆的最大数量 +0x090 ProcessHeaps : 0x7c99cfc0 -> 0x00090000 Void //堆句柄数组 ...
|
(和视频教程的有出入,需要验证)
GetProcessHeap
函数获取到 HEAP 结构体后,比较重要的
Linux 平台反调试
利用进程相关 ID
在 Linux 中,getppid
函数用于获取父进程 ID (parent process ID)。由于 Linux 中一个进程只能被它的父进程跟踪,因此如果某个进程的父进程不是常见的 shell(如 bash
)而是 gdb
等,说明该进程正在被跟踪。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| #include <stdio.h> #include <string.h> int main(int argc, char *argv[]) { char buf0[32], buf1[128]; FILE* p; snprintf(buf0, 24, "/proc/%d/cmdline", getppid()); p = fopen(buf0, "r"); fgets(buf1, 128, p); fclose(p); if(!strcmp(buf1, "gdb")) { printf("Debugger detected"); return 1; } printf("All good"); return 0; }
|
上述程序会检测到 gdb
的存在,从而终止程序并返回错误。
另外一种方法是利用 session ID(会话 ID)。无论被跟踪与否,session ID 不变,而 ppid 会变。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| #include <stdio.h> #include <stdlib.h> #include <unistd.h>
int main(void) { if(getsid(getpid()) != getppid()) { printf("traced!\n"); return(EXIT_FAILURE); } printf("not debugging\n"); return 0; }
|
对于这类反调试,修改 getppid()
或 getsid()
的返回值即可。
利用 ptrace
在 Linux 中,ptrace
函数用于让父进程跟踪子进程。有很多大家所常用的工具都基于 ptrace
来实现,如 strace
和 gdb
。
ptrace
只允许同一时间内进程最多被一个调试器进行调试。如果进程自身已经调用 ptrace
,那么其他通过 ptrace
发起的调试都会被拒绝并返回 -1。
1 2 3 4 5 6 7 8 9 10 11 12
| #include <stdio.h> #include <sys/ptrace.h> int main(int argc, char *argv[]) { if(ptrace(PTRACE_TRACEME, 0, 0, 0) == -1) { printf("Debugger detected"); return 1; } printf("All good"); return 0; }
|
对于这类反调试,要么绕过 ptrace()
函数,要么直接 nop 掉这个函数(nop 是比较好的方案)。
我们还可以构造多重 ptrace()
,并增加一些简单的验证:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| #include <stdio.h> #include <sys/ptrace.h>
int main() { int offset = 0;
if (ptrace(PTRACE_TRACEME, 0, 1, 0) == 0) { offset = 2; }
if (ptrace(PTRACE_TRACEME, 0, 1, 0) == -1) { offset = offset * 3; }
if (offset == 2 * 3) { printf("normal execution\n"); } else { printf("don't trace me !!\n"); }
return 0; }
|
现在 nop 都不能直接用了,因为我们需要保证验证时 offset
具有正确的值。