文章目录
- 前言
- 系统API实现方式
- IsDebuggerPresent (+0x2)
- NtGlobalFlag(+0x68)
- Heap flags(0x18)
- CheckRemoteDebuggerPresent
- NtQueryInformationProcess
- ZwSetInformationThread
- 示例
- 示例1
- 比较明文字符串和输入字符串
- NtGlobalFlag
- 时间差检测
- ProcessMonitor
- 检测进程名
- 检测 VMware
- SEH
- 获取flag
- 示例2
前言
思来想去还是先写反反调试,壳的话打算在PE文件内容里写
反调试顾名思义就是用尽一切手段防止运行时对程序的非法篡改和窥视,加大代码的复杂度和分析等
以下只是遇到的一些反调试,其中有一些知识都没有学习到,不过慢慢来也慢慢补坑
系统API实现方式
IsDebuggerPresent (+0x2)
微软给出的解释是此函数允许应用程序确定自己是否正在被调试,并依此改变行为。例如通过OutputDebugString函数提供更多调试信息
IsDebuggerPresent 这个 API 的实现方式是从 PEB 读取 BeingDebugged 字段来判断进程是否被调试状态
实现代码:
mov eax,dword ptr fs:[0x30]
movzx eax,byte ptr ds:[rax+0x2]
ret
fs:[0]是TEB结构的地址,其中fs:[0x30] 这个偏移是 PEB 指针,第一行的意思是将 PEB 指针赋值给 eax 寄存器,fs:[0x18]则是TEB指针
第二行就是从 PEB 结构的 0x2 偏移处,也就是 BeingDebugged 字段,取 1 字节,赋值到 eax
第三行就是返回了,没有参数和局部变量所以也没平栈,无论 __cdecl 调用还是 __stdcall 调用都是在 eax 寄存器保存返回值
想要 bypass 这种检查就非常容易,修改 PEB 结构中的 BeingDebugged 字段值为 0 就OK了。
NtGlobalFlag(+0x68)
在 32 位机器上, NtGlobalFlag字段位于PEB(进程环境块)0x68的偏移处, 64 位机器则是在偏移0xBC位置. 该字段的默认值为 0. 当调试器正在运行时, 该字段会被设置为一个特定的值. 尽管该值并不能十分可信地表明某个调试器真的有在运行, 但该字段常出于该目的而被使用.
该字段包含有一系列的标志位. 由调试器创建的进程会设置以下标志位:
FLG_HEAP_ENABLE_TAIL_CHECK (0x10)
FLG_HEAP_ENABLE_FREE_CHECK (0x20)
FLG_HEAP_VALIDATE_PARAMETERS (0x40)
调试程序时,PEB.NtGlobalFlag的值会被设置为0x70,所以,检测该成员的值即可判断进程是否处于被调试状态。
bypass 这个检查也很容易,因为标志位都在被调试进程的地址空间里,直接改掉就行了。
Heap flags(0x18)
这里引用ctfwiki给的解释:
Heap flags包含有两个与NtGlobalFlag一起初始化的标志: Flags和ForceFlags. 这两个字段的值不仅会受调试器的影响, 还会由 windows 版本而不同, 字段的位置也取决于 windows 的版本.
Flags 字段:
在 32 位 Windows NT, Windows 2000 和 Windows XP 中, Flags位于堆的0x0C偏移处. 在 32 位 Windows Vista 及更新的系统中, 它位于0x40偏移处.
在 64 位 Windows XP 中, Flags字段位于堆的0x14偏移处, 而在 64 位 Windows Vista 及更新的系统中, 它则是位于0x70偏移处.
ForceFlags 字段:
在 32 位 Windows NT, Windows 2000 和 Windows XP 中, ForceFlags位于堆的0x10偏移处. 在 32 位 Windows Vista 及更新的系统中, 它位于0x44偏移处.
在 64 位 Windows XP 中, ForceFlags字段位于堆的0x18偏移处, 而在 64 位 Windows Vista 及更新的系统中, 它则是位于0x74偏移处.。
在所有版本的 Windows 中, Flags字段的值正常情况都设为HEAP_GROWABLE(2), 而ForceFlags字段正常情况都设为0.
然而对于一个 32 位进程 (64 位程序不会有此困扰), 这两个默认值, 都取决于它的宿主进程(host process) 的 subsystem版本 (这里不是指所说的比如 win10 的 linux 子系统).
只有当subsystem在3.51及更高的版本, 字段的默认值才如前所述. 如果是在3.10-3.50版本之间, 则两个字段的HEAP_CREATE_ALIGN_16 (0x10000)都会被设置. 如果版本低于3.10, 那么这个程序文件就根本不会被运行.
如果某操作将Flags和ForgeFlags字段的值分别设为2和0, 但是却未对subsystem版本进行检查, 那么就可以表明该动作是为了隐藏调试器而进行的.
CheckRemoteDebuggerPresent
kernel32的CheckRemoteDebuggerPresent()函数用于检测指定进程是否正在被调试. Remote在单词里是指同一个机器中的不同进程.
两个参数:一个是进程的 HANDLE,一个是 PBOOL。
BOOL WINAPI CheckRemoteDebuggerPresent(
_In_ HANDLE hProcess,
_Inout_ PBOOL pbDebuggerPresent
);
如果调试器存在 (通常是检测自己是否正在被调试), 该函数会将pbDebuggerPresent指向的值设为0xffffffff.
NtQueryInformationProcess
NtQueryInformationProcess 是一个查询信息的接口,输入参数包括查询的信息类型、进程HANDLE、结果指针等。
kernel32的CheckRemoteDebuggerPresent()函数内部通过调用NtQueryInformationProcess()来检测调试, 而NtQueryInformationProcess内部则是查询EPROCESS结构体的DebugPort字段, 当进程正在被调试时, 返回值为0xffffffff.
内核接口,还不会hook先放置一下。
ZwSetInformationThread
ZwSetInformationThread 等同于 NtSetInformationThread,通过为线程设置 ThreadHideFromDebugger,可以禁止线程产生调试事件,代码如下:
#include <Windows.h>
#include <stdio.h>
typedef DWORD(WINAPI* ZW_SET_INFORMATION_THREAD) (HANDLE, DWORD, PVOID, ULONG);
#define ThreadHideFromDebugger 0x11
VOID DisableDebugEvent(VOID)
{
HINSTANCE hModule;
ZW_SET_INFORMATION_THREAD ZwSetInformationThread;
hModule = GetModuleHandleA("Ntdll");
ZwSetInformationThread = (ZW_SET_INFORMATION_THREAD)GetProcAddress(hModule, "ZwSetInformationThread");
ZwSetInformationThread(GetCurrentThread(), ThreadHideFromDebugger, 0, 0);
}
int main()
{
printf("Begin\n");
DisableDebugEvent();
printf("End\n");
return 0;
}
关键代码为ZwSetInformationThread(GetCurrentThread(), ThreadHideFromDebugger, 0, 0),如果处于调试状态,执行完该行代码,程序就会退出。
想要bypass也比较简单,可以看到ZwSetInformationThread()函数的第二个参数为ThreadHideFromDebugger,其值为0x11。当执行到该函数的时候,若发现第 2 个参数值为 0x11,跳过或者将 0x11 修改为其他值即可。
示例
示例1
还是以ctfwiki给的示例来做,直接运行,要求输入密码,输入出错则提示
无壳32位,丢ida看看,查找字符串
显然,字符串表明程序中可能有各种检测,比如检测进程名ollydbg.exe, ImmunityDebugger.exe, idaq.exe和Wireshark.exe,然后也有其他的检测,可以看到字符串password is wrong和You password is correct的字样,同时还有疑是待解密的flag字符串
先从提示处开始入手,也就是主函数,果然存在大量的反调试
int __cdecl main(int argc, const char **argv, const char **envp)
{
FILE *v3; // eax
HANDLE v4; // eax
int v11; // [esp+C4h] [ebp-A8h]
DWORD v12; // [esp+D4h] [ebp-98h]
LPCSTR lpFileName; // [esp+D8h] [ebp-94h]
BOOL pbDebuggerPresent; // [esp+DCh] [ebp-90h]
int v15; // [esp+E0h] [ebp-8Ch]
int v16; // [esp+E4h] [ebp-88h]
int i; // [esp+E8h] [ebp-84h]
int v18; // [esp+ECh] [ebp-80h]
int v19; // [esp+F0h] [ebp-7Ch]
char v20[4]; // [esp+F8h] [ebp-74h]
int v21; // [esp+108h] [ebp-64h]
char v22; // [esp+10Ch] [ebp-60h]
char v23; // [esp+10Dh] [ebp-5Fh]
CPPEH_RECORD ms_exc; // [esp+154h] [ebp-18h]
v22 = 0;
memset(&v23, 0, 0x3Fu);
v21 = 1;
printf("Input password >");
v3 = (FILE *)sub_40223D();
fgets(&v22, 64, v3);
strcpy(v20, "I have a pen.");
v21 = strncmp(&v22, v20, 0xDu); // 直接比较明文和输入字符串
if ( !v21 )
{
puts("Your password is correct.");
if ( IsDebuggerPresent() == 1 ) // API: IsDebuggerPresent() 静态反调
{
puts("But detected debugger!");
exit(1);
}
if ( sub_401120() == 0x70 ) // API:NtGlobalFlag 静态反调
{
puts("But detected NtGlobalFlag!");
exit(1);
}
v4 = GetCurrentProcess();
CheckRemoteDebuggerPresent(v4, &pbDebuggerPresent);// API:CheckRemoteDebuggerPresent() 静态反调
if ( pbDebuggerPresent )
{
printf("But detected remotedebug.\n");
exit(1);
}
v12 = GetTickCount(); // 时间差检测 动态反调
for ( i = 0; i == 100; ++i )
Sleep(1u);
v15 = 1000;
if ( GetTickCount() - v12 > 0x3E8 )
{
printf("But detected debug.\n");
exit(1);
}
lpFileName = "\\\\.\\Global\\ProcmonDebugLogger";// 通过检测设备文件来检测ProcessMonitor
if ( CreateFileA("\\\\.\\Global\\ProcmonDebugLogger", 0x80000000, 7u, 0, 3u, 0x80u, 0) != (HANDLE)-1 )
{
printf("But detect %s.\n", &lpFileName);
exit(1);
}
v11 = sub_401130(); // API: CreateToolhelp32Snapshot()检测进程
if ( v11 == 1 )
{
printf("But detected Ollydbg.\n");
exit(1);
}
if ( v11 == 2 )
{
printf("But detected ImmunityDebugger.\n");
exit(1);
}
if ( v11 == 3 )
{
printf("But detected IDA.\n");
exit(1);
}
if ( v11 == 4 )
{
printf("But detected WireShark.\n");
exit(1);
}
if ( sub_401240() == 1 ) // 检测 VMware
{
printf("But detected VMware.\n");
exit(1);
}
v16 = 1; // 异常 动态反调
v19 = 1;
v18 = 1 / 0;
ms_exc.registration.TryLevel = -2;
printf("But detected Debugged.\n");
exit(1);
}
printf("password is wrong.\n");
return 0;
}
进行一些反调的简单分析:
比较明文字符串和输入字符串
v22 = 0;
memset(&v23, 0, 0x3Fu);
v21 = 1;
printf("Input password >");
v3 = (FILE *)sub_40223D();
fgets(&v22, 64, v3);
strcpy(v20, "I have a pen.");
v21 = strncmp(&v22, v20, 0xDu);
先输出Input password >,然后用fgets()获取用户输入的字符串, 将I have a pen,复制到v20的缓冲区中,然后用strncmp比对用户输入与I have a pen.的内容, 并将比较结果返回给v21. 以下会根据v21, 也就是根据输入的password是否正确而进行跳转
NtGlobalFlag
if ( sub_401120() == 0x70 ) // API:NtGlobalFlag
{
puts("But detected NtGlobalFlag!");
exit(1);
}
//sub_401120
int sub_401120()
{
return *(_DWORD *)(__readfsdword(0x30u) + 104) & 0x70;
}
0x68是 PEB 的NtGlobalFlag字段对应偏移值
0x70是FLG_HEAP_ENABLE_TAIL_CHECK (0x10),FLG_HEAP_ENABLE_FREE_CHECK (0x20) 和FLG_HEAP_VALIDATE_PARAMETERS (0x40)这三个标志
由于是静态反调,所以在xdbg中无效
时间差检测
v13 = GetTickCount();
for ( i = 0; i == 100; ++i ) // 睡眠
Sleep(1u);
v16 = 1000;
if ( GetTickCount() - v13 > 1000 ) // 检测时间差
{
printf("But detected debug.\n");
exit(1);
}
GetTickCount会返回启动到现在的毫秒数,循环里光是sleep(1)就进行了 100 次,也就是 100 毫秒。 两次得到的时间作差如果大于 1000 毫秒,时差明显大于所耗的时间, 也就间接检测到了调试。
反动调经常使用的时间检测法,其余的时间检测法还有 时钟检测,其他的时间API函数,如QueryPerformanceCounter、GetTickCount、GetSystemTime、GetLocalTime等
bypass也很简单,只需修改标志位让其跳转即可
ProcessMonitor
lpFileName = "\\\\.\\Global\\ProcmonDebugLogger";
if ( CreateFileA("\\\\.\\Global\\ProcmonDebugLogger", 0x80000000, 7u, 0, 3u, 0x80u, 0) != (HANDLE)-1 )
{
printf("But detect %s.\n", &lpFileName);
exit(1);
}
这里通过检测设备文件\\.\Global\ProcmonDebugLogger来检测ProcessMonitor
检测进程名
v11 = sub_401130();
if ( v11 == 1 )
{
printf("But detected Ollydbg.\n");
exit(1);
}
if ( v11 == 2 )
{
printf("But detected ImmunityDebugger.\n");
exit(1);
}
if ( v11 == 3 )
{
printf("But detected IDA.\n");
exit(1);
}
if ( v11 == 4 )
{
printf("But detected WireShark.\n");
exit(1);
}
//sub_401130()
signed int sub_401130()
{
PROCESSENTRY32 pe; // [esp+0h] [ebp-138h]
HANDLE hSnapshot; // [esp+130h] [ebp-8h]
BOOL i; // [esp+134h] [ebp-4h]
pe.dwSize = 296;
memset(&pe.cntUsage, 0, 0x124u);
hSnapshot = CreateToolhelp32Snapshot(2u, 0);
for ( i = Process32First(hSnapshot, &pe); i == 1; i = Process32Next(hSnapshot, &pe) )
{
if ( !_stricmp(pe.szExeFile, "ollydbg.exe") )
return 1;
if ( !_stricmp(pe.szExeFile, "ImmunityDebugger.exe") )
return 2;
if ( !_stricmp(pe.szExeFile, "idaq.exe") )
return 3;
if ( !_stricmp(pe.szExeFile, "Wireshark.exe") )
return 4;
}
return 0;
}
通过执行sub_401130()函数来检测进程,并根据检测到的不同进程,返回相应的值。
sub_401120()中使用了 API: CreateToolhelp32Snapshot来获取当前的进程信息, 并在 for 循环里依次比对。如果找到指定的进程名, 就直接返回相应的值,然后根据返回值跳转到不同的分支里。
检测 VMware
if ( sub_401240() == 1 ) // 8. 通过vmware的I/O端口进行检测
{
printf("But detected VMware.\n");
exit(1);
}
//sub_401240()
signed int sub_401240()
{
unsigned __int32 v0; // eax
v0 = __indword(0x5658u);
return 1;
}
这是 VMware 的一个 "后门"I/O 端口, 0x5658 = “VX”.
如果程序在 VMware 内运行, 程序使用In指令通过0x5658端口读取数据时, EBX寄存器的值就会变为0x564D5868(0x564D5868 == “VMXh”)
SEH
v17 = 1;
v20 = 1;
v12 = 0;
v19 = 1 / 0; // SEH
ms_exc.registration.TryLevel = -2;
printf("But detected Debugged.\n");
exit(1);
接下来这一段,这里v19 = 1 / 0明显是不合常理的,会产生一个除零异常. 而后面的ms_exc.registration.TryLevel = -2,解除异常,TryLevel=TRYLEVEL_NONE (-2) . 来看汇编代码
这里的idiv [ebp+var_9C]触发异常后就由程序注册的异常处理函数接管,如果没有在异常处理程序入口设下断点的话, 程序就容易跑飞,将异常交给SEH处理下好断点即可。
获取flag
前面分析到有一串类似待解密的flag字符串,怎么都没有看到相关的代码流,实际上由于 IDA 反编译的限制,使得反编译出的伪 C 代码并不正确,来看最后一个Debugged提示的汇编代码,有些代码流没有被实际反编译出来
.text:00401627 loc_401627:
.text:00401627 call sub_4012E0
.text:0040162C movzx eax, ax
.text:0040162F mov [ebp+var_A8], eax
.text:00401635 cmp [ebp+var_A8], 0
.text:0040163C jz short loc_401652 //该函数也没有被反编译出来
.text:0040163E push offset aButDetectedDeb_2 ,"But detected Debugged.\n"
.text:00401643 call _printf
.text:00401648 add esp, 4
.text:0040164B push 1 ; int
.text:0040164D call _exit
以上代码并没有被实际反编译出来,而loc_401652()也是同样没有被反编译出来的代码
.text:00401652 loc_401652:
.text:00401652 mov [ebp+var_78], 0
.text:00401659 cmp [ebp+var_78], 1
.text:0040165D jnz loc_40174D
.text:00401663 mov ecx, 7
.text:00401668 mov esi, offset aAjJq7hbotHU8ac ; ";aj&@:JQ7HBOt[h?U8aCBk]OaI38"
.text:0040166D lea edi, [ebp+var_CC]
.text:00401673 rep movsd
.text:00401675 movsb
.text:00401676 xor ecx, ecx
.text:00401678 mov [ebp+var_AF], ecx
.text:0040167E lea edx, [ebp+var_CC]
.text:00401684 mov [ebp+var_D8], edx
.text:0040168A mov [ebp+Text], 0
.text:00401691 push 7Fh ; size_t
.text:00401693 push 0 ; int
.text:00401695 lea eax, [ebp+var_157]
.text:0040169B push eax ; void *
.text:0040169C call _memset
.text:004016A1 add esp, 0Ch
.text:004016A4 lea ecx, [ebp+Text]
.text:004016AA mov [ebp+var_D4], ecx
.text:004016B0 mov edx, [ebp+var_D8]
.text:004016B6 push edx ; char *
.text:004016B7 call _strlen
.text:004016BC add esp, 4
.text:004016BF mov [ebp+var_D0], eax
.text:004016C5 mov [ebp+var_15C], 0
.text:004016CF jmp short loc_4016FE
loc_4016FE():
.text:004016FE loc_4016FE:
.text:004016FE mov eax, [ebp+var_15C]
.text:00401704 cmp eax, [ebp+var_D0]
.text:0040170A jnb short loc_401737
.text:0040170C mov ecx, [ebp+var_D0]
.text:00401712 sub ecx, [ebp+var_15C]
.text:00401718 push ecx
.text:00401719 mov edx, [ebp+var_D4]
.text:0040171F push edx
.text:00401720 mov eax, [ebp+var_D8]
.text:00401726 push eax
.text:00401727 call sub_401000
.text:0040172C add esp, 0Ch
.text:0040172F test eax, eax
.text:00401731 jz short loc_401735
.text:00401733 jmp short loc_401737
loc_401737():
.text:00401737 loc_401737:
.text:00401737 ;
.text:00401737 push 0 ; uType
.text:00401739 push offset Caption ; "check!"
.text:0040173E lea ecx, [ebp+Text]
.text:00401744 push ecx ; lpText
.text:00401745 push 0 ; hWnd
.text:00401747 call ds:MessageBoxA
可以看到后续的代码中调用了MessageboxA()函数,其中第三个参数是解密后的flag
再向上查找的话发现在进入flag的解密前会有一个永假条件跳转,在调试的时候雄修改标志位跳过即可,之后让程序解密flag,调用窗体
弹出窗体,得到flag
示例2
改天再补吧