0x01 Windows PEB(Process Environment Block)
Windows PEB,中文的含义是进程环境块,意味着其中包含了很多与进程相关的复杂信息。微软官方给出了Windows PEB的结构体标准,每一个字段都代表了特定的意义。
typedef struct _PEB {
BYTE Reserved1[2];
BYTE BeingDebugged;
BYTE Reserved2[1];
PVOID Reserved3[2];
PPEB_LDR_DATA Ldr;
PRTL_USER_PROCESS_PARAMETERS ProcessParameters;
PVOID Reserved4[3];
PVOID AtlThunkSListPtr;
PVOID Reserved5;
ULONG Reserved6;
PVOID Reserved7;
ULONG Reserved8;
ULONG AtlThunkSListPtr32;
PVOID Reserved9[45];
BYTE Reserved10[96];
PPS_POST_PROCESS_INIT_ROUTINE PostProcessInitRoutine;
BYTE Reserved11[128];
PVOID Reserved12[1];
ULONG SessionId;
} PEB, *PPEB;
在Windbg中可以直观的看到PEB字段的详细信息:
Shellcode
shellcode是一段用于利用软件漏洞而执行的代码,shellcode执行后经常让攻击者获得shell而得名。一个漏洞从发现到利用,是艰巨的任务,因此往往编写shellcode是对漏洞挖掘黑客的一个风水岭。大部分shellcode编写知识都与Windows PEB结构紧密相连,Windows PEB结构中保存了当前进程的复杂信息,这些复杂信息中包含了PE文件的内存地址,通过解析PE文件,可以直接寻找到kernel32.dll
模块,利用这个模块中的Loadlibrary
API可以导入Windows所有的模块,最终能够在目标进程实现所有代码的执行。在实际应用领域的shellcode并不会太长,不同的漏洞利用对shellcode长度限制不同,所以越短越能直接达到目的的shellcode才是好code。
那么,如何找到PEB呢? - Thread Environment Block (TEB structure)
Windows操作系统中,CPU的FS寄存器指向当前活动线程的TEB结构(线程结构),TEB结构中包含了PEB结构:
typedef struct _TEB {
PVOID Reserved1[12];
PPEB ProcessEnvironmentBlock; // PEB 结构
PVOID Reserved2[399];
BYTE Reserved3[1952];
PVOID TlsSlots[64];
BYTE Reserved4[8];
PVOID Reserved5[26];
PVOID ReservedForOle;
PVOID Reserved6[4];
PVOID TlsExpansionSlots;
} TEB, *PTEB;
从这一点,我们就更能深刻理解,线程才是一个程序的执行单元,进程只是一个“容器”。
0x02 使用汇编语言访问PEB
我的理解:汇编语言分为纯汇编、伪汇编、宏汇编
- 纯汇编是机器代码,是编译器最终生成的代码
- 伪汇编是与一定开发环境绑定在一起使用的汇编代码,例如C语言中的
__asm { }
- 宏汇编是一定程度上将汇编语言进行了抽象、过程封装,比如MASM调用Windows只需要INVOKE即可,不需要关注保存现场、堆栈情况。
这里我使用C语言内置的汇编块进行获取PEB中BeingDebugged
的值,它只有1个字节。
#include <Windows.h>
#include <stdio.h>
int main()
{
DWORD dwIsDebug = 0;
__asm {
pushad
mov eax, fs: [30h]
movzx eax, byte ptr[eax + 2]
mov dword ptr [dwIsDebug], eax
popad
}
if (dwIsDebug) {
printf("Debug.....");
}
printf("Debug : %d \n", dwIsDebug);
return 0;
}
这个程序在没有调试的情况下启动,会输出Debug : 0
,如果有调试器进行调试,会输出Debug.....Debug : 1
。
如果想要判断当前程序是否被调试,可以获取PEB中的BeingDebugged
的值,看看它是否为1即可。
0x03 反调试代码优化
有些时候调试沙箱会自动跳过一些判断,让程序继续执行,这样一定程度会绕过反调试代码,所以判断是否被调试的代码最好不要用if...else
语法去写。
#include <Windows.h>
#include <stdio.h>
int main()
{
DWORD dwIsDebug = 0;
__asm {
pushad
mov eax, fs: [30h]
movzx ecx, byte ptr[eax + 2]
cmp ecx, 1
jz Again
jmp Bye
Again :
int 3
Bye :
popad
}
printf("HelloWorld...\n");
return 0;
}
上图中,如果遇到调试器,将会无限触发断点,而这个汇编代码块,在Windows代码中也是可以通用的。
编译器生成的机器代码其实往往并不是可控的,都是有迹可循的。
0x04 总结
本文对反调试技术有了一个初步认识,并了解了shellcode的编写过程,以及通过一个小例子实践了反调试代码。