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字段的详细信息:

2021-09-14-09-56-44

Shellcode

shellcode是一段用于利用软件漏洞而执行的代码,shellcode执行后经常让攻击者获得shell而得名。一个漏洞从发现到利用,是艰巨的任务,因此往往编写shellcode是对漏洞挖掘黑客的一个风水岭。大部分shellcode编写知识都与Windows PEB结构紧密相连,Windows PEB结构中保存了当前进程的复杂信息,这些复杂信息中包含了PE文件的内存地址,通过解析PE文件,可以直接寻找到kernel32.dll模块,利用这个模块中的LoadlibraryAPI可以导入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;

从这一点,我们就更能深刻理解,线程才是一个程序的执行单元,进程只是一个“容器”。

2021-09-14-10-59-33

0x02 使用汇编语言访问PEB

我的理解:汇编语言分为纯汇编、伪汇编、宏汇编

  1. 纯汇编是机器代码,是编译器最终生成的代码
  2. 伪汇编是与一定开发环境绑定在一起使用的汇编代码,例如C语言中的__asm { }
  3. 宏汇编是一定程度上将汇编语言进行了抽象、过程封装,比如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

2021-09-14-11-39-43

2021-09-14-11-41-12

如果想要判断当前程序是否被调试,可以获取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;
}

2021-09-14-11-47-16

上图中,如果遇到调试器,将会无限触发断点,而这个汇编代码块,在Windows代码中也是可以通用的。

编译器生成的机器代码其实往往并不是可控的,都是有迹可循的。

0x04 总结

本文对反调试技术有了一个初步认识,并了解了shellcode的编写过程,以及通过一个小例子实践了反调试代码。