调试与逆向
在这小节,我们将学习简单的调试以及逆向技能,这对于恶意软件开发领域有这些帮助:理解当下的恶意软件的技术、功能、原理,分析恶意软件并且改进,开发漏洞利用以及 Shellcode,理解安全产品的原理以规避检测等。
我们将主要使用 WinDBG 作为动态调试工具,IDA 作为逆向工具。两者相辅相成,动态与静态相结合。WinDBG 可以在微软商店中获取:
IDA 可以从 https://hex-rays.com/ida-free/ 下载,拥有专业版自然更好。
WinDBG
调试器是插入在目标应用程序和 CPU 之间的计算机程序,充当类似代理的角色。使用调试器使我们能够查看应用程序的内存和执行流程并与之交互。接下来的课程内容中,我们将与用户模式交互。
CPU以二进制级别处理代码,这对人类来说很难阅读和理解,而汇编语言引入了二进制内容和编程语言之间的一对一映射。尽管汇编语言应该是人类可读的,但它仍然是一种低级语言,并且需要时间来掌握。 操作码是由 CPU 解释为特定指令的二进制序列,这在调试器中显示为十六进制值以及汇编语言的翻译。
接下来,利用 WinDbg,我们将学习如何使用断点来单步执行和控制应用程序的流程。
自定义界面
为了能有一个舒适的调试环境,我们可以对 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 : <unnamed-tag>
+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 <windows.h>
#include <stdlib.h>
#include <iostream>
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 <iostream>
#include <windows.h>
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 <iostream>
#include <windows.h>
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 <内存地址> 检视主程序的 NTDOS 头
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,240,即 0xf0。