使用 WinDBG 调试 在这小节,我们将学习调试程序的技能,这对于恶意软件开发领域有这些帮助:理解当下的恶意软件的技术、功能、原理,分析恶意软件并且改进,开发漏洞利用以及 Shellcode,理解安全产品的原理以规避检测等。 我们将主要使用 WinDBG 作为动态调试工具,WinDBG 可以在微软商店中获取: 调试器是插入在目标应用程序和 CPU 之间的计算机程序,充当类似代理的角色。使用调试器使我们能够查看应用程序的内存和执行流程并与之交互。接下来的课程内容中,我们将与用户模式交互。 CPU以二进制级别处理代码,这对人类来说很难阅读和理解,而汇编语言引入了二进制内容和编程语言之间的一对一映射。尽管汇编语言应该是人类可读的,但它仍然是一种低级语言,并且需要时间来掌握。 操作码是由 CPU 解释为特定指令的二进制序列,这在调试器中显示为十六进制值以及汇编语言的翻译。 自定义界面 为了能有一个舒适的调试环境,我们可以对 WinDBG 的界面进行自定义,这样当我们调试应用程序的时候,可以在用户界面中查看到所需的信息,例如内存、汇编代码、断点信息等。 在导航栏中选中 View,我们可以添加多个窗口,这里面对我们调试过程会十分有帮助的有 WinDBG 命令 (Command)、寄存器 (Registers)、内存 (Memory)、栈 (Stack)、汇编代码 (Disassembly)。 点中之后,对应的窗口会浮现出来,可以使其与 WinDBG 主窗口相互独立,也可以将其嵌入至 WinDBG 主窗口,我会更推荐将它们嵌入至主窗口。不过因为主窗口空间有限,如果添加的窗口数量过多,也会影响我们对信息的提取效率,诸如模块 (Module)、断点 (Breakpoints) 等窗口我们不一定要添加到主页面,而是通过 WinDBG 的命令来查看相关信息。 下图是个人偏好的一个界面布局,你们可能注意到我添加了 2 个内存窗口,因为我们还想查看 RSP 的状态。 当我们想要调试一个进程或者应用的时候,可以选择使用 WinDBG 启动目标程序,或者附加到正常运行的进程中。 附加上之后,进程会自动被设置一个软件断点,对应的汇编指令为 int 3。 我们可以执行 WinDBG 命令 g 来继续执行。 在进入对 WinDBG 基本命令的学习之前,我们还需要知道调试符号。符号 (Symbol) 文件允许 WinDbg 使用名称而不是地址来引用内部函数、结构体和全局变量。例如,我们想给 kernelbase.dll 中的 CreateProcessA 函数设置软件断点,我们不需要先得到载入的 kernelbase.dll 中的 CreateProcessA 函数的地址,使用名称即可,命令为 bp kernelbase!CreateProcessA。在后面,我们也可以用符号来查看一些结构体。 符号文件以 .pdb 为拓展名,当应用程序的 pdb 与 PE 文件在同一目录下,符号文件会被自动载入从而识别应用程序中的符号。 这样,我们可以使用 process_calc!Main 来定位到 process_calc.exe 程序中的 Main 函数。 伪寄存器 除了 rsp, rip 等处理器寄存器外,在 WinDBG,还支持伪寄存器。WinDbg 中的伪寄存器不是实际的处理器寄存器,而是调试器本身提供的结构,允许我们快速方便地访问某些常用信息,而无需手动计算地址或使用较长的命令序列。我们会经常用到的伪寄存器有 $peb、$teb、$ip ,分别代表当前进程的 PEB,当前线程的 TEB,当前的指令寄存器。 0:000> r $peb $peb=0000005c12c6f000 0:000> r $teb $teb=0000005c12c70000 0:000> r $ip $ip=00007ffb80a4cea4 基本命令 有了对 WinDBG 的初始了解以及配置了最适合自己的界面布局后,我们来学习 WinDBG 的基本常用命令。WinDBG 内置的命令数量十分庞大,并且考虑到该小节的内容是作为恶意软件开发的前置知识,因此我们不会过于深入。接下来,我们依次介绍以下这些常用命令。 反汇编 在 WinDBG 中,使用命令 u <内存地址> 来检视给定内存地址的汇编代码。例如,我们可以查看 user32.dll 中的 MessageBoxA 函数的汇编实现: 0:000> u user32!messageboxa USER32!MessageBoxA: 00007ffb`7e9a7a90 4883ec38 sub rsp,38h 00007ffb`7e9a7a94 4533db xor r11d,r11d 00007ffb`7e9a7a97 44391dcaf70300 cmp dword ptr [USER32!gfEMIEnable (00007ffb`7e9e7268)],r11d 00007ffb`7e9a7a9e 742e je USER32!MessageBoxA+0x3e (00007ffb`7e9a7ace) 00007ffb`7e9a7aa0 65488b042530000000 mov rax,qword ptr gs:[30h] 00007ffb`7e9a7aa9 4c8b5048 mov r10,qword ptr [rax+48h] 00007ffb`7e9a7aad 33c0 xor eax,eax 对于内存地址,可以是符号、内存地址、寄存器的形式,只要是合法的内存地址。可以通过 l* 来指定显示的行数。 0:000> u 00007ffb`7e9a7a90 l3 USER32!MessageBoxA: 00007ffb`7e9a7a90 4883ec38 sub rsp,38h 00007ffb`7e9a7a94 4533db xor r11d,r11d 00007ffb`7e9a7a97 44391dcaf70300 cmp dword ptr [USER32!gfEMIEnable (00007ffb`7e9e7268)],r11d 读取内存 我们可以使用命令 d* <内存地址> 来读取给定内存地址,星号可以是不同的数据类型,例如 byte,word,dword 等。我们可以在微软文档 https://learn.microsoft.com/en-us/windows-hardware/drivers/debugger/d--da--db--dc--dd--dd--df--dp--dq--du--dw--dw--dyb--dyd--display-memor 中了解更加详细的用法。 0:000> dd rip 00007ffb`80a4cea4 4800ebcc c338c483 cccccccc cccccccc 00007ffb`80a4ceb4 245c8948 74894810 57551824 8d485641 00007ffb`80a4cec4 ff0024ac 8148ffff 000200ec 058b4800 00007ffb`80a4ced4 000bf648 48c43348 00f08589 8b4c0000 00007ffb`80a4cee4 0bc32705 058d4800 0005afe0 8948ff33 00007ffb`80a4cef4 c7502444 16482444 48001800 7024448d 00007ffb`80a4cf04 24448948 f18b4868 602444c7 01000000 00007ffb`80a4cf14 0100be41 89660000 4d70247c 2b74c085 0:000> db rip 00007ffb`80a4cea4 cc eb 00 48 83 c4 38 c3-cc cc cc cc cc cc cc cc ...H..8......... 00007ffb`80a4ceb4 48 89 5c 24 10 48 89 74-24 18 55 57 41 56 48 8d H.\$.H.t$.UWAVH. 00007ffb`80a4cec4 ac 24 00 ff ff ff 48 81-ec 00 02 00 00 48 8b 05 .$....H......H.. 00007ffb`80a4ced4 48 f6 0b 00 48 33 c4 48-89 85 f0 00 00 00 4c 8b H...H3.H......L. 00007ffb`80a4cee4 05 27 c3 0b 00 48 8d 05-e0 af 05 00 33 ff 48 89 .'...H......3.H. 00007ffb`80a4cef4 44 24 50 c7 44 24 48 16-00 18 00 48 8d 44 24 70 D$P.D$H....H.D$p 00007ffb`80a4cf04 48 89 44 24 68 48 8b f1-c7 44 24 60 00 00 00 01 H.D$hH...D$`.... 00007ffb`80a4cf14 41 be 00 01 00 00 66 89-7c 24 70 4d 85 c0 74 2b A.....f.|$pM..t+ 0:000> dw rip 00007ffb`80a4cea4 ebcc 4800 c483 c338 cccc cccc cccc cccc 00007ffb`80a4ceb4 8948 245c 4810 7489 1824 5755 5641 8d48 00007ffb`80a4cec4 24ac ff00 ffff 8148 00ec 0002 4800 058b 00007ffb`80a4ced4 f648 000b 3348 48c4 8589 00f0 0000 8b4c 00007ffb`80a4cee4 2705 0bc3 4800 058d afe0 0005 ff33 8948 00007ffb`80a4cef4 2444 c750 2444 1648 1800 4800 448d 7024 00007ffb`80a4cf04 8948 2444 4868 f18b 44c7 6024 0000 0100 00007ffb`80a4cf14 be41 0100 0000 8966 247c 4d70 c085 2b74 读取结构体 我们可以使用 dt <结构体名称> 命令来显示结构体的结构,当然了,读取结构体需要符号文件的载入。在之前的小节,我们讲了 PEB 和 TEB,那么我们在 WinDBG 中查看一下 PEB 的结构吧。其中,使用 -r 选项可以递归地展示成员,因为结构体的成员也可能是结构体。 0:000> dt ntdll!_peb +0x000 InheritedAddressSpace : UChar +0x001 ReadImageFileExecOptions : UChar +0x002 BeingDebugged : UChar +0x003 BitField : UChar +0x003 ImageUsesLargePages : Pos 0, 1 Bit +0x003 IsProtectedProcess : Pos 1, 1 Bit +0x003 IsImageDynamicallyRelocated : Pos 2, 1 Bit +0x003 SkipPatchingUser32Forwarders : Pos 3, 1 Bit +0x003 IsPackagedProcess : Pos 4, 1 Bit +0x003 IsAppContainer : Pos 5, 1 Bit +0x003 IsProtectedProcessLight : Pos 6, 1 Bit +0x003 IsLongPathAwareProcess : Pos 7, 1 Bit +0x004 Padding0 : [4] UChar +0x008 Mutant : Ptr64 Void +0x010 ImageBaseAddress : Ptr64 Void +0x018 Ldr : Ptr64 _PEB_LDR_DATA +0x020 ProcessParameters : Ptr64 _RTL_USER_PROCESS_PARAMETERS ............ +0x7ac CloudFileDiagFlags : Uint4B +0x7b0 PlaceholderCompatibilityMode : Char +0x7b1 PlaceholderCompatibilityModeReserved : [7] Char +0x7b8 LeapSecondData : Ptr64 _LEAP_SECOND_DATA +0x7c0 LeapSecondFlags : Uint4B +0x7c0 SixtySecondEnabled : Pos 0, 1 Bit +0x7c0 Reserved : Pos 1, 31 Bits +0x7c4 NtGlobalFlag2 : Uint4B +0x7c8 ExtendedFeatureDisableMask : Uint8B 0:000> dt -r ntdll!_peb +0x000 InheritedAddressSpace : UChar +0x001 ReadImageFileExecOptions : UChar +0x002 BeingDebugged : UChar +0x003 BitField : UChar +0x003 ImageUsesLargePages : Pos 0, 1 Bit +0x003 IsProtectedProcess : Pos 1, 1 Bit +0x003 IsImageDynamicallyRelocated : Pos 2, 1 Bit +0x003 SkipPatchingUser32Forwarders : Pos 3, 1 Bit +0x003 IsPackagedProcess : Pos 4, 1 Bit +0x003 IsAppContainer : Pos 5, 1 Bit +0x003 IsProtectedProcessLight : Pos 6, 1 Bit +0x003 IsLongPathAwareProcess : Pos 7, 1 Bit +0x004 Padding0 : [4] UChar +0x008 Mutant : Ptr64 Void +0x010 ImageBaseAddress : Ptr64 Void +0x018 Ldr : Ptr64 _PEB_LDR_DATA +0x000 Length : Uint4B +0x004 Initialized : UChar +0x008 SsHandle : Ptr64 Void +0x010 InLoadOrderModuleList : _LIST_ENTRY +0x000 Flink : Ptr64 _LIST_ENTRY +0x008 Blink : Ptr64 _LIST_ENTRY +0x020 InMemoryOrderModuleList : _LIST_ENTRY +0x000 Flink : Ptr64 _LIST_ENTRY +0x008 Blink : Ptr64 _LIST_ENTRY +0x030 InInitializationOrderModuleList : _LIST_ENTRY +0x000 Flink : Ptr64 _LIST_ENTRY +0x008 Blink : Ptr64 _LIST_ENTRY +0x040 EntryInProgress : Ptr64 Void +0x048 ShutdownInProgress : UChar +0x050 ShutdownThreadId : Ptr64 Void ............ +0x34e OemCodePage : Uint2B +0x350 UseCaseMapping : Uint2B +0x352 UnusedNlsField : Uint2B +0x358 WerRegistrationData : Ptr64 Void +0x360 WerShipAssertPtr : Ptr64 Void +0x368 EcCodeBitMap : Ptr64 Void +0x370 pImageHeaderHash : Ptr64 Void +0x378 TracingFlags : Uint4B +0x378 HeapTracingEnabled : Pos 0, 1 Bit +0x378 CritSecTracingEnabled : Pos 1, 1 Bit +0x378 LibLoaderTracingEnabled : Pos 2, 1 Bit +0x378 SpareTracingBits : Pos 3, 29 Bits +0x37c Padding6 : [4] UChar +0x380 CsrServerReadOnlySharedMemoryBase : Uint8B +0x388 TppWorkerpListLock : Uint8B +0x390 TppWorkerpList : _LIST_ENTRY +0x000 Flink : Ptr64 _LIST_ENTRY +0x000 Flink : Ptr64 _LIST_ENTRY +0x008 Blink : Ptr64 _LIST_ENTRY +0x008 Blink : Ptr64 _LIST_ENTRY +0x000 Flink : Ptr64 _LIST_ENTRY +0x008 Blink : Ptr64 _LIST_ENTRY +0x3a0 WaitOnAddressHashTable : [128] Ptr64 Void +0x7a0 TelemetryCoverageHeader : Ptr64 Void +0x7a8 CloudFileFlags : Uint4B +0x7ac CloudFileDiagFlags : Uint4B +0x7b0 PlaceholderCompatibilityMode : Char +0x7b1 PlaceholderCompatibilityModeReserved : [7] Char +0x7b8 LeapSecondData : Ptr64 _LEAP_SECOND_DATA +0x000 Enabled : UChar +0x004 Count : Uint4B +0x008 Data : [1] _LARGE_INTEGER +0x000 LowPart : Uint4B +0x004 HighPart : Int4B +0x000 u : +0x000 QuadPart : Int8B +0x7c0 LeapSecondFlags : Uint4B +0x7c0 SixtySecondEnabled : Pos 0, 1 Bit +0x7c0 Reserved : Pos 1, 31 Bits +0x7c4 NtGlobalFlag2 : Uint4B +0x7c8 ExtendedFeatureDisableMask : Uint8B 我们还可以使用 WinDBG 检视给定内存区域的结构体数据、查看结构体中的成员、显示结构体的尺寸。 0:000> dt ntdll!_peb @$peb +0x000 InheritedAddressSpace : 0 '' +0x001 ReadImageFileExecOptions : 0 '' +0x002 BeingDebugged : 0x1 '' +0x003 BitField : 0x4 '' +0x003 ImageUsesLargePages : 0y0 +0x003 IsProtectedProcess : 0y0 +0x003 IsImageDynamicallyRelocated : 0y1 +0x003 SkipPatchingUser32Forwarders : 0y0 +0x003 IsPackagedProcess : 0y0 +0x003 IsAppContainer : 0y0 +0x003 IsProtectedProcessLight : 0y0 +0x003 IsLongPathAwareProcess : 0y0 +0x004 Padding0 : [4] "" +0x008 Mutant : 0xffffffff`ffffffff Void +0x010 ImageBaseAddress : 0x00007ff6`4ba90000 Void +0x018 Ldr : 0x00007ffb`80af4380 _PEB_LDR_DATA +0x020 ProcessParameters : 0x00000200`bdb86990 _RTL_USER_PROCESS_PARAMETERS ............ +0x7c4 NtGlobalFlag2 : 0 +0x7c8 ExtendedFeatureDisableMask : 0 0:000> dt ntdll!_peb @$peb ProcessParameters +0x020 ProcessParameters : 0x00000200`bdb86990 _RTL_USER_PROCESS_PARAMETERS 0:000> ?? sizeof(ntdll!_PEB) unsigned int64 0x7d0 修改和写入内存 我们可以使用命令 e* 对给定内存地址进行数据写入或修改,星号为数据的类型。 0:000> dd rsp l1 0000005c`12a9ef80 00000000 0:000> ed rsp 41414141 0:000> dd rsp l1 0000005c`12a9ef80 41414141 0:000> da rsp 0000005c`12a9ef80 "AAAA" 0:000> ea rsp "BBBB" 0:000> da rsp 0000005c`12a9ef80 "BBBB" 搜索内存空间 当我们想在内存空间里搜索特定的字节序列或者字符串,可以使用 s 命令。我们可以指定不同的数据类型、内存范围、要搜索的内容。例如,在 x64 下,在所有用户态内存空间中搜索字符串 "dl3r",命令语句如下: s -a 0 L?0x7fffffffffffffff "dl3r" 因为内存空间十分庞大,我们最好能在更精确的范围中进行搜索。 检视寄存器 我们可以使用命令 r 来查看所有寄存器,或者 r <寄存器> 查看单个寄存器。伪寄存器也可以用 r 命令来查看。 2:008> r rax=0000000000000000 rbx=00007ffb80abcc50 rcx=00007ffb80a0f0f4 rdx=0000000000000000 rsi=000000b6da72e000 rdi=00007ffb80aa6c08 rip=00007ffb80a4cea4 rsp=000000b6da4ceda0 rbp=0000000000000000 r8=000000b6da4ced98 r9=0000000000000000 r10=0000000000000000 r11=0000000000000246 r12=0000000000000040 r13=0000000000000001 r14=000001bf3e860000 r15=0000000000000000 iopl=0 nv up ei pl zr na po nc cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246 ntdll!LdrpDoDebuggerBreak+0x30: 00007ffb`80a4cea4 cc int 3 2:008> r rip rip=00007ffb80a4cea4 2:008> r $peb $peb=000000b6da72e000 计算器 WinDBG 的命令 ? <表达式> 可用作计算器,实现算数运算、进制转换等任务。 命令 .formats <数据> 可将给定数值转换为不同数据类型: 列举模块 我们可以使用命令 lm 来列举当前进程载入的所有模块,或者检视指定的模块。使用命令 x 来查看模块中的符号 2:008> lm start end module name 00007ff6`4ba90000 00007ff6`4baea000 notepad (pdb symbols) C:\ProgramData\Dbg\sym\notepad.pdb\C694C0AA7279CC672966901283BF50541\notepad.pdb 00007ffb`6c470000 00007ffb`6c6fe000 COMCTL32 (deferred) 00007ffb`7dd70000 00007ffb`7de0a000 msvcp_win (deferred) 00007ffb`7de10000 00007ffb`7df21000 ucrtbase (deferred) 00007ffb`7dff0000 00007ffb`7e016000 win32u (deferred) 00007ffb`7e020000 00007ffb`7e139000 gdi32full (deferred) 00007ffb`7e3a0000 00007ffb`7e743000 KERNELBASE (pdb symbols) C:\ProgramData\Dbg\sym\kernelbase.pdb\69B5D8B50C7EA66AAE7105C36531C37F1\kernelbase.pdb 00007ffb`7e930000 00007ffb`7eadb000 USER32 (deferred) 00007ffb`7eae0000 00007ffb`7eb87000 msvcrt (deferred) 00007ffb`7fbb0000 00007ffb`7fc54000 sechost (deferred) 00007ffb`7fd40000 00007ffb`7fd69000 GDI32 (deferred) 00007ffb`7fd70000 00007ffb`7fe32000 KERNEL32 (deferred) 00007ffb`7ff70000 00007ffb`80061000 shcore (deferred) 00007ffb`80180000 00007ffb`8022e000 advapi32 (deferred) 00007ffb`80230000 00007ffb`80347000 RPCRT4 (deferred) 00007ffb`80510000 00007ffb`80899000 combase (deferred) 00007ffb`80970000 00007ffb`80b84000 ntdll (pdb symbols) C:\ProgramData\Dbg\sym\ntdll.pdb\ACBBF75A6C22094871DD84500F4F58F91\ntdll.pdb 2:008> lm m kernel* Browse full module list start end module name 00007ffb`7e3a0000 00007ffb`7e743000 KERNELBASE (pdb symbols) C:\ProgramData\Dbg\sym\kernelbase.pdb\69B5D8B50C7EA66AAE7105C36531C37F1\kernelbase.pdb 00007ffb`7fd70000 00007ffb`7fe32000 KERNEL32 (deferred) 2:008> x kernelbase!CreateProcess* 00007ffb`7e465717 KERNELBASE!CreateProcessInternalA$filt$1 (void) 00007ffb`7e4656f7 KERNELBASE!CreateProcessInternalA$filt$0 (void) 00007ffb`7e426e2c KERNELBASE!CreateProcessExtensions::ReleaseAppXContext (void) 00007ffb`7e3e0230 KERNELBASE!CreateProcessInternalW (void) 00007ffb`7e425840 KERNELBASE!CreateProcessInternalA (void) 00007ffb`7e462f6b KERNELBASE!CreateProcessInternalW$filt$1 (void) 00007ffb`7e4ffb20 KERNELBASE!CreateProcessWithTokenW (void) 00007ffb`7e462f91 KERNELBASE!CreateProcessInternalW$fin$2 (void) 00007ffb`7e4ffa90 KERNELBASE!CreateProcessAsUserA (void) 00007ffb`7e462f05 KERNELBASE!CreateProcessInternalW$fin$0 (void) 00007ffb`7e3e98e4 KERNELBASE!CreateProcessExtensions::PreCreationExtension (void) 00007ffb`7e4ffac0 KERNELBASE!CreateProcessAsUserW (void) 00007ffb`7e4ffaf0 KERNELBASE!CreateProcessWithLogonW (void) 00007ffb`7e465737 KERNELBASE!CreateProcessInternalA$fin$2 (void) 00007ffb`7e4d9880 KERNELBASE!CreateProcessExtensions::ErrorContext::LogError (public: void __cdecl CreateProcessExtensions::ErrorContext::LogError(long,struct Common::COMMON_STRING *)) 00007ffb`7e4d9424 KERNELBASE!CreateProcessExtensions::CreateSharedLocalFolder (public: static long __cdecl CreateProcessExtensions::CreateSharedLocalFolder(struct Common::COMMON_STRING const &)) 00007ffb`7e4f7ad0 KERNELBASE!CreateProcessAsUserA (CreateProcessAsUserA) 00007ffb`7e4257c0 KERNELBASE!CreateProcessA (CreateProcessA) 00007ffb`7e4231b0 KERNELBASE!CreateProcessAsUserW (CreateProcessAsUserW) 00007ffb`7e41fe80 KERNELBASE!CreateProcessW (CreateProcessW) 调用栈 有以下这么一个程序,执行了弹出 calc.exe 的 Shellcode。在 main 函数中,调用了 calc_exec 函数,而在 calc_exec 函数中,调用了数个 Windows API 来实现 Shellcode 执行。 #include #include #include unsigned char shellcode[] = { 0xfc, 0x48, 0x83, 0xe4, 0xf0, 0xe8, 0xc0, 0x00, 0x00, 0x00, 0x41, 0x51, 0x41, 0x50, 0x52, 0x51, 0x56, 0x48, 0x31, 0xd2, 0x65, 0x48, 0x8b, 0x52, 0x60, 0x48, 0x8b, 0x52, 0x18, 0x48, 0x8b, 0x52, 0x20, 0x48, 0x8b, 0x72, 0x50, 0x48, 0x0f, 0xb7, 0x4a, 0x4a, 0x4d, 0x31, 0xc9, 0x48, 0x31, 0xc0, 0xac, 0x3c, 0x61, 0x7c, 0x02, 0x2c, 0x20, 0x41, 0xc1, 0xc9, 0x0d, 0x41, 0x01, 0xc1, 0xe2, 0xed, 0x52, 0x41, 0x51, 0x48, 0x8b, 0x52, 0x20, 0x8b, 0x42, 0x3c, 0x48, 0x01, 0xd0, 0x8b, 0x80, 0x88, 0x00, 0x00, 0x00, 0x48, 0x85, 0xc0, 0x74, 0x67, 0x48, 0x01, 0xd0, 0x50, 0x8b, 0x48, 0x18, 0x44, 0x8b, 0x40, 0x20, 0x49, 0x01, 0xd0, 0xe3, 0x56, 0x48, 0xff, 0xc9, 0x41, 0x8b, 0x34, 0x88, 0x48, 0x01, 0xd6, 0x4d, 0x31, 0xc9, 0x48, 0x31, 0xc0, 0xac, 0x41, 0xc1, 0xc9, 0x0d, 0x41, 0x01, 0xc1, 0x38, 0xe0, 0x75, 0xf1, 0x4c, 0x03, 0x4c, 0x24, 0x08, 0x45, 0x39, 0xd1, 0x75, 0xd8, 0x58, 0x44, 0x8b, 0x40, 0x24, 0x49, 0x01, 0xd0, 0x66, 0x41, 0x8b, 0x0c, 0x48, 0x44, 0x8b, 0x40, 0x1c, 0x49, 0x01, 0xd0, 0x41, 0x8b, 0x04, 0x88, 0x48, 0x01, 0xd0, 0x41, 0x58, 0x41, 0x58, 0x5e, 0x59, 0x5a, 0x41, 0x58, 0x41, 0x59, 0x41, 0x5a, 0x48, 0x83, 0xec, 0x20, 0x41, 0x52, 0xff, 0xe0, 0x58, 0x41, 0x59, 0x5a, 0x48, 0x8b, 0x12, 0xe9, 0x57, 0xff, 0xff, 0xff, 0x5d, 0x48, 0xba, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x48, 0x8d, 0x8d, 0x01, 0x01, 0x00, 0x00, 0x41, 0xba, 0x31, 0x8b, 0x6f, 0x87, 0xff, 0xd5, 0xbb, 0xf0, 0xb5, 0xa2, 0x56, 0x41, 0xba, 0xa6, 0x95, 0xbd, 0x9d, 0xff, 0xd5, 0x48, 0x83, 0xc4, 0x28, 0x3c, 0x06, 0x7c, 0x0a, 0x80, 0xfb, 0xe0, 0x75, 0x05, 0xbb, 0x47, 0x13, 0x72, 0x6f, 0x6a, 0x00, 0x59, 0x41, 0x89, 0xda, 0xff, 0xd5, 0x63, 0x61, 0x6c, 0x63, 0x2e, 0x65, 0x78, 0x65, 0x00 }; void calc_exec() { int length = sizeof(shellcode); void* exec = VirtualAlloc(0, length, MEM_COMMIT, PAGE_EXECUTE_READWRITE); RtlMoveMemory(exec, shellcode, length); HANDLE th = CreateThread(0, 0, (LPTHREAD_START_ROUTINE)exec, 0, 0, 0); WaitForSingleObject(th, 0xFFFFFFFF); } int main() { calc_exec(); return 0; } 在程序执行过程中,如果有地方报错了,我们可以根据函数的调用次序来追踪问题。例如,当程序执行到 WaitForSingleObject 处的时候,使用 WinDBG 命令 k 或者查看 WinDBG 栈窗口,我们可以清晰地看到调用次序: 对于调用栈的检视也是检测恶意软件行为的方式之一,尤其是用于检测 syscall 的调用。因为我们之前说过,用户态应用程序很少会直接使用 syscall,一般是 WinAPI -> NTAPI -> 内核。恶意软件开发者会通过 syscall 的调用绕过安全产品的检视,但如果检视调用栈,会发现端倪:NTAPI 的前一链并非对应的 WINAPI。 TEB 与 PEB 查看当前线程的 TEB/PEB,既可以执行命令 !teb/!peb,也可以使用 dt 命令查看。 1:001> !teb TEB at 000000d59498e000 ExceptionList: 0000000000000000 StackBase: 000000d594e00000 StackLimit: 000000d594dfc000 SubSystemTib: 0000000000000000 FiberData: 0000000000001e00 ArbitraryUserPointer: 0000000000000000 Self: 000000d59498e000 EnvironmentPointer: 0000000000000000 ClientId: 0000000000007ad4 . 000000000000af5c RpcHandle: 0000000000000000 Tls Storage: 0000022969a05290 PEB Address: 000000d59498d000 LastErrorValue: 0 LastStatusValue: c000000d Count Owned Locks: 0 HardErrorMode: 0 1:001> dt ntdll!_TEB @$teb +0x000 NtTib : _NT_TIB +0x038 EnvironmentPointer : (null) +0x040 ClientId : _CLIENT_ID +0x050 ActiveRpcHandle : (null) +0x058 ThreadLocalStoragePointer : 0x00000229`69a05290 Void +0x060 ProcessEnvironmentBlock : 0x000000d5`9498d000 _PEB ............ +0x1838 LastSleepCounter : 0 +0x1840 SpinCallCount : 0 +0x1844 Padding8 : [4] "" +0x1848 ExtendedFeatureDisableMask : 0 使用 !teb 命令,我们可以更加容易看到当前线程的栈的区间。 内存状态 命令 !address <内存地址> 和 !vprot <内存地址> 可以显示有关给定内存地址的相关信息。!address 命令为我们提供有关特定内存地址或范围的详细信息,包括内存区域的类型 (堆、堆栈、映像等)、区域的大小、内存权限 (读、写、执行权限) 以及内存的状态 (提交、保留、空闲)。 该命令显示的信息更加全面。 如果我们想快速地查看特定内存的权限和状态,!vprot 命令则更加直接和方便。 0:004> !address kernelbase!createprocessa Usage: Image Base Address: 00007ffb`7e3a1000 End Address: 00007ffb`7e530000 Region Size: 00000000`0018f000 ( 1.559 MB) State: 00001000 MEM_COMMIT Protect: 00000020 PAGE_EXECUTE_READ Type: 01000000 MEM_IMAGE Allocation Base: 00007ffb`7e3a0000 Allocation Protect: 00000080 PAGE_EXECUTE_WRITECOPY Image Path: C:\Windows\System32\KERNELBASE.dll Module Name: KERNELBASE Loaded Image Name: C:\Windows\System32\KERNELBASE.dll Mapped Image Name: More info: lmv m KERNELBASE More info: !lmi KERNELBASE More info: ln 0x7ffb7e4257c0 More info: !dh 0x7ffb7e3a0000 Content source: 1 (target), length: 10a840 0:004> !vprot kernelbase!createprocessa BaseAddress: 00007ffb7e425000 AllocationBase: 00007ffb7e3a0000 AllocationProtect: 00000080 PAGE_EXECUTE_WRITECOPY RegionSize: 000000000010b000 State: 00001000 MEM_COMMIT Protect: 00000020 PAGE_EXECUTE_READ Type: 01000000 MEM_IMAGE 软件断点 断点用于暂停程序的执行,当在特定地址设置软件断点时,调试器会用 int 3 指令替换该地址处的指令,该指令在执行时会导致执行暂停。调试器还记录它替换的指令,并在到达断点时恢复原始指令。在 WinDBG 中,我们可以使用命令 bp <地址> 来在特定内存地址处设置软件断点,当到达断点处,程序会被暂停,我们从而可以检视这个时刻的程序运行状态。 此外,命令 bl 用于罗列所有断点,bc 用于清除断点,bd 用于暂时禁用断点,而 be 用于重新启用断点。 WinDBG 也提供了查看断点的窗口: 不过,有时候应用程序不会在运行时就自动载入所有的模块,而是在后续运行中载入,考虑如下代码: #include #include typedef void (*calc_export)(); void run() { HMODULE hModule = LoadLibraryA("D:\\tooling\\dllcpp\\x64\\Release\\dllcpp.dll"); calc_export calc_ptr = (calc_export)GetProcAddress(hModule, "calc_export"); calc_ptr(); } int main() { run(); } 因此,如果我们试图给 dllcpp!calc_export 设置断点,我们会被告知无法解析符号。对此,我们可以改用 bu 命令,表示给尚未解析的符号设置断点。 0:000> lm start end module name 00007ff6`68720000 00007ff6`68727000 dll_load C (private pdb symbols) C:\ProgramData\Dbg\sym\dll_load.pdb\105D812B10354E099F90E97E006DF74A9\dll_load.pdb 00007ffb`63eb0000 00007ffb`63ecb000 VCRUNTIME140 (deferred) 00007ffb`7de10000 00007ffb`7df21000 ucrtbase (deferred) 00007ffb`7e3a0000 00007ffb`7e743000 KERNELBASE (deferred) 00007ffb`7fd70000 00007ffb`7fe32000 KERNEL32 (deferred) 00007ffb`80970000 00007ffb`80b84000 ntdll (pdb symbols) C:\ProgramData\Dbg\sym\ntdll.pdb\ACBBF75A6C22094871DD84500F4F58F91\ntdll.pdb 0:000> bp dllcpp!calc_export Bp expression 'dllcpp!calc_export' could not be resolved, adding deferred bp 0:000> bc * 0:000> bu dllcpp!calc_export 0:000> g *** WARNING: Unable to verify checksum for D:\tooling\dllcpp\x64\Release\dllcpp.dll ModLoad: 00007ffb`13450000 00007ffb`13457000 D:\tooling\dllcpp\x64\Release\dllcpp.dll Breakpoint 0 hit dllcpp!calc_export: 00007ffb`13451000 4053 push rbx 单步执行 在调试过程中,我们除了使用 g 来继续执行外,还可以单次执行下一条执行。那么,如果当前落入到调用函数的这条指令,下一步究竟是执行调用函数的指令的下一条指令,还是函数内的第一条指令呢?这就是命令 p 和命令 t 的区别。 编译如下代码,并用 WinDBG 执行。 #include #include void fun(int num) { printf("In function fun\n"); if (num == 2) { printf("num = 2!\n"); } } int main() { int a = 1; int num = 2; if (a < 2) { fun(num); } } 在编译时,我们需要禁用优化,因为我们使用了一些常数,编译器会简化程序流程以提升性能。 反汇编后的主函数如下所示,我们单次执行到 call step!fun 指令。 使用命令 p,会执行调用函数的指令的下一条指令 xor eax, eax。 而如果使用命令 t,会进入函数 fun 并且到达函数内的第 1 条指令 mov  [rsp+8], ecx。 当下一条指令并不是调用函数的指令,那么 p 与 t 效果相同。 执行至 我们已经知道了 p 的作用是执行下一条指令,实际上我们还可以指定调试器运行至下一个分支,或下一个返回指令。 使用 ph 指令,可以执行至下一个分支,如下图所示,执行到了 jge step!main+0x24 指令处。 而 pt 指令,可以执行至下一个返回指令,即 ret 指令处。 解析 PE 我们还可以使用 WinDBG 在内存中解析 PE 文件。PE 文件被映射到内存中以被执行,这是一个将文件的特定元素逐字节复制到内存中的过程,PE 格式包含了加载程序用来完成此操作的信息。 例如,可选头中的 ImageBase 包含映射在内存中的起始地址,SectionAlignment 包含内存中各节的对齐系数,ImageSize 包含整个映像所需的内存量等。可选头还包含数据目录,有着诸多重要数据结构的 RVA, 使得它们可以在内存中被快速定位到。同样,节头也包含重要信息,例如 VirtualSize 包含内存中该节的大小,VirtualAddress 表示其 RVA。 PE 格式的数据结构在磁盘上与在内存中基本相同,但也存在一些例外以及造成的重要差异。造成这些差异的原因有几个,第 1 个是当文件在磁盘和内存中时不同的对其系数,这意味着相同的数据可能位于磁盘和内存中的不同偏移处。第 2 个原因是并非文件的所有元素都被映射到内存。例如,调试信息可能就不会被映射到内存。此外,个别字段由加载程序更新,例如 IAT 的条目。尽管一些元素在内存中的偏移量可能与在磁盘上的偏移量不完全相同,但这些元素的顺序是一致的。 PE 头 载入一我们在之前内容中编译的文件,查看导入的模块列表: 使用命令 dt ntdll!_IMAGE_DOS_HEADER <内存地址> 检视主程序的 DOS 头 0:000> dt ntdll!_IMAGE_DOS_HEADER 00007ff7`cc8b0000 +0x000 e_magic : 0x5a4d +0x002 e_cblp : 0x90 +0x004 e_cp : 3 +0x006 e_crlc : 0 +0x008 e_cparhdr : 4 +0x00a e_minalloc : 0 +0x00c e_maxalloc : 0xffff +0x00e e_ss : 0 +0x010 e_sp : 0xb8 +0x012 e_csum : 0 +0x014 e_ip : 0 +0x016 e_cs : 0 +0x018 e_lfarlc : 0x40 +0x01a e_ovno : 0 +0x01c e_res : [4] 0 +0x024 e_oemid : 0 +0x026 e_oeminfo : 0 +0x028 e_res2 : [10] 0 +0x03c e_lfanew : 0n240 0x3c 处的 e_lfanew 值为十进制的 240,即 0xf0。 接着,查看 NT 头:dt ntdll!_IMAGE_NT_HEADERS64 <内存地址> 0:000> dt ntdll!_IMAGE_NT_HEADERS64 00007ff7`cc8b0000+0xf0 +0x000 Signature : 0x4550 +0x004 FileHeader : _IMAGE_FILE_HEADER +0x018 OptionalHeader : _IMAGE_OPTIONAL_HEADER64 我们可以用这样的方法继续检视其他部分: 0:000> dt ntdll!_IMAGE_OPTIONAL_HEADER64 00007ff7`cc8b0000+0xf0+0x18 +0x000 Magic : 0x20b +0x002 MajorLinkerVersion : 0xe '' +0x003 MinorLinkerVersion : 0x24 '$' +0x004 SizeOfCode : 0xe00 +0x008 SizeOfInitializedData : 0x1e00 +0x00c SizeOfUninitializedData : 0 +0x010 AddressOfEntryPoint : 0x1300 +0x014 BaseOfCode : 0x1000 +0x018 ImageBase : 0x00007ff7`cc8b0000 +0x020 SectionAlignment : 0x1000 +0x024 FileAlignment : 0x200 +0x028 MajorOperatingSystemVersion : 6 +0x02a MinorOperatingSystemVersion : 0 +0x02c MajorImageVersion : 0 +0x02e MinorImageVersion : 0 +0x030 MajorSubsystemVersion : 6 +0x032 MinorSubsystemVersion : 0 +0x034 Win32VersionValue : 0 +0x038 SizeOfImage : 0x7000 +0x03c SizeOfHeaders : 0x400 +0x040 CheckSum : 0 +0x044 Subsystem : 3 +0x046 DllCharacteristics : 0x8160 +0x048 SizeOfStackReserve : 0x100000 +0x050 SizeOfStackCommit : 0x1000 +0x058 SizeOfHeapReserve : 0x100000 +0x060 SizeOfHeapCommit : 0x1000 +0x068 LoaderFlags : 0 +0x06c NumberOfRvaAndSizes : 0x10 +0x070 DataDirectory : [16] _IMAGE_DATA_DIRECTORY 但 WinDBG 的 !dh 命令更加方便,我们可以轻松地检视 PE 的元素,WinDBG 已经帮我们计算好了各种需要偏移值 例如,我们可以使用命令 !dh -f <模块基址> 来检视文件头和可选头的信息: 0:000> !dh -f 00007ff7`cc8b0000 File Type: EXECUTABLE IMAGE FILE HEADER VALUES 8664 machine (X64) 6 number of sections 64A36813 time date stamp Mon Jul 3 20:30:11 2023 0 file pointer to symbol table 0 number of symbols F0 size of optional header 22 characteristics Executable App can handle >2gb addresses OPTIONAL HEADER VALUES 20B magic # 14.36 linker version E00 size of code 1E00 size of initialized data 0 size of uninitialized data 1300 address of entry point 1000 base of code ----- new ----- 00007ff7cc8b0000 image base 1000 section alignment 200 file alignment 3 subsystem (Windows CUI) 6.00 operating system version 0.00 image version 6.00 subsystem version 7000 size of image 400 size of headers 0 checksum 0000000000100000 size of stack reserve 0000000000001000 size of stack commit 0000000000100000 size of heap reserve 0000000000001000 size of heap commit 8160 DLL characteristics High entropy VA supported Dynamic base NX compatible Terminal server aware 0 [ 0] address [size] of Export Directory 28BC [ A0] address [size] of Import Directory 5000 [ 1E0] address [size] of Resource Directory 4000 [ 168] address [size] of Exception Directory 0 [ 0] address [size] of Security Directory 6000 [ 30] address [size] of Base Relocation Directory 23B0 [ 70] address [size] of Debug Directory 0 [ 0] address [size] of Description Directory 0 [ 0] address [size] of Special Directory 0 [ 0] address [size] of Thread Storage Directory 2270 [ 140] address [size] of Load Configuration Directory 0 [ 0] address [size] of Bound Import Directory 2000 [ 1B0] address [size] of Import Address Table Directory 0 [ 0] address [size] of Delay Import Directory 0 [ 0] address [size] of COR20 Header Directory 0 [ 0] address [size] of Reserved Directory 与 PE Bear 中进行比对,我们发现数据是吻合的,但是 Image Base 并不相同,因为 PE Bear 中显示的是偏好基址,不一定是实际情况。 偏好基址的偏移应当是 NT 头偏移 + 可选头偏移 + ImageBase 偏移,即 0xf0 + 0x18 + 0x18,我们可以看到,该内存中的值确实是基址,然后模块的大小也确实是 0x7000 字节。 PE 节 接下来,我们来检视内存中的 PE 节,命令是 !dh -s <模块基址>。 0:000> !dh -s 00007ff7`cc8b0000 SECTION HEADER #1 .text name D6C virtual size 1000 virtual address E00 size of raw data 400 file pointer to raw data 0 file pointer to relocation table 0 file pointer to line numbers 0 number of relocations 0 number of line numbers 60000020 flags Code (no align specified) Execute Read SECTION HEADER #2 .rdata name F84 virtual size 2000 virtual address 1000 size of raw data 1200 file pointer to raw data 0 file pointer to relocation table 0 file pointer to line numbers 0 number of relocations 0 number of line numbers 40000040 flags Initialized Data (no align specified) Read Only Debug Directories(4) Type Size Address Pointer cv 51 24d4 16d4 Format: RSDS, guid, 16, D:\tooling\cpp_messagebox\x64\Release\cpp_messagebox.pdb ( 12) 14 2528 1728 ( 13) 284 253c 173c ( 14) 0 0 0 SECTION HEADER #3 .data name 758 virtual size 3000 virtual address 200 size of raw data 2200 file pointer to raw data 0 file pointer to relocation table 0 file pointer to line numbers 0 number of relocations 0 number of line numbers C0000040 flags Initialized Data (no align specified) Read Write SECTION HEADER #4 .pdata name 168 virtual size 4000 virtual address 200 size of raw data 2400 file pointer to raw data 0 file pointer to relocation table 0 file pointer to line numbers 0 number of relocations 0 number of line numbers 40000040 flags Initialized Data (no align specified) Read Only SECTION HEADER #5 .rsrc name 1E0 virtual size 5000 virtual address 200 size of raw data 2600 file pointer to raw data 0 file pointer to relocation table 0 file pointer to line numbers 0 number of relocations 0 number of line numbers 40000040 flags Initialized Data (no align specified) Read Only SECTION HEADER #6 .reloc name 30 virtual size 6000 virtual address 200 size of raw data 2800 file pointer to raw data 0 file pointer to relocation table 0 file pointer to line numbers 0 number of relocations 0 number of line numbers 42000040 flags Initialized Data Discardable (no align specified) Read Only 对照 PE Bear 中的数据,是吻合的。 在可选头中,我们可以知道 .text 节的 RVA,是 0x1000。接着用 WinDBG 查看距离模块基址 0x1000 字节处的内容,我们发现与 PE Bear 中文件 .text 节的数据吻合。 导入与导出 使用命令 !dh -i <模块基址> 查看当前进程主程序的导入信息: 0:000> !dh -i 00007ff7`cc8b0000 _IMAGE_IMPORT_DESCRIPTOR 00007ff7cc8b28bc KERNEL32.dll 00007FF7CC8B2000 Import Address Table 00007FF7CC8B2960 Import Name Table 0 time date stamp 0 Index of first forwarder reference 00007FFB7FD83EF0 5F1 VirtualAlloc 00007FFB7FD906D0 602 WaitForSingleObject 00007FFB7FD847B0 FB CreateThread 00007FFB7FD85840 4F1 RtlLookupFunctionEntry 00007FFB7FD83EA0 4F8 RtlVirtualUnwind 00007FFB7FDAC700 5D8 UnhandledExceptionFilter 00007FFB7FD88E30 597 SetUnhandledExceptionFilter 00007FFB7FD90460 22A GetCurrentProcess 00007FFB7FD89AC0 5B6 TerminateProcess 00007FFB7FD87240 39E IsProcessorFeaturePresent 00007FFB7FD869B0 28C GetModuleHandleW 00007FFB7FD88220 397 IsDebuggerPresent 00007FFB809E08C0 381 InitializeSListHead 00007FFB7FD81230 301 GetSystemTimeAsFileTime 00007FFB7FD72750 22F GetCurrentThreadId 00007FFB7FD90470 22B GetCurrentProcessId 00007FFB7FD80F80 464 QueryPerformanceCounter 00007FFB7FD90290 4E9 RtlCaptureContext ............ _IMAGE_IMPORT_DESCRIPTOR 00007ff7cc8b2934 api-ms-win-crt-heap-l1-1-0.dll 00007FF7CC8B20D0 Import Address Table 00007FF7CC8B2A30 Import Name Table 0 time date stamp 0 Index of first forwarder reference 00007FFB7DE39240 16 _set_new_mode 以导入的 kernel32.dll 模块为例,使用命令 dds 查看该 DLL 的 IAT,发现与 !dh 命令的输出吻合。 IAT 在磁盘与内存中的结构相同,我们可以得到 IAT 的偏移为 IAT 的虚拟内存地址 - (模块基址 + rdata节 RVA) = 00007ff7`dc7f2000 - (00007ff7`dc7f0000+ 2000) = 0。可能因为当前程序较为简单,因此 rdata 刚开始就是 IAT。 在 PE Bear 中查看 IAT 表的第 1 个条目,因为 kernel32 模块的函数都是通过名称导入的,我们得到的是 Hint/Name 表的 RVA,这里是 0x2b10。 因为 Hint/Name 表的 Name 成员在 0x2 偏移处,因此函数的名称位于 基址 + 2b10 +2 处。 接下来,我们来查看载入内存的 PE 文件的导出数据,以 kernel32 为例。 0:000> lm m kernel32 Browse full module list start end module name 00007ffb`7fd70000 00007ffb`7fe32000 KERNEL32 (pdb symbols) C:\ProgramData\Dbg\sym\kernel32.pdb\7FB3DEE4119B1AEEB148EB77866A99371\kernel32.pdb 0:000> !dh -e 00007ffb`7fd70000 _IMAGE_EXPORT_DIRECTORY 00007ffb7fe0d560 (size: 0000e8f4) Name: KERNEL32.dll Characteristics: 00000000 Ordinal base: 1. Number of Functions: 1671. Number of names: 1671. EAT: 00007ffb7fe0d588. ordinal hint target name 1 0 AcquireSRWLockExclusive (forwarded to NTDLL.RtlAcquireSRWLockExclusive) 2 1 AcquireSRWLockShared (forwarded to NTDLL.RtlAcquireSRWLockShared) 3 2 00007FFB7FD88D90 ActivateActCtx 4 3 00007FFB7FD84A50 ActivateActCtxWorker 5 4 00007FFB7FD91580 ActivatePackageVirtualizationContext 6 5 00007FFB7FDCA670 AddAtomA 7 6 00007FFB7FD745E0 AddAtomW 8 7 00007FFB7FD91290 AddConsoleAliasA 9 8 00007FFB7FD912A0 AddConsoleAliasW 10 9 AddDllDirectory (forwarded to api-ms-win-core-libraryloader-l1-1-0.AddDllDirectory) 11 10 00007FFB7FDAC900 AddIntegrityLabelToBoundaryDescriptor 12 11 00007FFB7FDCA790 AddLocalAlternateComputerNameA 13 12 00007FFB7FDCA7F0 AddLocalAlternateComputerNameW 14 13 00007FFB7FDAA860 AddRefActCtx 15 14 00007FFB7FD87260 AddRefActCtxWorker 16 15 00007FFB7FDAA880 AddResourceAttributeAce 17 16 00007FFB7FD8F890 AddSIDToBoundaryDescriptor 18 17 00007FFB7FDAA8A0 AddScopedPolicyIDAce 19 18 00007FFB7FDA80D0 AddSecureMemoryCacheCallback 20 19 AddVectoredContinueHandler (forwarded to NTDLL.RtlAddVectoredContinueHandler) 21 20 AddVectoredExceptionHandler (forwarded to NTDLL.RtlAddVectoredExceptionHandler) ............ 1667 1666 00007FFB7FDA6970 uaw_wcschr 1668 1667 00007FFB7FDA69A0 uaw_wcscpy 1669 1668 00007FFB7FDA69E0 uaw_wcsicmp 1670 1669 00007FFB7FDA6A00 uaw_wcslen 1671 1670 00007FFB7FDA6A30 uaw_wcsrchr 根据之前的结论,我们定位到导出目录的 RVA,是 0x9d560。 接着,我们得到了模块名称的 RVA 进而得到模块名称字符串。 函数的数量,以及函数名称数量都为 687: 得到 EAT 的 RVA,访问 EAT,并得到第 1 个函数的 RVA,依旧与 PE Bear 中显示的相同。 得到 ENPT 的 RVA,并访问 ENPT,得到第 1 个函数名称的 RVA,进而得到该函数名称的字符串: 最后,得到序数表的 RVA,并访问序数表: