花指令&反调试

2024/01/17

好,做道upx练手,把sublime搞崩了。后来才知道是文件没有保存用不了。
现在还有个问题,就是文件git不上去。。。听天由命吧

2024/01/18

git上去了!

方法:更改了C:\Windows\System32\drivers\etc的hosts,加了IP表,好用!(呜呜呜)

2024/01/19

听伯尼学长说,修改系统配置的http_proxy和https_proxy为梯子的ip,这样就可以在ping的时候走梯子了

或者直接在cmd里set http_proxy=http://127.0.0.1:7890 & set https_proxy=http://127.0.0.1:7890

欸嘿,昨天玩儿了一天,今天继续学

花指令

解析失败,一般可能是花指令

会被执行

干扰静态分析*

改变堆栈操作

1
2
3
4
5
6
7
8
9
#include <stdio.h>
int main()
{
_asm {
push eax;
add esp, 4;
}
printf("Hello World!\n");
}

利用call指令或jmp指令增加执行流程复杂度

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
int main()
{
_asm {
call xxx
xxx :
add[esp], 0x7
retn
_emit 0x12
_emit 0x34
}
printf("wwwwww");
}

因为有0x12,0x34,push没识别出来,printf后没法执行。

call–>结束

找到栈变化的地方(esp转向)按C重编译

不会被执行

插入机器码

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
int main()
{
_asm {
xor eax, eax;
jz xxx;
_emit 0x11;
_emit 0x22;
_emit 0x33; // 0x33是 xor 指令的操作码,会导致后面正常的 push 指令被错误解析
xxx:
}
printf("Hello World!\n");
}

改变堆栈平衡

1
2
3
4
5
6
7
8
9
10
#include<stdio.h>
int main(){
_asm{
xor eax,eax;
jz s;
add esp,0x11;
s:
}
printf("hello");
}

IDA反编译报错,好像可以F5重编译。。。或者发现花指令地方nop!

xor eax,eax后有jz命令后有一个+1

反调试

检测调试的陷阱w~

函数检测:

函数检测就是通过 Windows 自带的公开或未公开的函数直接检测程序是否处于调试状态。

IsDebuggerPresent

最简单的调试器检测函数是 IsDebuggerPresent() :

1
BOOL WINAPI IsDebuggerPresent(void);

该函数查询进程环境块(PEB)中的 BeingDebugged 标志,如果进程处在调试上下文中,则返回一个非零值,否则返回零。

1
2
3
4
BOOL CheckDebug()
{
return IsDebuggerPresent();
}

CheckRemoteDebuggerPresent

CheckRemoteDebuggerPresent() 用于检测一个远程进程是否处于调试状态:

1
2
3
4
BOOL WINAPI CheckRemoteDebuggerPresent(
_In_ HANDLE hProcess,
_Inout_ PBOOL pbDebuggerPresent
);

如果 hProcess 句柄表示的进程处于调试上下文,则设置 pbDebuggerPresent 变量被设置为TRUE ,否则被设置为 FALSE 。

1
2
3
4
5
6
BOOL CheckDebug()
{
BOOL ret;
CheckRemoteDebuggerPresent(GetCurrentProcess(), &ret);
return ret;
}

NtQueryInformationProcess

NtQueryInformationProcess 用于获取给定进程的信息:

1
2
3
4
5
6
7
NTSTATUS WINAPI NtQueryInformationProcess(
_In_ HANDLE ProcessHandle,
_In_ PROCESSINFOCLASS ProcessInformationClass,
_Out_ PVOID ProcessInformation,
_In_ ULONG ProcessInformationLength,
_Out_opt_ PULONG ReturnLength
);

第二个参数 ProcessInformationClass 给定了需要查询的进程信息类型。当给定值为0 ( ProcessBasicInformation )或 7 (ProcessDebugPort )时,就能得到相关调试信息,返回信息会写到第三个参数 ProcessInformation 指向的缓冲区中。

示例:

1
2
3
4
5
6
7
8
BOOL CheckDebug()
{
DWORD dbgport = 0;
HMODULE hModule = LoadLibrary("Ntdll.dll");
NtQueryInformationProcessPtr NtQueryInformationProcess =(NtQueryInformationProcessPtr)GetProcAddress(hModule,"NtQueryInformationProcess");
NtQueryInformationProcess(GetCurrentProcess(), 7, &dbgPort,sizeof(dbgPort),NULL);
return dbgPort != 0;
}

GetLastError

编写应用程序时,经常需要涉及到错误处理问题。许多函数调用只用 TRUE 和 FALSE 来表明函数的运行结果。一旦出现错误,MSDN 中往往会指出请用 GetLastError 函数来获得错误原因。

恶意代码可以使用异常来破坏或者探测调试器。调试器捕获异常后,并不会立即将处理权返回被调试进程处理,大多数利用异常的反调试技术往往据此来检测调试器。

多数调试器默认的设置是捕获异常后不将异常传递给应用程序。如果调试器不能将异常结果正确返回到被调试进程,那么这种异常失效可以被进程内部的异常处理机制探测。

对于 OutputDebugString 函数,它的作用是在调试器中显示一个字符串,同时它也可以用来探测调试器的存在。使用 SetLastError 函数,将当前的错误码设置为一个任意值。

如果进程没有被调试器附加,调用 OutputDebugString 函数会失败,错误码会重新设置,因此GetLastError 获取的错误码应该不是我们设置的任意值。

但如果进程被调试器附加,调用 OutputDebugString 函数会成功,这时 GetLastError 获取的错误码应该没改变。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
BOOL CheckDebug()
{
DWORD errorValue = 12345;
SetLastError(errorValue);
OutputDebugStringA("Test for debugger!");
if (GetLastError() == errorValue)
{
return TRUE;
}
else
{
return FALSE;
}
}

对于 DeleteFiber 函数,如果给它传递一个无效的参数的话会抛出ERROR_INVALID_PARAMETER 异常。如果进程正在被调试的话,异常会被调试器捕获。所以,同样可以通过验证 LastError 值来检测调试器的存在。如代码所示,0x57 就是指 ERROR_INVALID_PARAMETER 。

1
2
3
4
5
6
BOOL CheckDebug()
{
char fib[1024] = {0};
DeleteFiber(fib);
return (GetLastError() != 0x57);
}

同样还可以使用 CloseHandle 、 CloseWindow 产生异常,使得错误码改变。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
BOOL CheckDebug()
{
DWORD ret = CloseHandle((HANDLE)0x1234);
if (ret != 0 || GetLastError() != ERROR_INVALID_HANDLE)
{
return TRUE;
}
else
{
return FALSE;
}
}
BOOL CheckDebug()
{
DWORD ret = CloseWindow((HWND)0x1234);
if (ret != 0 || GetLastError() != ERROR_INVALID_WINDOW_HANDLE)
{
return TRUE;
}
else
{
return FALSE;
}
}

数据检测

BeingDebugged

数据检测是指程序通过测试一些与调试相关的关键位置的数据来判断是否处于调试状态。比如 PEB 中的 BeingDebugged 参数。数据检测就是直接定位到这些数据地址并测试其中的数据,从而避免调用函数,使程序的行为更加隐蔽。

1
2
3
4
5
6
7
8
9
10
11
BOOL CheckDebug()
{
int BeingDebug = 0;
__asm
{
mov eax, dword ptr fs:[30h] ; 指向 PEB 基地址
movzx eax, byte ptr [eax+2]
mov BeingDebug, eax
}
return BeingDebug != 0;
}

fs:[30h]

NTGlobalFlag

由于调试器中启动的进程与正常启动的进程创建堆的方式有些不同,系统使用 PEB 结构偏移量 0x68 处的一个未公开的位置 NTGlobalFlag ,来决定如何创建堆结构。如果这个位置上的值为 0x70 ,则进程处于调试器中。

1
2
3
4
5
6
7
8
9
10
11
12
BOOL CheckDebug()
{
int BeingDbg = 0;
__asm
{
mov eax, dword ptr fs:[30h]
mov eax, dword ptr [eax + 68h]
and eax, 0x70
mov BeingDbg, eax
}
return BeingDbg != 0;
}

进程检测

进程检测通过检测当前桌面中是否存在特定的调试进程来判断是否存在调试器,但不能判断该调试器是否正在调试该程序。

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
BOOL CheckDebug()
{
if (FindWindowA("x32dbg", 0))
{
return 0;
}
return 1;
}
BOOL CheckDebug()
{
DWORD ID;
DWORD ret = 0;
PROCESSENTRY32 pe32;
pe32.dwSize = sizeof(pe32);
HANDLE hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hProcessSnap == INVALID_HANDLE_VALUE)
{
return FALSE;
}
BOOL bMore = Process32First(hProcessSnap, &pe32);
while (bMore)
{
if (stricmp(pe32.szExeFile, "OllyDBG.EXE") == 0 || stricmp(pe32.szExeFile, "OllyICE.exe") == 0 || stricmp(pe32.szExeFile, "x64_dbg.exe") == 0 || stricmp(pe32.szExeFile, "windbg.exe") == 0 || stricmp(pe32.szExeFile, "ImmunityDebugger.exe") == 0)
{
return TRUE;
}
bMore = Process32Next(hProcessSnap, &pe32);
}
CloseHandle(hProcessSnap);
return FALSE;
}

特征码检测

特征码检测枚举当前正在运行的进程,并在进程的内存空间中搜索特定调试器的代码片段。

例如 OllyDbg 有这样一段特征码:

1
2
3
4
0x41, 0x00, 0x62, 0x00, 0x6f, 0x00, 0x75, 0x00, 0x74, 0x00,
0x20, 0x00, 0x4f, 0x00, 0x6c, 0x00, 0x6c, 0x00, 0x79, 0x00,
0x44, 0x00, 0x62, 0x00, 0x67, 0x00, 0x00, 0x00, 0x4f, 0x00,
0x4b, 0x00, 0x00, 0x00
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
BOOL CheckDebug()
{BYTE sign[] = { 0x41, 0x00, 0x62, 0x00, 0x6f, 0x00, 0x75, 0x00, 0x74, 0x00,
0x20, 0x00, 0x4f, 0x00, 0x6c, 0x00, 0x6c, 0x00, 0x79, 0x00,
0x44, 0x00, 0x62, 0x00, 0x67, 0x00, 0x00, 0x00, 0x4f, 0x00,
0x4b, 0x00, 0x00, 0x00; }
PROCESSENTRY32 sentry32 = { 0 };
sentry32.dwSize = sizeof(sentry32);
HANDLE phsnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
Process32First(phsnap, &sentry32);
do {
HANDLE hps = OpenProcess(MAXIMUM_ALLOWED, FALSE,
sentry32.th32ProcessID);
if (hps != 0)
{
DWORD szReaded = 0;
BYTE signRemote[sizeof(sign)];
ReadProcessMemory(hps, (LPCVOID)0x4f632a, signRemote,
sizeof(signRemote), &szReaded);
if (szReaded > 0)
{
if (memcmp(sign, signRemote, sizeof(sign)) == 0)
{
CloseHandle(phsnap);
return 0;
}
}
}
}
sentry32.dwSize = sizeof(sentry32);
}while (Process32Next(phsnap, &sentry32));

时间检测

时间检测是指在程序中通过代码感知程序处于调试时与未处于调试时的各种运行时间差异来判断程序是否处于调试状态。

例如我们在调试时步过两条指令所花费的时间远远超过 CPU 正常执行花费的时间,于是就可以通过rdtsc 指令或 GetTickCount 函数来进行测试。

注: rdtsc 指令用于将时间标签计数器读入 EDX:EAX 寄存器。 GetTickCount 返回从操作系统动

所经过的毫秒数。

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
BOOL CheckDebug()
{
int BeingDbg = 0;
__asm
{
rdtsc
mov ecx, edx
rdtsc
sub edx, ecx
mov BeingDbg, edx
}
if (BeingDbg > 2)
{
return 0;
}
return 1;
}
BOOL CheckDebug()
{
DWORD time1 = GetTickCount();
__asm
{
mov ecx, 10
mov edx, 6
mov ecx, 10
}
DWORD time2 = GetTickCount();
if (time2 - time1 > 0x1A)
{
return TRUE;
}
else
{
return FALSE;
}
}

断点检测

断点检测是根据调试器设置断点的原理来检测软件代码中是否设置了断点。调试器一般使用两者方法设置代码断点:

  • 通过修改代码指令为 INT3(机器码为0xCC)触发软件异常

  • 通过硬件调试寄存器设置硬件断点

针对软件断点,检测系统会扫描比较重要的代码区域,看是否存在多余的 INT3 指令。

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
BOOL CheckDebug()
{
PIMAGE_DOS_HEADER pDosHeader;
PIMAGE_NT_HEADERS32 pNtHeaders;
PIMAGE_SECTION_HEADER pSectionHeader;
DWORD dwBaseImage = (DWORD)GetModuleHandle(NULL);
pDosHeader = (PIMAGE_DOS_HEADER)dwBaseImage;
pNtHeaders = (PIMAGE_NT_HEADERS32)((DWORD)pDosHeader + pDosHeader -
> e_lfanew);
pSectionHeader = (PIMAGE_SECTION_HEADER)((DWORD)pNtHeaders +
sizeof(pNtHeaders->Signature) + sizeof(IMAGE_FILE_HEADER) +
(WORD)pNtHeaders->FileHeader.SizeOfOptionalHeader);
DWORD dwAddr = pSectionHeader->VirtualAddress + dwBaseImage;
DWORD dwCodeSize = pSectionHeader->SizeOfRawData;
BOOL Found = FALSE;
__asm
{
cld
mov edi, dwAddr
mov ecx, dwCodeSize
mov al, 0CCH
repne scasb; 在EDI指向大小为ECX的缓冲区中搜索AL包含的字节
jnz NotFound
mov Found, 1
NotFound:
}
return Found;
}

而对于硬件断点,由于程序工作在保护模式下,无法访问硬件调试断点,所以一般需要构建异常程序来获取 DR 寄存器的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
BOOL CheckDebug()
{
CONTEXT context;
HANDLE hThread = GetCurrentThread();
context.ContextFlags = CONTEXT_DEBUG_REGISTERS;
GetThreadContext(hThread, &context);
if (context.Dr0 != 0 || context.Dr1 != 0 || context.Dr2 != 0 ||
context.Dr3 != 0)
{
return 1;
}
return 0;
}

其他检测

判断父进程是否为 explorer.exe

一般双击运行的进程的父进程都是 explorer.exe,但是如果进程被调试父进程则是调试器进程。也就是说如果父进程不是 explorer.exe 则可以认为程序正在被调试。

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
BOOL CheckDebug()
{
LONG status;
DWORD dwParentPID = 0;
HANDLE hProcess;
PROCESS_BASIC_INFORMATION pbi;
int pid = getpid();
hProcess = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, pid);
if (!hProcess)
return -1;
PNTQUERYINFORMATIONPROCESS NtQueryInformationProcess =
(PNTQUERYINFORMATIONPROCESS)GetProcAddress(GetModuleHandleA("ntdll"), "NtQueryInf
ormationProcess");
status = NtQueryInformationProcess(hProcess, SystemBasicInformation,
(PVOID)&pbi, sizeof(PROCESS_BASIC_INFORMATION), NULL);
PROCESSENTRY32 pe32;
pe32.dwSize = sizeof(pe32);
HANDLE hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hProcessSnap == INVALID_HANDLE_VALUE)
{
return FALSE;
}
BOOL bMore = Process32First(hProcessSnap, &pe32);
while (bMore)
{
if (pbi.InheritedFromUniqueProcessId == pe32.th32ProcessID)
{
if (stricmp(pe32.szExeFile, "explorer.exe") == 0)
{
CloseHandle(hProcessSnap);
return FALSE;
}
else
{
CloseHandle(hProcessSnap);
return TRUE;
}
}
bMore = Process32Next(hProcessSnap, &pe32);
}
CloseHandle(hProcessSnap);
}

测试 STARTUPINFO

在使用 CreateProcess 创建进程时,需要传递 STARTUPINFO 的结构体指针,而常常我们并不会一个一个设置其结构的值,连把其他不用的值清 0 都会忽略。

故可以使用 GetStartupInfo 检查启动信息,如果很多值不为 0,那么就说明自己的父进程不是explorer(explorer.exe 使用 shell32 中 ShellExecute 来运行程序, ShellExecute 会清掉不用的值)

所以可以利用 STARTUPINFO 结构体中不用的字段来判断程序是否在被调试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
BOOL CheckDebug()
{
STARTUPINFO si;
GetStartupInfo(&si);
if (si.dwX != 0 || si.dwY != 0 || si.dwFillAttribute != 0 || si.dwXSize != 0 ||
si.dwYSize != 0 || si.dwXCountChars != 0 || si.dwYCountChars != 0)
{
return TRUE;
}
else
{
return FALSE;
}
}

基于异常的反调试

进程中发生异常时若 SEH 未处理或注册的 SEH 不存在,会调用 UnhandledExceptionFilter ,它会运行系统最后的异常处理器。 UnhandledExceptionFilter 内部调用了前面提到过的NtQueryInformationProcess 以判断进程是否正在被调试。

若进程未被调试,则运行最后的异常处理器。若进程处于调试状态,则将异常派送给调试器。

SetUnhandledExceptionFilter 函数可以修改系统最后的异常处理器。

Debug Block

Debug Block 是指在需要保护的程序中,程序自身将一些只能同时有 1 个实例的功能占为己用。比如一般情况下,一个进程只能同时被 1 个调试器调试,那么就可以设计一种模式,将程序以调试方式启动,然后利用系统的调试机制防止被其他调试器调试。