逆向工程核心原理笔记-4

0x00 优秀分析工具标准

 即便是普通的工具,对其认真研习并运用到极致,它也能成为天下独一无二的优秀工具。

0x01 64位计算

 windows 95是32位的,但也能运行16位DOS。windows 2000/XP也是32位的。IA-64(Itanium)是专用在大型服务器与超算中,不支持32位。而现在的PC中的x64则支持32位。如果要用64位支持32位程序,首先在数据的定义上,如下所示:

image-20230423194634120

1
2
3
ILP32: Integer、Long、Pointer-32位
LLP: LongLong、Pointer-64位
LP64: Long、Pointer-64位

 微软提供了WOW64(windows on windows 64)的机制,使得32位程序在64位程序中正常运行。64位应用程序会加载kernel32.dll(64位)与ntdll.dll (64位)。而32位应用程序则会加x64载kernel32.dll(32位)与ntdll.dll(32位),WOW64会在中间将ntdll.dll(32位)的请求(API调用)重定向到ntdll.dll(64位)。如下图所示:

image-20230423195727831

 windows下有SysWOW64System32两个文件夹,SysWOW64中存放32位dll,当运行32位程序时,就用这个,映射到System32文件夹下的dll中。System32存放64位dll。因此,64位应用程序中使用GetSystemDirectory查找系统文件夹,正常返回System32文件夹,32位应用程序中调用GetSystemDirectory返回的文件夹名称也为System32,文件夹的实际内容与SysWOW64文件夹是一样的,这是WOW64在中间截获了API调用并进行操作后返回的结果,这使32位应用程序可以正常运行。

 对于注册表而言,使用32位进程请求访问HKLM/SOFTWARE下的键时,WOW64会将其重定向到32位的HKLM/SOFTWARE/Wow6432Node下的键。当使用64位进程请求访问HKLM/SOFTWARE下的键时,就正常进行访问。注意,与文件系统不同,注册表无法完全分离32位与64位,经常出现32/64位共用的情形。有时候向 32 位部分写入的值会自动写入 64 位部分。

0x02 x64处理器

 x64相比于x86新增了很多东西,我们在此只说一小部分。

  • 内存地址变为64位,因此寄存器大小以及栈都变为64位。
  • x64进程虚拟内存大小为16TB,32位进程虚存仅为4GB。
  • x64系统中通用寄存器大小为64位,数量增加到18个,此外,还有16个XMM寄存器,每个XMM寄存器为128位。64位寄存器的字母以R开头,x86以E开头。64位系统中不使用段寄存器。

image-20230423204452028

  • call/jmp解析方式不同。

 32位如下所示,其中FF15后面的00405000解析为绝对地址。

image-20230423205047985

 64位如下所示,其中FF15后面的3FFA解析为相对地址。

image-20230423205207224

计算方法:00000001 0040100 + 3FFA + 6 = 00000001 00405000。而又因为00000001 00405000存储着00000000 75CE1E12,因此最后被解析为call 00000000 75CE1E12

  • 32位调用函数包括cdeclstdcallfastcall三种,而64位系统中只有fastcall,它把函数的4个参数传递到寄存器中,进行参数传递,64位系统中由调用者清理栈。(复习:被调函数执行完毕后,函数的调用者(Caller)负责清理存储在栈中的参数,这种方式称为cdecl方式。反之,被调用者(Callee)负责清理保存在栈中的参数,这种方式称为stdcall方式。fastcall方式与stdcall类似,但是使用寄存器传递前两个参数,而不是使用栈。其调用速度快。)如下表所示:

image-20230423210308824

  • 调用子函数时,不再使用push命令传递参数,而是通过mov指令操作寄存器与预定的栈来传递。创建栈帧时也不再使用RBP寄存器,而是直接使用RSP寄存器来实现。使用 Visual C++ 编译32位程序时,若开启了编译器的优化功能,则几乎看不到使用EBP寄存器的栈帧。

 该方式的优点:调用子函数无需改变栈指针RSP,函数返回也无需清理RSP,大幅度提升速度。下面来验证一下,代码如下,分别用win32与x64进行编译:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include "stdio.h"
#include "windows.h"

void main()
{
HANDLE hFile = INVALID_HANDLE_VALUE;

hFile = CreateFileA("c:\\work\\ReverseCore.txt", // 1st - (string)
GENERIC_READ, // 2nd - 0x80000000
FILE_SHARE_READ, // 3rd - 0x00000001
NULL, // 4th - 0000000000
OPEN_EXISTING, // 5th - 0x00000003
FILE_ATTRIBUTE_NORMAL, // 6th - 0x00000080
NULL); // 7th - 0x00000000

if( hFile != INVALID_HANDLE_VALUE )
CloseHandle(hFile);
}

 win32的main函数情况:

(1)不使用栈帧。由于代码比较简单,变量又少,开启编译器的优化选项后,栈帧会被省略。

(2)调用子函数(CreateFileACloseHandle)时使用栈传递参数特征。

(3)使用PUSH指令压入栈的函数参数不需要main函数清理。在32位环境中采用stdcall方式调用Win32API时,由被调用者清理栈。

(4)调用CreateFileA后,CreateFileA会自动调CreateFileW

 x64的程序则需要使用windbg调试。其main代码如下所示:

image-20230424093158952

(1)使用变形的栈帧。在代码起始部分分配58h字节大小的栈,最后在RET命令之前释放。栈操作并未使用RBP寄存器,而直接使用RSP寄存器。

(2)几乎没有使用PUSH/POP指令。设置参数的代码(00000001 4000101d-00000001 40001044)。前4个参数使用寄存器(RCX、RDX、R8、R9)。后3个参数使用栈。有意思的是并未看到调用者清理栈(这原来是64位fastcall的特征),原因在于子函数使用的是分配给main的栈,子函数本身不会分配栈。

(3)第5个参数在栈中的存储位置有些奇怪。如下所示:

1
00000001-40001028 mov dword ptr [rsp+20h], 3;

 从以上代码可以看到,第5个参数在栈中的存储位置为[rsp+20h][rsp]并未指向栈顶。原因在于,虽然x64系统中前4个参数使用寄存器传递,但栈中仍然为它们预留了同等大小的空间(20h=4*8字节)。

0x03 PE32+

 PE32+是64位windows的可执行文件格式。前面说过,64位windows的进程虚拟内存为16TB,低8TB给用户模式,高8TB给内核模式。本节主要讲解PE32+与PE32的不同点。

PE32+IMAGE_NT_HEADERS如下:

image-20230424112901180

PE32+IMAGE_FILE_HEADER的Machine值为0x8664PE32的Machine值为0x014C

PE32相比,PE32+IMAGE_OPTIONAL_HEADER变化最大。具体变化简述如下:

1
2
3
4
Magic。PE32中Magic值为010B,PE32+中Magic值为020B。
BaseOfData。PE32文件中该字段用于指示数据节的起始地址(RVA)而PE32+文件中删除了该字段。
ImageBase。由4字节变为8字节。AddressOfEntryPoint、SizeOfImage等字段大小与原PE32位是一样的,都是4字节,意味着 PE32+格式的文件占用的实际虚拟内存中,各映像的大小最大为4GB。但是由于ImageBase的大小为8个字节(64位),程序文件可以加载到进程虚拟内存中的任意地址。
堆和栈相关的字段变为8字节。

PE32相比,IMAGE_THUNK_DATA结构体的大小由原来的4个字节变为8个字节。下图是IMAGE_IMPORT_DESCRIPTOR示意图(其中OriginalFirstThunkFirstThunkIMAGE_THUNK_DATA结构体,装载PE文件时,OS的PE装载器会向IAT中写入真正的API入口地址):

image-20230424125405520

注:

  • IMAGE_TLS_DIRECTORY:在Windows PE格式中,TLS代表线程本地存储(Thread Local Storage),用于在多线程应用程序中为每个线程分配独立的内存空间。IMAGE_TLS_DIRECTORY是PE结构中的一个数据结构,用于描述TLS数据的位置和大小。
  • CFF ExplorerPE view都是观察PE格式的工具,但是CFF Explorer可以查看64位PE的格式,而PE view只能观察32位PE格式。

0x04 windbg学习

 符号指的是调试信息文件(*.pdb)。使用Visual C++编译程序时,除了生成PE文件外,还会一起生成(*pdb:Program Data Base)程序数据库文件,该文件包含PE文件的各种调试信息(变量/函数名、函数地址等)。

windbg不仅可以对64位程序进行调试,还可以进行内核调试。使用WinDbg进行内核调试时,一般要使用2台PC(调试器-被调试者)调试前需要将2台PC连接起来。使用虚拟机也可以,被称为本地内核调试(Local Kernel Debugging),但是调试器的许多功能在这种调试方式下都受到限制。

 为什么要用两台PC?内核调试中,被调试者为系统内核,所以 OS 系统自身会暂停。

 下面我们想进行本地内核调试。

1
2
3
4
5
1. bcdedit  // 查看系统信息
2. bcdedit -debug on // 打开调试模式
3. 管理员打开windbg-File-Kernel debug-Local-确定
4. 之后显示Ntoskrnl.exe文件的装载地址,此文件其实是windows内核的内存装载映像
5. 输入u nt!ZwCreateFile L50,代表输出ZwCreateFile的前50行汇编指令

0x05 64位调试

 示例文件源代码:

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
// WOW64Test.cpp -> WOW64Test.exe
#include "stdio.h"
#include "windows.h"
#include "Shlobj.h"
#include "tchar.h"
#pragma comment(lib, "Shell32.lib")

int _tmain(int argc, TCHAR* argv[])
{
HKEY hKey = NULL;
HANDLE hFile = INVALID_HANDLE_VALUE;
TCHAR szPath[MAX_PATH] = {0,};

////////////////
// system32 folder
if( GetSystemDirectory(szPath, MAX_PATH) )
{
_tprintf(L"1) system32 path = %s\n", szPath);
}

////////////////
// File size
_tcscat_s(szPath, L"\\kernel32.dll");
hFile = CreateFile(szPath, GENERIC_READ, FILE_SHARE_READ, NULL,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if( hFile != INVALID_HANDLE_VALUE )
{
_tprintf(L"2) File size of \"%s\" = %d\n",
szPath, GetFileSize(hFile, NULL));
CloseHandle(hFile);
}

////////////////
// Program Files
if( SHGetSpecialFolderPath(NULL, szPath,
CSIDL_PROGRAM_FILES, FALSE) )
{
_tprintf(L"3) Program Files path = %s\n", szPath);
}

////////////////
// Registry
if( ERROR_SUCCESS == RegCreateKey(HKEY_LOCAL_MACHINE,
L"SOFTWARE\\ReverseCore", &hKey) )
{
RegCloseKey(hKey);
_tprintf(L"4) Create Registry Key : HKLM\\SOFTWARE\\ReverseCore\n");
}

return 0;
}

 分别编译成x86与x64程序,并运行,其输出如下:

image-20230424144647177

 对于x86程序,VC++2010工具生成的(基于控制台的)PE32文件的EP代码的特征,ollydbg一开始就停在了entry处:

image-20230424145706207

image-20230424145814617

 如何查找main函数:

1
2
3
(1) 在GetCommandLine上设置断点。被调试者(WOW64Test_x86.exe)是一个基于控制台的应用程序。因此,调用main函数前会先调用GetCommandLine,需要先把main函数的参数存储到栈中。
(2) 在GetSystemDirectory、GetFileSize、CreateFile等API上打断点。
(3) 字符串检索。

 对于x64程序,使用windbg,程序一开始停在ntdll.dll的系统断点处。因此,我们先要程序停在entry处。

1
2
3
4
5
6
(1) 输入!dh WOW64Test_x64,查看文件头信息,从而获得 entry point 地址,发现为142C(RVA)。
(2) 输入g WOW64Test_x64+0x142C,到达entry地址。
(3) 输入u eip L10,显示10条汇编指令。
(4) 连续输入p,到startup代码处。
(5) 使用bp kernel32!GetCommandLineWStub 设置断点,之后使用g来运行。
(6) 使用r rsp查看rsp中的值,并用dq rsp查看rsp的值指向的地址的8字节值。(也就是看栈)

 如下所示:

image-20230424154857618

 由于GetCommandLineW没有参数,因此rsp指向函数返回地址,也就是00000001 40001381

1
(7) g 00000001 40001381,之后输入u eip L20

 如下所示:

image-20230424155451417

1
(8) g 00000001 400013ea,转到main函数,使用r查看其寄存器

image-20230424155910387

image-20230424160155922

image-20230424160413801

image-20230424160955237

 上述,main(int argc, char* argv[]),其中rcx==argc==1argv==rdx是参数数组,r8是一个指针数组,所有元素都指向栈区域,它是使用Visual C++ 2010工具编译代码时由编译器自动添加的参数。

注:

(1)bp与bu的区别:bp就是正常断点,bu命令也用于设置断点,但它是一个不稳定的断点,即一旦命中后就自动失效。

0x06 ASLR

ASLR是一种针对缓冲区溢出的安全保护技术,微软从windows vista(内核版本6.0开始)开始使用此技术。

image-20230424163445011

ASLR使得PE文件每次加载到内存的起始地址都会随机变化,并且每次运行程序时相应进程的栈以及堆的起始地址也会随机改变。也就是说,每次EXE文件运行时加载到进程内存的实际地址都不同,最初加载DLL文件时装载到内存中的实际地址也是不同的。

image-20230424164225737

 可以看到,采用ASLR的程序有.reloc节区,而不采用ASLR的程序没有.reloc节区。两者Number of Sections也不一样。

 两个重要的不同字段:IMAGE_FILE_HEADER\CharacteristicsIMAGE_OPTIONAL_HEADER\DLL Characteristics

image-20230424170641049

image-20230424170749191

练习-删除ASLR功能

 跟着P428来操作就可以。仅需要删除IMAGE_OPTIONAL_HEADER\DLL Characteristics的部分即可。而添加ASLR功能比较麻烦,既需要在文件头添加ASLR相应条目,还要增加重定位节区(.reloc)。

0x07 内核版本为6中的会话

 0x06中说了,windows vista使用的windows版本为6的内核。其采用了一种新的会话管理机制

 一个在XP中可运行的服务程序在Vista中无法正常运行,且这些服务程序主要是与用户存在交互行为的程序。这是为什么呢?Kernel 6中使用的会话管理机制引起的。会话管理机制意味着:使用CreateRemoteThread进行DLL注入的方法不再适用于kernel6中的服务进程(对一般进程仍然适用)。

会话指的是登录后的用户环境。以Windows操作系统为例,”切换用户”可以创建本地用户会话,”远程桌面连接”可以创建远程用户会话。

image-20230424185917114

 XP与vista的系统进程与服务进程都在ID为0的会话中。但是,对于XP来说,第一个登陆系统的用户会话ID为0;而对于vista来说,第一个登录系统的用户会话ID为1。相当于将会话ID为0与其他会话隔离开,能够提高安全性,这叫做会话0隔离机制

 虽然微软的ASLR与会话0隔离机制能够提高安全性,但是也有攻破的方法。例如,对于会话0隔离机制,会话1中的进程可以强行终止会话0中的进程;对于ASLR,ReadProcessMemoryWriteProcessMemoryVirtualAllocExAPI也能正常运行(绕过了ASLR)。

0x08 内核版本为6中的DLL注入

 由于使用了新的会话管理机制,因此使用CreateRemoteThread注入DLL的旧方法对某些进程(服务进程)不再适用。这是为什么呢?该如何解决呢?

 首先,先给出一个失败的例子:

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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
// InjectDll.cpp -> InjectDll.exe
// 老生常谈
#include "windows.h"
#include "tchar.h"

BOOL SetPrivilege(LPCTSTR lpszPrivilege, BOOL bEnablePrivilege)
{
TOKEN_PRIVILEGES tp;
HANDLE hToken;
LUID luid;

if( !OpenProcessToken(GetCurrentProcess(),
TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY,
&hToken) )
{
_tprintf(L"OpenProcessToken error: %u\n", GetLastError());
return FALSE;
}

if( !LookupPrivilegeValue(NULL, // lookup privilege on local system
lpszPrivilege, // privilege to lookup
&luid) ) // receives LUID of privilege
{
_tprintf(L"LookupPrivilegeValue error: %u\n", GetLastError() );
return FALSE;
}

tp.PrivilegeCount = 1;
tp.Privileges[0].Luid = luid;
if( bEnablePrivilege )
tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
else
tp.Privileges[0].Attributes = 0;

// Enable the privilege or disable all privileges.
if( !AdjustTokenPrivileges(hToken,
FALSE,
&tp,
sizeof(TOKEN_PRIVILEGES),
(PTOKEN_PRIVILEGES) NULL,
(PDWORD) NULL) )
{
_tprintf(L"AdjustTokenPrivileges error: %u\n", GetLastError() );
return FALSE;
}

if( GetLastError() == ERROR_NOT_ALL_ASSIGNED )
{
_tprintf(L"The token does not have the specified privilege. \n");
return FALSE;
}

return TRUE;
}

BOOL InjectDll(DWORD dwPID, LPCTSTR szDllPath)
{
HANDLE hProcess = NULL, hThread = NULL;
HMODULE hMod = NULL;
LPVOID pRemoteBuf = NULL;
DWORD dwBufSize = (DWORD)(_tcslen(szDllPath) + 1) * sizeof(TCHAR);
LPTHREAD_START_ROUTINE pThreadProc;
BOOL bRet = TRUE;

// #1. 通过dwPID,打开要注入的进程
if ( !(hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPID)) )
{
_tprintf(L"OpenProcess(%d) failed!!! [%d]\n", dwPID, GetLastError());
return FALSE;
}

// #2. 向要注入的进程中分配存储空间
pRemoteBuf = VirtualAllocEx(hProcess, NULL, dwBufSize, MEM_COMMIT, PAGE_READWRITE);

// #3. 将要注入的dll放入空间中
WriteProcessMemory(hProcess, pRemoteBuf, (LPVOID)szDllPath, dwBufSize, NULL);

// #4. LoadLibraryA()
hMod = GetModuleHandle(L"kernel32.dll");
pThreadProc = (LPTHREAD_START_ROUTINE)GetProcAddress(hMod, "LoadLibraryW");

// #5. LoadLibraryA(hack.dll)
hThread = CreateRemoteThread(hProcess, NULL, 0, pThreadProc, pRemoteBuf, 0, NULL);
if( hThread == NULL )
{
_tprintf(L"[ERROR] CreateRemoteThread() failed!!! [%d]\n", GetLastError());
bRet = FALSE;
goto _ERROR;
}

WaitForSingleObject(hThread, INFINITE);

_ERROR:

if( pRemoteBuf )
VirtualFreeEx(hProcess, pRemoteBuf, 0, MEM_RELEASE);

if( hThread )
CloseHandle(hThread);

if( hProcess )
CloseHandle(hProcess);

return bRet;
}

int _tmain(int argc, TCHAR *argv[])
{
if( argc != 3)
{
_tprintf(L"USAGE : %s <pid> <dll_path>\n", argv[0]);
return 1;
}

// change privilege
if( !SetPrivilege(SE_DEBUG_NAME, TRUE) )
return 1;

// inject dll
if( InjectDll((DWORD)_tstol(argv[1]), argv[2]) )
_tprintf(L"InjectDll() success!!!\n");
else
_tprintf(L"InjectDll() failed!!!\n");

return 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
// dummy.cpp -> dummp.dll
#include "windows.h"
#include "tchar.h"

BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
TCHAR szPath[MAX_PATH] = {0,};
TCHAR szMsg[1024] = {0,};
TCHAR *p = NULL;

switch( fdwReason )
{
case DLL_PROCESS_ATTACH :
GetModuleFileName(NULL, szPath, MAX_PATH);
p = _tcsrchr(szPath, L'\\');
// 如果注入成功,就输出相关调试信息
if( p != NULL )
{
_stprintf_s(szMsg, 1024 - sizeof(TCHAR),
L"Injected in %s(%d)",
p + 1, // Process Name
GetCurrentProcessId()); // PID
OutputDebugString(szMsg);
}

break;
}

return TRUE;
}

 可以发现,向正常进程notepad.exe注入是成功的,然而向服务进程svchost.exe注入则是失败的。

 首先,调试InjectDll.exe发现,运行完CreateRemoteThread(),显示错误ERROR_ACCESS_DENIED

 再次进行调试,可以发现CreateRemoteThread()调用了CreateRemoteThreadEx(),发现参数差不多(只是多了一个lpAttributeList),其又调用了ntdll!ZwCreateThreadEx(),其参数也是差不多,重要参数都传过来了。书上说,最终调用了sysenter,但是我没有找到进入sysenter的证据,程序会在某个环节卡住。

 实际上,CreateRemoteThreadEx()ntdll!ZwCreateThreadEx()都是vista新增的API,之前没有。下图展示了在XP与win7中调用CreateRemoteThread的流程:

image-20230424202136277

 那我们得到结论,由于增加的API,让注入失败。

注:kernelbase.dll是从Vista开始新增的DLL文件,负责包装(wrapper)kernel32.dll

由于kernelbase!CreateRemoteThreadEx只是kernel32!CreateRemoteThread的包装器(wrapper),他俩没啥区别,因此问题就在ntdll!ZwCreateThreadEx()中。

ZwCreateThreadEx()定义如下:

image-20230424203243937

 下图是注入失败时ZwCreateThreadEx()的参数:

image-20230424203148869

Google后发现,在vista中,直接调用ZwCreateThreadEx()就能注入成功,不受会话影响,但是第7个参数(CreateSuspended)要设置为0,而不是1。Windows XP开始,CreateRemoteThread内部的实现算法采用了挂起模式,即先创建出线程,再使用”恢复运行”方法继续执行(CreateSuspended=1)。

我改了第7个参数发现没用。

 发现了一个问题,就是ZwCreateThreadEx()NtCreateThreadEx()地址一样,且我看不到ZwCreateThreadEx()的代码,且调试时,CreateRemoteThreadEx()调用的是ntdll!NtCreateThreadEx(),而不是ntdll!ZwCreateThreadEx()。经过思考,可能是用ollydbg进行调试的问题,ollydbg只能调试用户模式下的应用。因此,换成windbg调试一波(但是要进行两个主机之间的连接,win7虚拟机一直报dll缺失,懒了,搁置)

注:

  • ZwCreateThreadEx()NtCreateThreadEx()的区别是什么?ZwCreateThreadEx()是内核调用的。NtCreateThreadEx()由用户模式代码调用。

 除了改第7个参数以外,书中还说了一种解决注入失败的方法。可以看到,ZwCreateThreadEx()的第一个参数是线程句柄,说明此时远程线程已经被创建,但是无法工作。原因可能就是ZwResumeThreadEx调用失败(注意是resume)导致的。经过调试发现,程序将ZwResumeThreadEx跳过去了,通过改变程序执行路径,就能实现正确的DLL注入。(但是复现的时候有问题)

 存在的问题如下:

1
2
3
1. 找不到NtCreateThreadEx(),只有ZwCreateThreadEx()
2. 修改参数值 CreateSuspended 没用
3. 程序运行到 NtCreateThreadEx() 总是卡在 ADD ESP, 4 上

总结一下无法注入的原因:在API内部创建远程线程时采用了挂起模式,若远程进程属于会话0,则不会恢复运行,而是直接返回错误。(一般来说,在创建远程线程时,先采用挂起模式创建,再”恢复运行”。)

 之后,我们编写一个新的InjectDll.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
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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
#include "windows.h"
#include "stdio.h"
#include "tchar.h"

BOOL SetPrivilege(LPCTSTR lpszPrivilege, BOOL bEnablePrivilege)
{
...
}

// 定义 ZwCreateThreadEx 函数()
typedef DWORD (WINAPI *PFNTCREATETHREADEX)
(
PHANDLE ThreadHandle,
ACCESS_MASK DesiredAccess,
LPVOID ObjectAttributes,
HANDLE ProcessHandle,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
BOOL CreateSuspended,
DWORD dwStackSize,
DWORD dw1,
DWORD dw2,
LPVOID Unknown
);

// 判断是否是vista
BOOL IsVistaOrLater()
{
OSVERSIONINFO osvi;

ZeroMemory(&osvi, sizeof(OSVERSIONINFO));
osvi.dwOSVersionInfoSize = sizeof(OSVERSIONINFO);

GetVersionEx(&osvi);

if( osvi.dwMajorVersion >= 6 )
return TRUE;

return FALSE;
}

BOOL MyCreateRemoteThread(HANDLE hProcess, LPTHREAD_START_ROUTINE pThreadProc, LPVOID pRemoteBuf)
{
HANDLE hThread = NULL;
FARPROC pFunc = NULL;

if( IsVistaOrLater() ) // Vista, 7, Server2008
{
// 直接调用 NtCreateThreadEx
pFunc = GetProcAddress(GetModuleHandle(L"ntdll.dll"), "NtCreateThreadEx");
if( pFunc == NULL )
{
printf("MyCreateRemoteThread() : GetProcAddress(\"NtCreateThreadEx\") failed!!! [%d]\n",
GetLastError());
return FALSE;
}

((PFNTCREATETHREADEX)pFunc)(&hThread,
0x1FFFFF,
NULL,
hProcess,
pThreadProc,
pRemoteBuf,
FALSE, // 第7各参数设置为0
NULL,
NULL,
NULL,
NULL);
if( hThread == NULL )
{
printf("MyCreateRemoteThread() : NtCreateThreadEx() failed!!! [%d]\n", GetLastError());
return FALSE;
}
}
else // 2000, XP, Server2003
{
hThread = CreateRemoteThread(hProcess,
NULL,
0,
pThreadProc,
pRemoteBuf,
0,
NULL);
if( hThread == NULL )
{
printf("MyCreateRemoteThread() : CreateRemoteThread() failed!!! [%d]\n", GetLastError());
return FALSE;
}
}

if( WAIT_FAILED == WaitForSingleObject(hThread, INFINITE) )
{
printf("MyCreateRemoteThread() : WaitForSingleObject() failed!!! [%d]\n", GetLastError());
return FALSE;
}

return TRUE;
}

BOOL InjectDll(DWORD dwPID, char *szDllName)
{
HANDLE hProcess = NULL;
LPVOID pRemoteBuf = NULL;
FARPROC pThreadProc = NULL;
DWORD dwBufSize = strlen(szDllName)+1;

if ( !(hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPID)) )
{
printf("[ERROR] OpenProcess(%d) failed!!! [%d]\n",
dwPID, GetLastError());
return FALSE;
}

pRemoteBuf = VirtualAllocEx(hProcess, NULL, dwBufSize,
MEM_COMMIT, PAGE_READWRITE);

WriteProcessMemory(hProcess, pRemoteBuf, (LPVOID)szDllName,
dwBufSize, NULL);

pThreadProc = GetProcAddress(GetModuleHandle(L"kernel32.dll"),
"LoadLibraryA");

if( !MyCreateRemoteThread(hProcess, (LPTHREAD_START_ROUTINE)pThreadProc, pRemoteBuf) )
{
printf("[ERROR] MyCreateRemoteThread() failed!!!\n");
return FALSE;
}

VirtualFreeEx(hProcess, pRemoteBuf, 0, MEM_RELEASE);

CloseHandle(hProcess);

return TRUE;
}

int main(int argc, char *argv[])
{
// adjust privilege
SetPrivilege(SE_DEBUG_NAME, TRUE);

// InjectDll.exe <PID> <dll_path>
if( argc != 3 )
{
printf("usage : %s <PID> <dll_path>\n", argv[0]);
return 1;
}

if( !InjectDll((DWORD)atoi(argv[1]), argv[2]) )
{
printf("InjectDll() failed!!!\n");
return 1;
}

printf("InjectDll() succeeded!!!\n");

return 0;
}

发现在win7下官方给的示例也不行,win11下也不行,只不过报错变了。离谱啊?为啥啊?

image-20230424223444435

 经过分析,是如下代码出了问题,其中pFunc总为空(问题搁置):

image-20230424224956644

0x09 InjDll.exe:DLL注入专用工具

InjDll.exe是之前我们使用的DLL注入的程序,若目标进程为32位,那么InjDll.exe和要注入的DLL都是32位。若目标进程为64位,那么InjDll.exe和要注入的DLL都是64位。(突然想到,之前的注入问题是不是64位与32位的问题??)

高级逆向分析技术

 咱什么时候也弄上了高级逆向啊?我好牛蛙。

0x10 TLS回调函数

 TLS(Thread Local Storage,线程局部存储)回调函数(Callback Function)常用于反调试。TLS回调函数的调用运行要先于EP代码的执行,该特征使它可以作为一种反调试技术使用。

 下面跟着书做实验:

1
打开HelloTls.exe,显示Hello。但用Ollydbg调试之后,显示Debugger Detected。(为什么不同呢?是因为程序运行EP代码前先调用了TLS回调函数,而该回调函数中含有反调试代码,使程序在被调试时弹出"Debugger Detected!"消息对话框。)

TLS是各线程的独立的数据存储空间,使用TLS技术可在线程内部使用或修改进程的全局数据,就像对待自身的局部变量一样。

 如果在编程中启用了TLS功能,PE头文件就会设置TLS表,在IMAGE_NT_HEADERS -> IMAGE_OPTIONAL_HEADER -> IMAGE_DATA_DIRECTORY[9]中,它存的值指向了IMAGE_TLS_DIRECTORY。其结构如下:

image-20230426212313168

 结构体中AddressOfCallBacks比较重要,它指向含有TLS回调函数的数组(VA),这意味着同一程序可以注册多个TLS回调函数。

 TLS回调函数是指,每当创建或终止进程的线程时会自动调用执行的函数。创建进程的主线程时也会自动调用回调函数,且其调用执行先于EP代码。反调试技术利用的就是TLS回调函数的这一特征。

 TLS回调函数的定义如下:

image-20230427142514992

 再来看DLL的main函数的定义:

image-20230427142546390

 是不是差不多?在TLS回调函数的定义中,DllHandle为模块句柄(加载地址);Reason表示调用TLS回调函数的原因,包括DLL_PROCESS_ATTACH(1)DLL_THREAD_ATTACH(2)DLL_THREAD_DETACH(3)DLL_PROCESS_DETACH(0)

 下面看一个程序TlsTest.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
43
44
45
46
47
48
49
50
51
52
53
54
55
#include <windows.h>

// 指示编译器在代码中使用线程局部存储TLS
// TLS是一种机制,允许多线程程序为每个线程分配独立的存储空间
#pragma comment(linker, "/INCLUDE:__tls_used")

void print_console(char* szMsg)
{
HANDLE hStdout = GetStdHandle(STD_OUTPUT_HANDLE);

WriteConsoleA(hStdout, szMsg, strlen(szMsg), NULL, NULL);
}

void NTAPI TLS_CALLBACK1(PVOID DllHandle, DWORD Reason, PVOID Reserved)
{
char szMsg[80] = {0,};
wsprintfA(szMsg, "TLS_CALLBACK1() : DllHandle = %X, Reason = %d\n", DllHandle, Reason);
print_console(szMsg);
}

void NTAPI TLS_CALLBACK2(PVOID DllHandle, DWORD Reason, PVOID Reserved)
{
char szMsg[80] = {0,};
wsprintfA(szMsg, "TLS_CALLBACK2() : DllHandle = %X, Reason = %d\n", DllHandle, Reason);
print_console(szMsg);
}

// 定义TLS数组
#pragma data_seg(".CRT$XLX")
PIMAGE_TLS_CALLBACK pTLS_CALLBACKs[] = { TLS_CALLBACK1, TLS_CALLBACK2, 0 };
#pragma data_seg()

DWORD WINAPI ThreadProc(LPVOID lParam)
{
print_console("ThreadProc() start\n");

print_console("ThreadProc() end\n");

return 0;
}

int main(void)
{
HANDLE hThread = NULL;

print_console("main() start\n");

hThread = CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL);
WaitForSingleObject(hThread, 60*1000);
CloseHandle(hThread);

print_console("main() end\n");

return 0;
}

 程序运行如下:

image-20230427144905556

 可以看到,在进程初始与结束,会运行TLS。在线程初始与结束,也会运行TLS。程序中没有使用printf而是使用了WriteConsole,是因为TLS函数先于主线程来运行,此时可能C语言的某些库还没有加载,所以使用printf会报错。

 使用ollydbg调试此程序时需要改变first pause at Tls callback。调试Hellotls.exe,发现回调函数:

image-20230427150016162

 其使用kernel32.IsDebuggerPresent来判断是否有调试器。

 下面对于简单的Hello.exe添加TLS功能。

1
2
3
4
5
6
// Hello.cpp -> Hello.exe
#include "windows.h"
void main()
{
MessageBoxA(NULL, "Hello :)", "main()", MB_OK);
}

 添加思路如下:IMAGE_OPTIONAL_HEADER中添加TLS TableIMAGE_TLS_DIRECTORY)位置,之后在相应位置添加IMAGE_TLS_DIRECTORYIMAGE_TLS_DIRECTORY中有Address of Callbacks,定义好它的位置后,在相应位置添加TLS函数内容即可。

 其中IMAGE_TLS_DIRECTORYAddress of Callbacks放到哪儿呢?有几种可选位置:

1
2
3
1. 添加到某节区末尾的空白区域
2. 增加最后一个节区的大小(书中采用的方法)
3. 在PE文件最后添加一个新节区

 书中采用方法2,跟着书上的实验做,是可以进行的。具体步骤如下:

 首先,查看PE文件相关信息:Section Alignment=1000File Alignment=200。那么,由于我们选择方法2,即增加最后一个节区的大小,那么我们就把最后一个节区.rsrc后面添加200字节。(原来.rsrc节区的VirtualSize1B4,PE装载器会按照Section Alignment值对齐该值,即加载到内存中的大小为 1000。所以将节区的文件大小增加200后,实际VirtualSize值变为3B4,它比加载到内存中的尺寸1000要小,所以不需要再单独增大VirtualSize的值。)

 其次,更改.rsrc节区头信息,SizeofRawData=400(原来是200)、Characteristics=E0000060Characteristics的属性如下:

image-20230428115333896

 之后,设置TLS表(位置在IMAGE_NT_HEADERS -> IMAGE_OPTIONAL_HEADER -> IMAGE_DATA_DIRECOTRY[9])的RVA与Size。接下来,设置TLS表指向的IMAGE_TLS_DIRECTORY,在IMAGE_TLS_DIRECTORY中的AddressOfCallbacks中定义TLS函数的地址。并在相应地址填入回调函数,如下:

image-20230428120243068

 程序的解释如下:Reason参数值为1(DLL_PROCESS_ATTACH)时,MOV EAX, DWORD PTR FS:[30]用于检查PEB.BeingDebugged成员,若处于调试状态,则弹出消息框(MessageBoxA)后终止并退出进程(ExitProcess)。注意,0x49c2700x40c280存储MessageBoxA所需的字符串。

0x11 TEB

 TEB(Thread Environment Block)指线程环境块,该结构体包含进程中运行线程的各种信息,进程中的每个线程都对应1个TEB结构体。TEB结构体如下:

image-20230428155051055

 上述对于TEB的定义很简单(实际很复杂),windows 7与XP的TEB定义是不同的,也很复杂。使用windbg查看win7的TEB如下(内核模式下,在用户模式下使用此命令只能看到wow64_teb):

image-20230428161915690

 在TEB中,有几个成员比较重要。

 在用户调试中,比较重要的两个成员是NtTibProcessEnvironmentBlock。如下图所示:

image-20230428160505827

NtTibTIB: Thread Information Block)线程信息块定义如下所示:

image-20230428160831287

ExceptionList成员指向EXCEPTION_REGISTRATION_RECORD结构体组成的链表,它用于Windows的SEH(异常处理机制)。Self成员是 NT_TIB结构体的自引用指针,也是TEB结构体的指针(因为TEB结构体的第一个成员就是NT_TIB)。

 那么如何在用户模式下访问TEB结构体呢?除了使用windbg,还有就是通过API(Ntdll.NtCurrentTeb())进行访问。经过调试发现,TEB与FS段寄存器有某种关联。

 FS寄存器除了可以访问TLS,还指向当前线程的TEB。32位系统中进程的虚拟内存大小为4GB,因而需要32位的指针才能访问整个内存空间。但是FS寄存器的大小只有16位,那么它如何表示进程内存空间中的TEB结构体的地址呢?实际上,FS寄存器并非直接指向TEB结构体的地址,它持有SDT的索引,而该索引持有实际TEB地址。如下所示,TEB结构体位于FS段寄存器所指的段内存的起始地址处。:

image-20230428172345831

 那么,就有:FS:[0x18] = TEB.NtTib.Self = address of TIB = address of TEB,其中,self成员在TEB中的偏移是0x18。

 还有:FS:[0x30] = TEB ProcessEnvironmentBlock = address of PEB,PEB是进程环境块。

 最后,FS:[0] = TEB.NtTib.ExceptionList = address of SEH,SEH代表异常处理机制。

补充:

 1. SDT是Segment Descriptor Table(段描述符表)的缩写,其位于内核内存区域,其地址存储在特殊的寄存器 GDTR(Global Descriptor Table Resiger,全局描述符表寄存器)中。

 2. FS段寄存器作用:FS段寄存器提供一个用于访问线程局部存储(TLS)的基址。TLS是一种内存管理机制,允许多个线程在共享内存的情况下独立地访问自己的数据。当一个线程访问TLS时,它将使用FS段寄存器中指定的地址作为数据的基址。这个地址通常被初始化为指向一个线程私有的数据区域,这个数据区域可以存储线程专有的数据,例如线程的栈、线程局部变量等等。

0x12 PEB

 PEB,进程环境块,它是存放进程信息的结构体,尺寸非常大。获得PEB的两种方式如下:

  • 直接获取PEB地址。
1
MOV EAX,DWORD PTR FS:[30]     ; FS[30] = address of PEB
  • 先获取TEB地址,再通过ProcessEnvironmentBlock成员(+30偏移)获取PEB地址。
1
2
MOV EAX,DWORD PTR FS:[18]     ; FS[18] = address of TEB
MOV EAX,DWORD PTR DS:[EAX+30] ; DS[EA+30] = address of PEB

 PEB结构如下,与TEB一样,实际复杂得多:

image-20230428181927943

 其中的几个重要成员有:BeingDebuggedImageBaseAddressLdrProcessHeapNtGlobalFlag:(这样就能解释通0x10处的代码了)

image-20230428182324350

 下面是一个PEB的dump:

image-20230428184429094

 对于BeingDebugged成员,windows API中有一个Kernel32!IsDebuggerPresent(),代码如下,来判断是否被调试,其中调用了此成员:

image-20230428183907053

 对于ImageBaseAddress成员,其可以表示进程的基址。GetModuleHandle的API可以用来获取ImageBase,其中就调用了此成员。

 对于Ldr成员,它是指向_PEB_LDR_DATA结构体的指针,其结构如下:

image-20230428190402190

Ldr的作用是:当模块(DLL)加载到进程后,通过PEB.Ldr成员可以直接获取该模块的加载基地址。 _PEB_LDR_DATA结构体中,有3个_LIST_ENTRY类型的成员,分别为InLoadOrderModuleListInMemoryOrderModuleListInInitializationOrderModuleList。下面是_LIST_ENTRY的结构:

image-20230428190724386

 可以看到_LIST_ENTRY是一个双向链表,这个双线链表保存着_LDR_DATA_TABLE_ENTRY结构体的信息(我没看出来)每一个加载到进程中的DLL模块都有对应的_LDR_DATA_TABLE_ENTRY结构体。需要注意的是,_PEB_LDR_DATA中存在3种链表(InLoadOrderModuleListInMemoryOrderModuleListInInitializationOrderModuleList),即,存在多个_LDR_DATA_TABLE_ENTRY结构体,并且有3种链接方法可以将它们链接起来。

 对于ProcessHeapNtGlbalFlag成员,可以应用于反调试技术。若进程处于调试状态,它们就持有特定值,类似于BeingDebugged

0x13 SEH

 SEH(Structure Exception Handler)是Windows操作系统默认的异常处理机制。逆向分析中,SEH可以用于反调试程序。在程序源码中,使用__try__except__finally等关键字来具体实现。注意,SEH与C++中的trycatch相比有不同的结构。

 有一个练习示例seh.exe该程序故意触发了内存非法访问(Memory Access Violation)异常,然后通过SEH机制来处理该异常。并且使用PEB信息向程序添加简单的反调试代码,使程序在正常运行与调试运行时表现出不同的行为动作。此程序是通过编译空的main函数,之后用ollyDbg向其中加入汇编代码来实现的,因此没有源代码。

 内存非法访问如下:

image-20230503154102025

 红框语句就是向内存地址为0的单位放入1,但是内存地址0是未分配的,因此会报异常。之后按shift+F9将异常抛给程序,它就会显示检测出调试器

 OS处理异常的方法:

1
2
3
4
1. 运行时出现异常:
进程运行过程中若发生异常,OS会委托进程处理。若进程代码中存在具体的异常处理(如SEH)代码,则能顺利处理相关异常,程序继续运行。但如果进程内部没有具体实现SEH,那么相关异常就无法处理,OS就会启动默认的异常处理机制,终止进程运行。
2. 调试时出现异常:
被调试者出现异常,OS会将异常交给调试器处理。此时调试器可以选择:(1)直接修改异常:代码、寄存器。(2)将异常交给被调试者,被调试者使用SEH(异常处理函数)进行处理。(3)若调试器与被调试者都无法处理,则最终由OS处理。

 下面介绍几种常见的异常:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
1. EXCEPTION_ACCESS_VIOLATION(C0000005)
触发条件:试图访问不存在或不具访问权限的内存区域
举例:
(1)MOV DWORD PTR DS:[0], 1 // 内存地址0处是尚未分配的区域
(2)ADD DWORDPTR DS:[401000], 1 // .text节区的起始地址401000仅具有“读”权限 (无“写”权限)
(3)XOR DWORDPTR DS:[80000000], 1234 // 内存地址80000000属于内核区域,用户模式下无法访问
2. EXCEPTION_BREAKPOINT(80000003)
触发条件:断点处
举例:
(1)INT 3,机器码为0xCC
3. EXCEPTION_ILLEGAL_INSTRUCTION(C000001D)
触发条件:遇到无法解析的指令
举例:
(1) 0FFF
4. EXCEPTION_INT_DIVIDE_BY_ZERO(C0000094)
触发条件:除法运算中分母为0
举例:
(1) MOV EAX, 1
XOR ECX, ECX
DIV ECX
5. EXCEPTION_SINGLE_STEP(80000004)
触发条件:执行1条指令,然后暂停,这也叫CPU的单步模式。只要将EFLAGS寄存器的TF设置为1后,CPU就会进入单步模式

注:PE tools可以将运行的进程转储,可以作为ollydbg保存调试文件的另一种方式。

 SEH以链的形式存在。第一个异常处理器中若未处理相关异常,它就会被传递到下一个异常处理器,直到得到处理。SEH是由_EXCEPTION_REGISTRATION_RECORD结构体组成的链表,_EXCEPTION_REGISTRATION_RECORD的结构如下,若next为全1,那么就代表是最后一个节点:

image-20230503162505280

 SEH链如下:

image-20230503162541039

 而异常处理函数(上图HANDLER)定义如下:

image-20230503162710047

 可以看到,异常处理函数接收4个参数,返回一个PEXCEPTION_DISPOSITION

 异常处理函数的第1个参数,类型为EXCEPTION_RECORD,定义如下(其中ExceptionCode指明异常类型,ExceptionAddress指明异常发生的地址):

image-20230503162954366

 异常处理函数的第2个参数pFrame,代表SEH链的起始地址。

 异常处理函数的第3个参数,类型为CONTEXT,它用于存储CPU各寄存器的值,也就是上下文。这种情况在多线程中有用,每个线程都有一个CONTEXT结构体。CPU暂时离开当前线程去运行其他线程时,CPU寄存器的值就会保存当前线程的CONTEXT结构体;CPU再次运行该线程时,会使用保存在CONTEXT结构体的值来覆盖CPU寄存器的值,然后从之前暂停的代码处继续执行。

 第4个参数pValue供系统内部使用,可忽略。

 异常处理函数的返回值(PEXCEPTION_DISPOSITION)是一个枚举类型,其定义如下:

image-20230503164224302

 异常处理函数如果能正确处理异常,则会返回ExceptionContinueExecution(0),之后从发生异常的代码处继续运行。若异常处理函数无法处理异常,则返回ExceptionContinueSearch(1),将异常派送到SEH链的下一个处理函数。

 通过TEB(线程)的NtTib可以访问SEH链,其中TEB.NtTib.ExceptionList是第一个SEH成员。FS段寄存器指向段内存的起始地址,FS:[0]即指向SEH链开头。

SEH定义安装

 C语言中使用__try__exceptfinally就可以向代码添加SEH。使用汇编的话,则需要加入如下汇编指令:

1
2
3
PUSH @MyHandler           ; 第2个参数
PUSH DWORD PTR FS:[0] ; 第1个参数
MOV DWORD PTR FS:[0], ESP ; 将SEH头改变

 前面的seh.exe就是通过修改汇编指令来做的,如下图红框所示:

image-20230503194217723

 安装好后,发现SEH代码存放在0040105A中,代码如下所示:

image-20230503202015261

image-20230503203450569

 上述代码可以检测是否为调试器。

1
2
3
4
5
6
7
8
9
MOV ESI, DWORD PTR SS:[ARG3]  // 将pContext放入到ESI中
MOV EAX,DWORD PTR FS:[30] // 将PEB地址放到EAX中
CMP BYTE PTR DS:[EAX+2], 1 // 比较BeingDebugged与1
JNE SHORT 00401076 // 若BeingDebugged!=1, 则跳转到00401076
MOV DWORD PTR DS:[ESI+B8],00401023 // 程序处于调试状态,pContext+B8=EIP的位置,说明将EIP改变为00401023,而00401023输出Debugger detected
JMP SHORT 00401080 // 跳转到XOR EAX, EAX
MOV DWORD PTR DS:[ESI+B8],00401039 // 程序未处于调试状态,则将EIP改为00401039
XOR EAX,EAX // 返回值EAX=0,代表ExceptionContinueExecution=0,继续执行异常代码
RETN

 在0040104D代码中,为POP DWORD PTR FS:[0](可以分解为MOV EAX, [ESP]; ADD ESP, 4;)。之后为ADD ESP,4,使用这两条语句,就能将刚才定义的SEH函数删除,并将FS:[0]定为SEH的第二个函数。

 SEH大量应用于压缩器、保护器、恶意程序(Malware),用来反调试。

什么是枚举类型?

 枚举类型通过列出枚举器(Enumerator)的方式定义,每个枚举器代表一个具体的取值。例如:

1
2
3
4
5
enum Color {
RED,
GREEN,
BLUE
};

 这定义了一个名为Color的枚举类型,它包含三个枚举器:REDGREENBLUE。这些枚举器的取值分别为0、1和2(默认情况下,第一个枚举器的取值为0,后续枚举器的取值依次递增)。使用上面的枚举类型,可以声明一个变量表示颜色:

1
Color myColor = GREEN;

 这比使用数字0或1更容易理解,也更容易修改和维护。

FPU(Floating Point Unit,浮点运算单元)是专门用于浮点数运算的处理器,它有一套专用指令,与普通x86指令的形态结构不同。

0x14 IA-32(x86)指令

 指令格式如下:

image-20230504091119098

指令解析练习

 了解即可。P524之前的内容。

为什么要掌握IA-32解析?

 检测变形病毒(Polymorphic Virus)时必须探测出Polymorphic引擎中产生的指令类型,这时就需要分析人员具有丰富的指令知识。掌握IA-32指令相关知识对于编写打补丁代码、分析漏洞Shell代码都非常有帮助。

留言

© 2024 wd-z711

⬆︎TOP