-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathsearch.xml
111 lines (51 loc) · 432 KB
/
search.xml
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
<?xml version="1.0" encoding="utf-8"?>
<search>
<entry>
<title>演示版保护技术</title>
<link href="/2024/07/26/%E6%BC%94%E7%A4%BA%E7%89%88%E4%BF%9D%E6%8A%A4%E6%8A%80%E6%9C%AF/"/>
<url>/2024/07/26/%E6%BC%94%E7%A4%BA%E7%89%88%E4%BF%9D%E6%8A%A4%E6%8A%80%E6%9C%AF/</url>
<content type="html"><![CDATA[<h1 align = "center">演示版保护技术</h1><p>本文将介绍一些常用的软件保护技术,对其优缺点进行分析,并给出软件保护的一般性建议 。</p><h3 id="序列号保护"><a href="#序列号保护" class="headerlink" title="序列号保护"></a>序列号保护</h3><p>首先来看看常见的序列号(又称注册码)保护的工作原理。从网上下载的共享软件(Shareware)一般都有使用时间或功能上的限制,如果超过了共享软件的试用期,就必须到这个软件的公司去注册方能继续使用。注册过程一般是用户把自己的信息(例如用户名、电子邮件地址、机器特征码等)告诉软件公司,软件公司根据用户的信息,利用预先编写的一个用于计算注册码的程序(称为注册机,KeyGen)算出一个序列号,并以电子邮件等形式将其发给用户。用户得到序列号后,在软件中输入注册信息和序列号。当注册信息验证通过后,软件就会取消各种限制,例如时间限制、功能限制等,从而成为完全正式版本。软件每次启动时,会从磁盘文件或系统注册表中读取注册信息并对其进行检查。如果注册信息正确,则以完全正式版的模式运行,否则将作为有功能限制或时间限制的版本来运行。注册用户可以根据所拥有的注册信息得到相应的售后服务。当软件推出新版本后,注册用户还可以向软件作者提供自己的注册信息,以获得版本升级服务。这种保护实现起来比较简单,不需要额外的成本,用户购买也非常方便。网上大部分的软件都是以这种方式实现保护的。</p><ol><li>序列号保护机制</li></ol><p>软件验证序列号,其实就是验证用户名和序列号之间的数学映射关系。因为这个映射关系是由软件的设计者制定的,所以各个软件生成序列号的算法是不同的。显然,映射关系越复杂,序列号就越不容易被破解。根据映射关系的不同,程序检查序列号有如下4种基本方法。</p><p>(1)将用户名等信息作为自变量,通过函数F变换之后得到注册码</p><p>将这个注册码和用户输入的注册码进行字符串比较或者数值比较,以确定用户是否为合法用户,公式如下。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">序列号 = F(用户名)</span><br></pre></td></tr></table></figure><p>因为负责验证注册码合法性的代码是在用户的机器上运行的,所以用户可以利用调试器等工具来分析程序验证注册码的过程。由于通过上述方法计算出来的序列号是以明文形式在内存中出现的,我们很容易就能在内存中找到它,从而获得注册码。这种方法在检查注册码合法性的同时,也在用户机器上再现了生成注册码的过程(即在用户机器上执行了函数F)。实际上,这是非常不安全的,因为不论函数F有多么复杂,解密者只需把函数F的实现代码从软件中提取出来,就可编制一个通用的计算注册码程序了。由此可见,这种检查注册码的方法是极其脆弱的。解密者也可通过修改比较指令的方法来通过注册码检查。</p><p>(2)通过注册码验证用户名的正确性</p><p>软件作者在给注册用户生成注册码的时候,使用的仍然是上面那种变换。这里要求F是个可逆变换。而软件在检查注册码的时候,是利用F的逆变换F^-1对用户输人的注册码进行变换的。如果变换的结果和用户名相同,则说明是正确的注册码,即</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">用户名 = F^-1(序列号)</span><br></pre></td></tr></table></figure><p>可以看到,用来生成注册码的函数F未直接出现在软件代码中,而且正确注册码的明文也未出现在内存中。所以,这种检查注册码的方法比第1种方法要安全一些。</p><p>破解这种注册码检查方法时,除了可以采用修改比较指令的办法,还有如下考虑:</p><p>因为F^-1的实现代码是包含在软件中的,所以可以通过F^-1找出其逆变换,即函数F,从而得到正确的注册码或者写出注册机。给定一个用户名,利用穷举法找到一个满足式的<code>用户名 = F^-1(序列号)</code>序列号。这只适用于穷举难度不大的函数。<br>给定一个序列号,利用式<code>用户名 = F^-1(序列号)</code>变换得出一个用户名(当然,这个用户名中一般包含不可显示字符),从而得到一个正确的用户名序列号对。</p><p>(3)通过对等函数检查注册码</p><p>如果输入的用户名和序列号满足式<code>用户名 = F^-1(序列号)</code>,则认为是正确的注册码。采用这种方法,同样可以实现在内存中不出现正确注册码的明文。如果F2是一个可逆函数,则本方法实际上是第2种方法的个推广,解密方法也类似。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">F1(用户名)=F2(序列号)</span><br></pre></td></tr></table></figure><p>上面3种检查注册码的方法采用的自变量都只有1个,自变量是用户名或注册码。</p><p>(4)同时将用户名和注册码作为自变量(即采用二元函数)</p><p>这种检查注册码的方法将采用如下判断规则:当对用户名和序列号进行变换时,如果得出的结果和某个特定的值相等,则认为是合法的用户名序列号对。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">特定值=F3(用户名,序列号)</span><br></pre></td></tr></table></figure><p>这个算法看上去相当不错,用户名与序列号之间的关系不再那么清晰了。但是,同时可能失去了用户名与序列号的一一对应关系,软件开发者很可能无法写出注册机。所以,必须维护用户名与序列号之间的唯一性。建一个数据库就可以了。当然,也可根据这一思路把用户名和序列号分为几个部分来构造多元的算法。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">特定值=Fn(用户名1,用户名2…序列号1,序列号2)</span><br></pre></td></tr></table></figure><p>以上所说的都是注册码与用户名相关的情祝。实际上,注册码也可以与用户名没有关系,这完全取决于软件作者的考虑。<br>可见,注册码的复杂性问题归根到底是一个数学问题。设计难以求逆的算法。当然,即使检查注册码的算法再复杂,如果可执行程序可以被任意修改,解密者还是可以通过修改比较跳转指令使程序成为注册版。所以,仅有好的算法是不够的,还要结合软件完整性检查等方法。</p><ol start="2"><li>如何攻击序列号保护机制</li></ol><p>若要找到序列号,或者修改判断序列号之后的跳转指令,最重要的是利用各种工具来定位判断序列号的代码段。</p><p>一种办法是通过跟踪输入注册码之后的判断找到注册码。通常用户会在一个编辑框中输入注册码,软件需要调用一些标准的API将用户输人的注册码字符串复制到自己的缓冲区中。利用调试器针对API设置断点的功能,就有可能找到判断注册码的地方。常用的API包括Get WindowTextA(W)、GetDIgItemTextA(W)、GetDlgItemInt,hmemcpy(仅Windows9x/Me)等。程序完成对注册码的判断流程后,一般会显示一个对话框,告诉用户注册码是否正确,这也是一个切人点。MessageBoxA(W)、MessageBoxExA(W)、Show Window、MessageBoxIndirectA(W)、CreateDialogParamA(W)、CreateDialog IndirectParamA(W)、DialogBoxParamA(W)、DialogBoxIndirectParamA(W)等API经常用于显示对话框。<br>另一种办法是跟踪程序启动时对注册码的判断过程(因为程序每次启动时,都需要将注册码读出并加以判断),从而决定是否以注册版的模式工作。根据序列号存放位置的不同,可以使用不同的API断点。如果序列号存放在注册表中,可以使用RegQuery ValueExA(W)函数;如果序列号存放在INI文件中,可以使用GetPrivateProfileStringA(M、GetPrivateProfileIntA(W)、GetProfileIntA(W)、GetProfileStringA(w)等函数;如果序列号存放在一般的文件中,可以使用CreateFileA(M)、_lopent()等函数。</p><p>(1)数据约束性的秘诀</p><p>这个概念是由+ORC提出的,只在用明文比较注册码的保护方式中使用。在大多数的序列号保护程序中,那个真正的、正确的注册码会于某个时刻出现在内存中。当然,它出现的位置是不定的,但多数情况下它会在一个范围之内,即存放用户输人序列号的内存地址±90h字节的地方。数据约束性(Data constraint)或者密码相邻性(Password proximity)的依据是:加密者在编程的时候需要留意保护功能是否“工作”,必须“看到”用户输入的数字,以及用户输入的转换结果和真正的密码之间的关系,这种联系必须经常地检查以调用这些代码。通常,它们会共同位于一个小的栈区域中(注意:参数或局部变量通常都是存储在栈中的,而软件作者一般会使用局部变量存放临时计算出来的注册码),使它们可以在同一个监视(Watch)窗口中出现。所以,在大多数情况下,真正的密码会在离保存用户输入密码不远的地方露出“马脚”。</p><p>运行TraceMe.exe程序,输入用户名“pediy”,序列号“12121212”。单击“Check”按钮,TraceMe将提示序列号错误。不要关闭此提示窗口。运行十六进制工具WinHex,.单击菜单项“Tools’”→“RAM Editor’”或按“Alt+F9”快捷键,打开内存编辑工具。单击“TraceMe”选项,打开Primary Memory内存并查看。按“Ctrl+F”快捷键打开查找对话框,输入假序列号“12121212”,在附近会发现另一个字符串“2470”,这就是真序列号,结果如图所示。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240722153835542.webp" alt="image-20240722153835542"></p><p>OllyDbg也可以实现这种查找功能。用OllyDbg加载TraceMe,输入假序列号,单击“Check’”按钮直到出现错误提示框。按“Alt+M”快捷键打开内存窗口,在上面一行按“Ctl+B”快捷键打开搜索框,搜索刚输人的序列号“12121212”,如图所示。OllyDbg的数据查找功能非常有用,可以在当前进程的整个内存映像里查找数据。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240722154434391.webp" alt="image-20240722154434391"></p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240722154417838.webp" alt="image-20240722154417838"></p><p>(2)hmemcpy函数</p><p>hmemepy函数(俗称“万能断点”)是Windows9x系统的内部函数,它的作用是将内存中的一块数据复制到另一个地方。由于Windows9x系统频繁使用该函数处理各种字符串,将该函数作为断点是非常实用的,该函数也成为Windows9x/Me平台最常用的断点。Windows NT/2000以上版本的系统中没有这个断点,因为其内核和Windows9x完全不同。</p><p>(3)利用消息断点</p><p>许多序列号保护软件都有一个按钮,当按下和释放鼠标时,将发送WM_LBUTTONDOWN(O201h)和WM LBUTTONUP(O202h)消息。因此,用这个消息下断点很容易就能找到按钮的事件代码。</p><p>(4)利用提示信息</p><p>目前大多数软件在设计时采用了人机对话的方式。所谓人机对话,即软件在执行一段程序之后会显示一串提示信息,以反映该段程序运行后的状态。例如,在TraceMe实例中输入假序列号,会显示“序列号错误,再来一次”。可以用OllyDbg、IDA等反汇编工具查找相应的字符串,定位到相关代码处。</p><p>用OllyDbg打开TraceMe..exe实例,单击右键,在弹出的快捷菜单中执行“Search for’”→“AIl referenced text strings”(“查找”→“所有参考文本字串”)命令,OllyDbg将列出程序中出现的字符串。但OllyDbg自带的这个功能对中文支持得不好,因此建议使用Ultra String Reference插件。安装插件后,在右键快捷菜单中执 行“Ultra String Reference ” → “Find ASCII’”命令,即可列出中文字符串,双击相关字符串即可定位到所需代码处。</p><ol start="3"><li>字符串比较形式</li></ol><p>在序列号分析过程中,字符串处理是一个重点,因此我们必须掌握一定的分析技能。加密者为了有效防止解密者修改跳转指令,往往会采取一些技巧,从而迂回比较字符串。</p><p>(1)寄存器直接比较</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">mov eax []</span><br><span class="line">mov ebx []</span><br><span class="line">cmp eax,ebx</span><br><span class="line">jz(jnz) xxxx</span><br></pre></td></tr></table></figure><p>(2)函数比较a</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">mov eax []</span><br><span class="line">mov ebx []</span><br><span class="line">call xxxxxxxx</span><br><span class="line">test eax,eax</span><br><span class="line">jz(jnz)</span><br></pre></td></tr></table></figure><p>在这种情况下,call指令一般是一个BOOL函数,其结果通过eax返回。在分析时,要关注该call指令返回时处理eax的代码。call指令中的代码如下。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">cmp xxx,xxx</span><br><span class="line">jz Lable</span><br><span class="line">xor eax,eax</span><br><span class="line">Lable: pop edi</span><br><span class="line">pop esi</span><br><span class="line">pop ebp</span><br><span class="line">ret</span><br></pre></td></tr></table></figure><p>(3)函数比较b</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">push xxxx</span><br><span class="line">push xxxx</span><br><span class="line">call xxxxxxxx</span><br><span class="line">test eax,eax</span><br><span class="line">jz(jnz)</span><br></pre></td></tr></table></figure><p>(4)串比较</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">lea edi []</span><br><span class="line">lea esi []</span><br><span class="line">repz cmosd ;比较字符串</span><br><span class="line">jz(jnz)</span><br></pre></td></tr></table></figure><ol start="4"><li>制作注册机</li></ol><p>软件开发结束后,软件作者很有必要先做攻击测试,找出弱点,避免犯一些低级错误。注册算法一般是一些极为简单的算法,基本上都是明码的,或者是明码相近的,例如查表、异或、换位、移位、累加和等,算法实现都比较容易。</p><p>(1)对明码比较软件的攻击</p><p>只要正确的序列号在内存中曾以明码形式出现(不管比较时是否使用明码),就都属于这一类。有些软件采取了一机一号的保护方式,即软件根据用户硬件等产生唯一的机器号,注册码与机器号对应,有效地防止了序列号被散发。如果是明码比较,攻击还是很容易的。之所以能轻易实现这一目的,就是因为利用了keymake软件,它能够拦载截程序指令并将出现的明码以某种方式直接显示出来。</p><p>实例TraceMe.exe的序列号是明码比较的,相关代码如下。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240722162049247.webp" alt="image-20240722162049247"></p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240722162127035.webp" alt="image-20240722162127035"></p><p>运行keymake后,单击菜单项“其他”→“内存注册机”,打开如图所示的界面。具体操作步骤如下。</p><ul><li>单击“浏览”按钮,打开目标程序TraceMe.exe。</li><li>设置寄存器为“内存方式”,本例是ebp,即序列号保存在ebp所指向的内存地址中。</li><li>中断地址列表。</li></ul><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240722165844708.webp" alt="image-20240722165844708"></p><p>按上述步骤完成设置,单击“生成”按钮,就可以生成一个注册机。使用时,该注册机和目标程序放在同一目录下。运行时,注册机装载目标程序,在指定地址处插入一个INT3指令,目标程序会在此中断,注册机将内存或寄存器的值读出,再恢复原程序指令。TraceMe被装载后,输人用户名,单击“Check”按钮,注册机将跳出一个窗口告知正确的序列号。</p><p>(2)非明码比较</p><p>实例Serial.exe通过对等函数检查序列号。如果输入的用户名和序列号满足</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">F1(用户名)=F2(序列号)</span><br></pre></td></tr></table></figure><p>则认为是正确的序列号。采用这种方法,可以使内存中不出现明码。</p><p>单击实例程序Serial.exe的菜单项“Help”→“Register’”,打开注册窗口。这个窗口是用DialogBoxParamA函数建立、用EndDialog函数关闭的。可以用GetDIgItemTextA、EndDialog等函数设断拦截。因为程序关闭对话框后才开始比较序列号,所以要在系统里运行一段时间才能回到Serial.exe的领空。也可以直接从提示信息切人,找到关键点。用OllyDbg装载Serial.exe后,输入姓名“pediy”和序列号“1234”,单击“0K”按钮,将跳出“Incorrect! Try Again”提示窗口。记下这串字符。单击右键,在弹出的快捷菜单中选择“Search for”→“All referenced text strings”选项,交叉参考字符串窗口,找到“Incorrect!Try Again’”并双击,就可以来到关键代码处。很明显,00401228h这段代码处理输入的字符串“pediy”。在此按“F2”键设断,重新运行。运行程序单步分析如下。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240722174619632.webp" alt="image-20240722174619632"></p><p>call 0040137E 函数内部如下</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240722174722709.webp" alt="image-20240722174722709"></p><p>上面的代码用于计算k1=F1(用户名)。用C语言来描述,代码如下。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">int</span> <span class="title function_">F1</span> <span class="params">(<span class="type">char</span> *name)</span>{</span><br><span class="line"> <span class="type">int</span> i,k1 = <span class="number">0</span>;</span><br><span class="line"> <span class="type">char</span> ch;</span><br><span class="line"> <span class="keyword">for</span>(i=<span class="number">0</span>;name[i]!=<span class="number">0</span>;i++){</span><br><span class="line"> ch=name[i];</span><br><span class="line"> <span class="keyword">if</span>(ch<<span class="string">'A'</span>) <span class="keyword">break</span>;</span><br><span class="line"> k1+=(ch><span class="string">'Z'</span>)?(ch<span class="number">-32</span>):ch;</span><br><span class="line"> }</span><br><span class="line"> k1 = k1^<span class="number">0x5678</span>;</span><br><span class="line"> <span class="keyword">return</span> k1;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>call 004013D8 函数内部如下</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240722181907411.webp" alt="image-20240722181907411"></p><p>上面的代码用于计算k2=F2(序列号)。用C语言来描述,代码如下。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">int</span> <span class="title function_">F2</span> <span class="params">(<span class="type">char</span> *code)</span>{</span><br><span class="line"> <span class="type">int</span> i,k2=<span class="number">0</span>;</span><br><span class="line"> <span class="keyword">for</span>(i=<span class="number">0</span>;code[i]!=<span class="number">0</span>;i++){</span><br><span class="line"> k2= k2*<span class="number">10</span>+code[i]<span class="number">-48</span>;</span><br><span class="line"> }</span><br><span class="line"> k2=k2^<span class="number">0x1234</span>;</span><br><span class="line"> <span class="keyword">return</span> k2;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>只要满足关系式k1=k2,注册就成功了。编写注册机时要对函数F1或F2进行逆变换,若F1和F2都不可逆,就只能使用穷举法了。如果要通过用户名算出正确的序列号,只要写出的逆函数k1=F2^-1(序列号)即可。求逆函数F2有多个解,比较复杂,但幸运的是,k1的结果是一个十六进制数,因此,可以将函数F2的功能看成将输入的十讲制数转换成十六讲制数。</p><p>注册机代码如下</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">int</span> <span class="title function_">keygen</span><span class="params">(<span class="type">char</span> *name)</span>{</span><br><span class="line"><span class="type">int</span> i,k1=<span class="number">0</span>,k2=<span class="number">0</span>;</span><br><span class="line"> <span class="keyword">for</span>(i=<span class="number">0</span>; name[i]!=<span class="number">0</span>;i++){</span><br><span class="line"> <span class="keyword">if</span>(ch<<span class="string">'A'</span>)<span class="keyword">break</span>;</span><br><span class="line"> k1+=(ch><span class="string">'Z'</span>)?(ch<span class="number">-32</span>):ch;</span><br><span class="line"> }</span><br><span class="line"> k2=k1^<span class="number">0x5678</span>^<span class="number">0x1234</span>;</span><br><span class="line"> <span class="keyword">return</span> k2;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>算法求逆是有难度的,需要有一定的编程基本功。常见的加密配对指令有xorr/xor、add/sub、inc/dec、rol/ror等,这些指令对都是一条用于加密,另一条用于解密的。<br>还有一种写注册机的方法是不分析其运算过程,用OllyDbg的Asm2Clipboard插件、IDA等工具直接将序列号算法的汇编代码提取出来,嵌人高级语言。这个方法的优点是不用理解算法实现的细节,只要将汇编代码嵌入注册机即可。函数就属于这种情况。将从0040137Eh到004013D6h处的汇编代码转换成asm文件格式,然后嵌人高级语言中调用,在代码转换中要注意栈平衡、数据进制、汇编语法格式、宇符串引用等。直接提取汇编代码并将其嵌人VC的代码如下。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br></pre></td><td class="code"><pre><span class="line">int kengen(char *name){</span><br><span class="line"> int k1=0,k2=0;</span><br><span class="line">BOOL bIsnum=FALSE;</span><br><span class="line"></span><br><span class="line">__asm</span><br><span class="line">{</span><br><span class="line"> mov esi,OFFSET cName</span><br><span class="line"> push esi</span><br><span class="line"> L002:</span><br><span class="line"> mov al, byte ptr [esi]</span><br><span class="line"> test al, al</span><br><span class="line"> je L014</span><br><span class="line"> cmp al, 0x41</span><br><span class="line"> jb L019</span><br><span class="line"> cmp al, 0x5A</span><br><span class="line"> jnb L011</span><br><span class="line"> inc esi</span><br><span class="line"> jmp L002</span><br><span class="line"> L011:</span><br><span class="line"> call L035</span><br><span class="line"> inc esi</span><br><span class="line"> jmp L002</span><br><span class="line"> L014:</span><br><span class="line"> pop esi</span><br><span class="line"> call L026</span><br><span class="line"> xor edi, 0x5678</span><br><span class="line"> mov eax, edi</span><br><span class="line"> jmp L025</span><br><span class="line"> L019:</span><br><span class="line"> pop esi</span><br><span class="line">mov bIsnum,1</span><br><span class="line"> L025:</span><br><span class="line"> jmp LEND</span><br><span class="line"> L026:</span><br><span class="line"> xor edi, edi</span><br><span class="line"> xor ebx, ebx</span><br><span class="line"> L028:</span><br><span class="line"> mov bl, byte ptr [esi]</span><br><span class="line"> test bl, bl</span><br><span class="line"> je L034</span><br><span class="line"> add edi, ebx</span><br><span class="line"> inc esi</span><br><span class="line"> jmp L028</span><br><span class="line"> L034:</span><br><span class="line"> retn</span><br><span class="line"> L035:</span><br><span class="line"> sub al, 0x20</span><br><span class="line"> mov byte ptr [esi], al</span><br><span class="line"> retn</span><br><span class="line"> LEND:</span><br><span class="line"> mov k1,eax</span><br><span class="line">}</span><br><span class="line">if(bIsnum)</span><br><span class="line">return 0;</span><br><span class="line">k2=k1^0x1234;</span><br><span class="line">return k2;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="警告窗口"><a href="#警告窗口" class="headerlink" title="警告窗口"></a>警告窗口</h3><p>“Nag”的本义是“烦人”。警告(Nag)窗口是软件设计者用来不时提醒用户购买正式版本的窗口。软件设计者可能认为,当用户忍受不了软件试用版中这些烦人的窗口时,就会考虑购买正式版。警告窗口可能会在程序启动或退出时弹出,或者在软件运行的某个时刻随机或定时弹出,确实比较烦人。<br>去除警告窗口常用的3种方法是修改程序的资源、静态分析及动态分析。使用资源修改工具去除警告窗口是个不错的方法,可以通过将可执行文件中警告窗口的属性改成透明或不可见来变相去除警告窗口。若要完全去除警告窗口,只需找到创建该窗口的代码并将其跳过。显示窗口的常用函数有MessageBoxA(W)、MessageBoxExA(W) , DialogBoxParamA(M),Show Window、Create WindowExA(W)等。然而,这些断点对某些警告窗口无效,这时可以尝试利用消息设置断点拦截。<br>实例Nag.exe是一个用于显示警告窗口的程序,它调用DialogBoxParamA函数来显示资源中的对话框。由于Nag.exe是调用资源来显示对话框的,可以用eXeScope或Resource Hacker打开它。警告窗口的资源如图所示。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240723112710606.webp" alt="image-20240723112710606"></p><p>启动画面窗口的ID是121,换算成十六进制数就是79h。od打开。具体代码如下。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240723114426459.webp" alt="image-20240723114426459"></p><p>DialogBoxParam函数一般和EndDialog函数配对使用,前者用于打开对话框,后者用于关闭对话框,因此,不能简单地将DialogBoxParam函数屏蔽。DialogBoxParam函数的原型如下。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">INT_PTR DialogBoxParamA(</span><br><span class="line"> [in, optional] HINSTANCE hInstance,</span><br><span class="line"> [in] LPCSTR lpTemplateName,</span><br><span class="line"> [in, optional] HWND hWndParent,</span><br><span class="line"> [in, optional] DLGPROC lpDialogFunc,</span><br><span class="line"> [in] LPARAM dwInitParam</span><br><span class="line">);</span><br></pre></td></tr></table></figure><p>通过上面的函数可以看出,IpDialogFunc参数很重要,DialogBoxParam函数将跳转到其指向的地址执行,对lpDialogFunc参数(此处为004010C4h)设断。中断后的代码如下。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240723150633267.webp" alt="image-20240723150633267"></p><p>主程序也是用DialogBoxParam函数显示的,因此有如下两种改法。<br>(1)跳过警告窗口代码。将“00401051 push00000000”改成“00401051 jmp 4010E5”。修改时,在OllyDbg里输入正确的代码。选择修改后的代码,执行右键快捷菜单中的“复制到可执行文件”功能,即可将修改保存到磁盘文件中。<br>(2)将两个DialogBoxParam函数的参数对调。DialogBoxParam函数有两个参数很重要,一个是主对话框处理函数指针,另一个是对话框ID。这种方法的思路是将主窗口的这两个参数放到警告窗口的DialogBoxParam函数上。修改代码如下。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240723152335403.webp" alt="image-20240723152335403"></p><p>在另外一些情况下,对话框不是以资源形式存在的,通过常用断点又拦截不下来,这时可以尝试使用消息断点,例如WM_DESTROY。</p><h3 id="时间限制"><a href="#时间限制" class="headerlink" title="时间限制"></a>时间限制</h3><p>时间限制程序有两类:一类是限制每次运行的时长;另一类是每次运行时长不限,但是有时间限制,例如使用30天。</p><ol><li>计时器</li></ol><p>有一类程序,每次运行时都有时间限制,例如运行10分钟或20分钟就停止,必须重新运行程序才能正常工作。这类程序里有一个计时器来统计程序运行的时间。那么,如何实现计时器呢?在DOS中,应用程序可以通过接管系统的计时器中断(一般为int 8h或int 1Ch)来维护一个计时器,它能每55毫秒发生1次(18.2次/秒)。在Windows中,计时器有如下选择。</p><p>(1)setTimer()函数</p><p>应用程序可在初始化时调用这个API函数,向系统申请一个计时器并指定计时器的时间间隔,同时获得一个处理计时器超时的回调函数。若计时器超时,系统会向申请该计时器的窗口过程发送消息WM_TIMER,或者调用程序提供的那个回调函数。该函数的原型如下。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">UINT_PTR <span class="title function_">SetTimer</span><span class="params">(</span></span><br><span class="line"><span class="params"> [in, optional] HWND hWnd,</span></span><br><span class="line"><span class="params"> [in] UINT_PTR nIDEvent,</span></span><br><span class="line"><span class="params"> [in] UINT uElapse,</span></span><br><span class="line"><span class="params"> [in, optional] TIMERPROC lpTimerFunc</span></span><br><span class="line"><span class="params">)</span>;</span><br></pre></td></tr></table></figure><p>hWd:窗口句柄。若计时器到时,系统将向这个窗口发送WM_TIMER消息。</p><p>nIDEvent:计时器标识。<br>uElapse:指定计时器时间间隔(以毫秒为单位)。<br>TIMERPROC:回调函数。若计时器超时,系统将调用这个函数。如果本参数为NULL,若计时器超时,将向相应的窗口发送WM_TIMER消息。这个回调函数的原型如下。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">void</span> <span class="title function_">Timerproc</span><span class="params">(</span></span><br><span class="line"><span class="params"> HWND unnamedParam1,</span></span><br><span class="line"><span class="params"> UINT unnamedParam2,</span></span><br><span class="line"><span class="params"> UINT_PTR unnamedParam3,</span></span><br><span class="line"><span class="params"> DWORD unnamedParam4</span></span><br><span class="line"><span class="params">)</span></span><br></pre></td></tr></table></figure><p>(2)高精度的多媒体计时器</p><p>多媒体计时器的精度可以达到1毫秒。应用程序可以通过调用timeSetEvent()函数来启动一个多媒体计时器。该函数的原型如下。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">MMRESULT <span class="title function_">timeSetEvent</span><span class="params">( UINT uDelay,</span></span><br><span class="line"><span class="params"> UINT uResolution,</span></span><br><span class="line"><span class="params"> LPTIMECALLBACK lpTimeProc,</span></span><br><span class="line"><span class="params"> WORD dwUser,</span></span><br><span class="line"><span class="params"> UINT fuEvent )</span></span><br></pre></td></tr></table></figure><p>(3)GetTickCount()函数</p><p>Windows提供了API函数GetTickCount(),该函数返回的是系统自成功启动以来所经过的时间(以毫秒为单位)。将该函数的两饮返回值相减,就能知道程序已经运行多长时间了。这个函数的精度取决于系统的设置。实际上,也可以在高级语言里利用其各自开发库提供的函数来实现计时,例如在C语言中可以使用time()函数获得系统时间。</p><p>(4)timeGetTime()函数</p><p>多媒体计时器函数timeGetTime0也可以返回Windows自启动后所经过的时间(以毫秒为单位)。一般情况下,不需要使用高精度的多媒体计时器,因为精度太高会对系统性能造成影响。</p><ol start="2"><li>时间限制</li></ol><p>演示版软件一般都有使用时间的限制,例如试用30天,超过试用期就不能运行,只有向软件作者付费注册之后,才能得到无时间限制的注册版。这种保护的实现方式大致如下。</p><p>在安装软件的时候由安装程序取得当前系统日期,或者由主程序在软件第1次运行的时候获得系统日期,并将其记录在系统中的某个地方(可能记录在注册表的某个不显眼的位置,也可能记录在某个文件或扇区中)。这个时间统称为软件的安装日期。程序每次运行时都要取得当前系统日期,并将其与之前记录的安装日期进行比较,当差值超出允许的时间(例如30天)时就停止运行。<br>这种日期限制的原理很简单,但是在实现的时候,如果对各种情况的处理不够周全,就很容易被绕过。例如,在到期后简单地把机器时间调回去,软件就又可以正常使用了。</p><p>如果考虑得比较周全,软件最少要保存两个时间值。一个时间值是上面所说的安装时间。这个时间可以由安装程序在安装软件的时候记录,也可以在软件第1次运行的时候记录(即软件发现该值不存在时,就将当前日期作为其值记录下来)。为了提高解密难度,最好把这个时间值存储在多个地方(解密者可能通过RegMon、FileMon等监视工具轻易地找到存放该值的地方,然后剔除该键值,这样软件就又可以正常使用了。<br>另一个时间值就是软件最近一次运行的日期,这是防止用户将机器日期改回去而设的。软件每次退出的时候都要将该日期取出,与当前日期进行比较,如果当前日期大于该日期,则用当前日期替换该日期,否则保持该日期。同时,软件每次启动时要把该日期读出,与当前日期进行比较,如果该日期大于当前系统日期,则说明用户修改了机器时间,软件可以拒绝运行。<br>用于获取时间的API函数有GetSystemTime、GetLocalTime和GetFileTime。软件作者可能不会直接使用这些函数来获得系统时间(例如,采用高级语言中封装好的类来操作系统时间等,但这些封装好的类实际上也调用了这些函数)。解密者在采用动态跟踪方法破解这种日期限制时,最常用的断点也是这几个。</p><p>还有一种可以比较方便地获得当前系统日期的方法就是读取需要频繁修改的系统文件(例:Windows注册表文件user.dat、system.dat等)的最后修改日期,利用FileTime ToSystem Timet()函数其转换为系统日期格式,从而得到当前系统日期。<br>需要指出的是,采用时间限制的软件必须能防范RegMon、FileMon之类的监视软件,否则时间的存放位置会很容易被找到。</p><ol start="3"><li>拆解时间限制保护</li></ol><p>实例程序Timer.exe采用SetTimert()函数计时,每次运行20秒,运行原理是:先用SetTimer(hwnd,L,1000,NULL)函数设置一个计时器,时间间隔是1000毫秒,这个函数每秒发送1次WM_TIMER消息。当应用程序收到消息时,将执行如下语句。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">case</span> WM_TIMER :</span><br><span class="line"><span class="keyword">if</span>(i<=<span class="number">19</span>)</span><br><span class="line"> i++;</span><br><span class="line"><span class="keyword">else</span></span><br><span class="line"> sendMessage(hDlg, WM_CLOSE,<span class="number">0</span>,<span class="number">0</span>);</span><br><span class="line"><span class="keyword">return</span> <span class="number">0</span>;</span><br></pre></td></tr></table></figure><p>因此,可以用SetTimer()函数设断拦截,代码如下</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240723161541470.webp" alt="image-20240723161541470"></p><p>去除时间限制有如下两种方法。<br>(1)直接跳过SetTimer(0函数,不产生WM_TIMER消息。来到004010C6h处,输入修改指令”jmp 4010D6”。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240723162909725.webp" alt="image-20240723162909725"></p><p>(2)利用WM_TIMER消息,查看VC的头文件WINUSER.H,得知“#define WM_TIMER Ox0113”。在调试器里查找字串“113”(当然,在实际使用中有可能采取其他形式检查字串是否为“113”),代码如下。因此,只要修改00401184h处,就能取消时间限制了。可以用2字节替换,例如“9090”或“eb00”。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240723165237624.webp" alt="image-20240723165237624"></p><p>直接nop即可</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240723165838252.webp" alt="image-20240723165838252"></p><p>另外,辅助工具变速齿轮可以加快和减慢应用程序的时间,一般与动态分析配合使用。例如某软件运行1小时后才退出,可以用变速齿轮让“时间”加速,几分钟后,软件就认为到了1小时而退出,为调试程序带来便利。</p><h3 id="菜单功能限制"><a href="#菜单功能限制" class="headerlink" title="菜单功能限制"></a>菜单功能限制</h3><p>这类程序一般是Demo版的,其菜单或窗口中的部分选项是灰色的,无法使用。这种功能受限的程序一般分成两种。<br>一种是试用版和正式版的软件是两个完全不同的版本,被禁止的功能在试用版的程序中根本没有相应的程序代码,这些代码只在正式版中才有,而正式版是无法免费下载的,只能向软件作者购买。对这种程序,解密者要想在试用版中使用和正式版一样的功能几乎是不可能的,除非自己向可执行程序中添加相应的代码。<br>另一种是试用版和正式版为同一个文件。没有注册的时候按照试用版运行,禁止用户使用某些功能;注册之后就以正式版运行,用户可以使用其全部功能。可见,被禁止的那些功能的程序代码其实是存在于程序之中的,解密者只要通过一定的方法恢复被限制的功能,就能使该Deo软件与正式版一样了。<br>对比一下就能知道,前一种保护方式更好,因为它使被解难度大大增加。如果采用功能限制的保护方式,强烈建议使用前一种方式。</p><ol><li>相关函数</li></ol><p>如果要将软件菜单和窗口变灰(不可用状态),可以使用如下函数</p><p>(1)EnableMenultem()函数</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">BOOL <span class="title function_">EnableMenuItem</span><span class="params">(</span></span><br><span class="line"><span class="params"> [in] HMENU hMenu,</span></span><br><span class="line"><span class="params"> [in] UINT uIDEnableItem,</span></span><br><span class="line"><span class="params"> [in] UINT uEnable</span></span><br><span class="line"><span class="params">)</span>;</span><br></pre></td></tr></table></figure><p>hMem:菜单句柄。<br>uIDEnableItem:欲允许或禁止的一个菜单条目的标识符。</p><p>uEnable:控制标志,包括MF_ENABLED(允许,Oh)、MF_GRAYED(灰化,1h)、MF_DISABLED(禁止,2h)、MF_BYCOMMAND和MF_BYPOSITION。<br>返回值:返回菜单项以前的状态。如果菜单项不存在,就返回FFFFFFFFh。</p><p>(2)EnableWindow()函数</p><p>允许或禁止指定窗口,原型如下。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">BOOL <span class="title function_">EnableWindow</span><span class="params">(</span></span><br><span class="line"><span class="params"> [in] HWND hWnd,</span></span><br><span class="line"><span class="params"> [in] BOOL bEnable</span></span><br><span class="line"><span class="params">)</span>;</span><br></pre></td></tr></table></figure><p>hWnd:窗口句柄。<br>bEnable:“TRUE”为允许,“FALSE”为禁止。<br>返回值:非0表示成功,0表示失败。</p><ol start="2"><li>拆解菜单限制保护</li></ol><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240723174223975.webp" alt="image-20240723174223975"></p><p>当uEnable控制标志为0时,恢复菜单的功能,具体操作为将“0040I1E3 push 0x1”改成“push 0”。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240723174309601.webp" alt="image-20240723174309601"></p><h3 id="KeyFile保护"><a href="#KeyFile保护" class="headerlink" title="KeyFile保护"></a>KeyFile保护</h3><p>KeyFile是一种利用文件来注册软件的保护方式。KeyFile一般是一个小文件,可以是纯文本文件,也可以是包含不可显示字符的二进制文件。其内容是一些加密或未加密的数据,其中可能有用户名、注册码等信息,文件格式则由软件作者自己定义。试用版软件没有注册文件。当用户向作者付费注册之后,会收到作者提供的注册文件,其中可能包含用户的个人信息。用户只要将该文件放入指定的目录,就可以让软件成为正式版了。该文件一般放在软件的安装目录或系统目录下。软件每次启动时,从该文件中读取数据,然后利用某种算法进行处理,根据处理的结果判断是否为正确的注册文件。如果正确,则以注册版模式运行。<br>在实现这种保护的时候,建议软件作者采用稍大一些的文件作为KeyFile(一般在几KB左右),其中可以加人一些垃圾信息以干扰解密者。对注册文件的合法性检查可以分成几部分,分散在软件的不同模块中进行判断。注册文件内的数据处理也要尽可能采用复杂的运算,而不要使用简单的异或运算。这些措施都可以增加解密的难度。和注册码一样,也可以让注册文件中的部分数据和软件中的关键代码或数据发生关系,使软件无法被暴力破解。</p><ol><li>相关API函数</li></ol><p>KevFile是一个文件,因此,所有与Windows文件操作有关的API函数都可作为动态跟踪破解的断点。这类常用的文件函数如表所示。各API函数的具体含义请参考MSDN或相关API文档。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240723174611079.webp" alt="image-20240723174611079"></p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240723174624871.webp" alt="image-20240723174624871"></p><ol start="2"><li>拆解KeyFile保护</li></ol><p>采用实例文件PacMe</p><p>(1)拆解KeyFile的一般思路</p><p>①用Process Monitor等工具监视软件对文件的操作,以找到KeyFile的文件名,<br>②伪造一个KevFile文件。用十六进制工具编辑和修改KeyFile(普通的文本编辑工具可能无法完成这项任务)。<br>③在调试器里用CreateFileA函数设断,查看其打开的文件名指针,并记下返回的句柄。</p><p>④用ReadFile函数设断,分析传递给ReadFile函数的文件句柄和缓冲区地址。文件句柄一般和第③步返回的相同(若不同,则说明读取的不是该KeyFile。在这里也可以使用条件断点)。缓冲区地址是非常重要的,因为读取的重要数据就放在这里。对缓冲区中存放的字节设内存断点,监视读取的KeyFile的内容。</p><p>当然,上述只是大致步骤,有的程序在判断KevFile时会先判断文件大小和属性、移动文件指针等。总之,对KeyFile的分析深入与否,取决于分析者对Win32 File I/O API的熟悉程度,也就是API编程的水平。</p><p>(2)监视文件的操作</p><p>PacMe的注册信息放在某一文件中(可以通过文件监视工具得到)。Process Monitor是一个不错的文件监视工具,使用时建议设置过滤器。<br>所谓过滤器,其实是一组条件。这组条件用来限制Process Monitor该显示什么、不该显示什么。单击菜单项“Filter’”,打开过滤器,在第1个下拉列表框中选择“Process Name”选项,在第2个下拉列表框中选择“is”选项,在第3个下拉列表框中填写要监控的文件名“PacMe..exe”,单击“Add”按钮,如图所示。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240724094458660.webp" alt="image-20240724094458660"></p><p>Process Monitor启动后会立刻进行监控操作,包括文件系统、注册表、网络、进程及性能分析。在本例中,只需要监控文件系统,其他如注册表、进程等监控可以取消。单击工具栏上的注册表和进度监控等按钮即可取消监控,如图所示。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240724094722969.webp" alt="image-20240724094722969"></p><p>按“Crl+E”快捷键可以捕捉事件,按“Ctrl+X”快捷键可以清除所有记录。Process Monitor会按时间顺序记录系统中发生的各种文件访问事件,如图所示。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240724094953387.webp" alt="image-20240724094953387"></p><p>(3)分析过程</p><p>除了用Process Monitor监视文件获得KeyFile文件名,也可以直接对文件的相关函数设断,从而获得KeyFile的相关信息。用OllyDbg装载PacMe后,按“Fg”键运行PacMe。用CreateFileA函数设断,单击PacMe的“Check”按钮,中断代码如下。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240724095502284.webp" alt="image-20240724095502284"></p><p>OllyDbg会直接把CreateFileA函数读取的文件名显示出来。KeyFile名为“KwazyWeb.bit”。用十六进制工具伪造一个KeyFile,建议将其内容设置为一些有规律的数字,例如1、2、3、4、5…以便在跟踪时进行分析。<br>重新运行程序,PacMe将打开KwazyWeb.bit文件,读取数据并进行计算比较,代码如下。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240724095859789.webp" alt="image-20240724095859789"></p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240724095955055.webp" alt="image-20240724095955055"></p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240724100026540.webp" alt="image-20240724100026540"></p><p>再来分析一下验证的核心代码,具体如下。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240724100916977.webp" alt="image-20240724100916977"></p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240724101005804.webp" alt="image-20240724101005804"></p><p>这是放大后的String2</p><p>这就是经典的“吃豆子”游戏,“C”是吃家,“*”是墙壁,“.”是通路,“X”是终点</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240724101121367.webp" alt="image-20240724101121367"></p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240724101152993.webp" alt="image-20240724101152993"></p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240724101242596.webp" alt="image-20240724101242596"></p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240724101354753.webp" alt="image-20240724101354753"></p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240724103306528.webp" alt="image-20240724103306528"></p><p>这是一个标准的迷宫程序,从“C”开始,一共走18次,每次可以走4步(18次大循环和4次小循环)。碰到“*”就中断,直到遇见“X”注册成功。路线非常清楚,就是顺着“.”走。按照上面的分析。“0”代表“↑”,“1”代表“→”,“2”代表“↓”,“3”代表“←”,按图一步步前进,就可以得到一系列数据,如图所示。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240724103354502.webp" alt="image-20240724103354502"></p><p>图中的数是四进制数,转换成十六进制数为“A9 AB A5 10 54 3F 30 55 65 16 56 BE F3 EA E9 50 55 AF”。然后,程序通过用户名算出一个数,再与上面的十六进制数进行异或运算。<br>在此以用户名“pediy”推出KeyFile。“pediy”的十六进制数是“70 65 64 69 79”。KeyFile由3部分组成,如图所示。计算步骤如下。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240724103508773.webp" alt="image-20240724103508773"></p><p>①计算“pediy”字符的和,70h+65h+64h+69h+79h=21Bh,取低8位1Bh。<br>②用1Bh依次与“A9 AB A5 10 54 3F 30 55 65 16 56 BE F3 EA E9 50 55 AF”进行异或运算,结果是“B2 B0 BE 0B 4F 24 2B 4E 7E 0D 4D A5 E8 F1 F2 4B 4E B4”。</p><h3 id="网络验证"><a href="#网络验证" class="headerlink" title="网络验证"></a>网络验证</h3><p>网络验证是目前很流行的一种保护技术,其优点是可以将一些关键数据放到服务器上,软件必须从服务器中取得这些数据才能正确运行。拆解网络验证的思路是拦截服务器返回的数据包,分析程序是如何处理数据包的。</p><ol><li>相关函数</li></ol><p>当一个连接建立以后,就可以传输数据了。常用的数据传送函数有send()和recv()两个Socket函数,以及微软的扩展函数WSASend()和WSARecv()。</p><p>(1)send()函数</p><p>客户程序一般用send()函数向服务器发送请求,服务器则通常用send()函数向客户程序发送应答,示例如下</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">int</span> <span class="title function_">sent</span><span class="params">(</span></span><br><span class="line"><span class="params">SOCKET s,</span></span><br><span class="line"><span class="params"><span class="type">const</span> <span class="type">char</span> FAR *buf,</span></span><br><span class="line"><span class="params"><span class="type">int</span> len,</span></span><br><span class="line"><span class="params"><span class="type">int</span> flags</span></span><br><span class="line"><span class="params">)</span></span><br></pre></td></tr></table></figure><p>(2)recv()函数</p><p>不论是客户还是服务器应用程序,都使用recv()函数从TCP连接的另一端接收数据。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">int</span> <span class="title function_">recv</span><span class="params">(</span></span><br><span class="line"><span class="params">SOCKET s,</span></span><br><span class="line"><span class="params"><span class="type">char</span> FAR *buf,</span></span><br><span class="line"><span class="params"><span class="type">int</span> len,</span></span><br><span class="line"><span class="params"><span class="type">int</span> flags</span></span><br><span class="line"><span class="params">)</span></span><br></pre></td></tr></table></figure><ol start="2"><li>破解网络验证的一般思路</li></ol><p>如果网络验证的数据包内容固定,可以将数据包抓取,写一个本地服务端模来拟服务器。如果验证的数据包内容不固定,则必须分析其结构,找出相应的算法。<br>实例CrackMeNet.exe是一款网络验证工具。CrackMeNetS.exe是服务端,提供了一组正确的登录账号。因为在实际操作中是接触不到服务端的,所以必须从客户端入手,利用一组正确的账号来击破这个网络验证保护机制。</p><p>(1)分析发送的数据包</p><p>建议用IDA与OllyDbg一起进行分析。IDA能正确识别C函数,分析起来非常方便。OllyDbg加载客户端后,用send()函数设断。输入正确的账号与口令,单击“Register’”按钮,中断并回到当前程序领空,代码如下。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240724152302389.webp" alt="image-20240724152302389"></p><p>send()函数将把Data缓冲区中的数据发送到服务端。查看Data中的数据,发现是加密的。在IDA中向前查看代码,再结合OllyDbg进行分析。这段代码的功能如下。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240724153000159.webp" alt="image-20240724153000159"></p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240724153025948.webp" alt="image-20240724153025948"></p><p>原来,客户端将输人的Name及Key按如图所示的格式处理,进行异或加密运算,将数据发送给服务端。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240724153611266.webp" alt="image-20240724153611266"></p><p>(2)分析接收的数据包</p><p>服务端接收数据后,经过计算,将包括正确数据包在内的数据返回客户端,客户端程序使用recv()函数接收数据,相关代码如下。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240724153939467.webp" alt="image-20240724153939467"></p><p>上面这段代码表示收到数据并进行解密,解密后的数据存放在41AE68h~41AEC1h这段空间中,如图所示。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240724154225543.webp" alt="image-20240724154225543"></p><p>接下来,程序会从41AE68h~41AEC1h中读取需要的字节,因此,只要对这段数据下内存读断点,就可以很容易地定位到相关代码处。但在实际应用中,程序读取这部分数据的操作可能比铰隐蔽,例如运行一段时间再比较或使用某功能后再比较等,因此有可能遗漏相关的读取代码。</p><p>本实例用全局变量构建缓冲区。由于是以Debug编译的程序,实例程序会直接用如下指令读取缓冲区数据。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240724154343377.webp" alt="image-20240724154343377"></p><p>一个简单且有效的办法是在整个代码里搜索访问41AE68h~41AEC1h这段缓冲区中的mov指令。这时,IDA的强大就体现出来了。用IDA运行如下Python脚本,可将读取指定内存的代码列出。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> idaapi</span><br><span class="line"><span class="keyword">import</span> idc</span><br><span class="line"><span class="keyword">import</span> idautils</span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">Getasm</span>(<span class="params">ea_from, ea_to, range1, range2</span>):</span><br><span class="line"> <span class="keyword">with</span> <span class="built_in">open</span>(<span class="string">"code.txt"</span>, <span class="string">"w"</span>) <span class="keyword">as</span> fp:</span><br><span class="line"> ea = ea_from</span><br><span class="line"> <span class="keyword">while</span> ea < ea_to:</span><br><span class="line"> cmd = idc.print_insn_mnem(ea) <span class="comment"># 使用 idc.print_insn_mnem 获取指令助记符</span></span><br><span class="line"> <span class="keyword">if</span> cmd == <span class="string">"mov"</span> <span class="keyword">or</span> cmd == <span class="string">"lea"</span>:</span><br><span class="line"> opcode = idc.get_wide_dword(ea + <span class="number">4</span>) <span class="comment"># 获取指令后面4个字节作为操作码</span></span><br><span class="line"> <span class="keyword">if</span> opcode < <span class="number">0</span>: <span class="comment"># 如果操作码是负数,进行补码转换</span></span><br><span class="line"> opcode = (~opcode + <span class="number">1</span>)</span><br><span class="line"> idaapi.msg(<span class="string">"-> %08X %08X\n"</span> % (ea, opcode))</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> range1 <= opcode <= range2:</span><br><span class="line"> delta = opcode - range1</span><br><span class="line"> idc.set_cmt(ea, <span class="string">"// +0x%04X"</span> % delta, <span class="number">0</span>) <span class="comment"># 在指令上添加注释</span></span><br><span class="line"> fp.write(<span class="string">"%08X %s\n"</span> % (ea, idc.generate_disasm_line(ea)))</span><br><span class="line"> ea = idc.next_head(ea, ea_to) <span class="comment"># 获取下一个指令地址</span></span><br><span class="line"> idaapi.msg(<span class="string">"OK!\n"</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment"># 示例调用</span></span><br><span class="line">Getasm(<span class="number">0x401000</span>, <span class="number">0x40F951</span>, <span class="number">0x41AE68</span>, <span class="number">0x0041AEC1</span>)</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>单击菜单项“File”一“Script file”,打开getasm.py脚本。这个脚本将程序访问缓冲区中的所有指令列出,在当前目录下生成code.txt文件,内容如下。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240724164650215.webp" alt="image-20240724164650215"></p><p>本例将向缓冲区中放入全局变量。在一般情祝下,缓冲区中存放的是局部变量。访问缓冲区数据的指令如下,其中“rm32”表示32位寄存器存储器。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">mov r/m16, [r/m32+n]</span><br><span class="line">lea r/m16, [r/m32+n]</span><br><span class="line">mov r/m16, [ebp-n]</span><br></pre></td></tr></table></figure><p>(3)解除网络验证</p><p>发送与接收的封包都分析完毕。比较简单的解决方法是编写一个服务端,模拟服务器来接收和发送数据。如果软件是用域名登录服务器的,可以修改hosts文件,使域名指向本地(127.0.0.1)。如果软件是直接用IP地址连接服务器的,可以用inet_addr或connect等设断,将IP地址修改为本地IP地址,或者使用代理软件将IP地址指向本地。</p><p>除了编写服务端,也可直接修改客户端程序,将封包中的数据整合进去,步骤如下。</p><p>①将实例CrackMeNet.exe复制一份,用OllyDbg打开,然后将开始截取的正确数据粘贴到41AE68h-41AEC1h这段地址中。</p><p>②将发包功能(send)去除,再读取随机数并将其放到0041AE76处,代码如下。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240724171213820.webp" alt="image-20240724171213820"></p><p>此处原是send,现在跳转到0040FAA8h这个空白地址处</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240724171405199.webp" alt="image-20240724171405199"></p><p>③将recv()函数去除并跳过数据解密代码,修改代码如下。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240724172657789.webp" alt="image-20240724172657789"></p><p>经过这样的处理,再运行实例,单击“Register’”按钮,会跳出一个对话框,提示“Emor:Connectionfailed”。直接强行跳过该对话框。修改代码如下。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240724180206136.webp" alt="image-20240724180206136"></p><p>从以上分析中可以看出,网络验证的关键就是数据包分析。数据包分析的辅助工具有WPE、iris等。如果数据包是加密的,或者需要彻底分析数据包处理过程,就必须用发送接收函数设断,跟踪程序对数据包的处理过程。</p><h3 id="光盘检测"><a href="#光盘检测" class="headerlink" title="光盘检测"></a>光盘检测</h3><p>一些采用光盘形式发行的应用软件和游戏,在使用时需要检查光盘是否插在光驱中,如果没有则拒绝运行。这是为了防止用户将软件或游戏的一份正版拷贝安装在多台机器上且同时使用,其思路与DOS时代的钥匙盘保护类似,虽然能在一定程度上防止非法拷贝,但也给正版用户带来了一些麻烦一一旦光盘被划伤,用户就无法使用软件了。本节将介绍常见的光盘检测实现方式,以及如何去除光盘检测的基本知识。一些专业的光盘保护软件(例如SafeDisc等)比较复杂,在本节中就不讲述了。<br>最简单也最常见的光盘检测就是程序在启动时判断光驱中的光盘里是否存在特定的文件。如果不存在,则认为用户没有使用正版光盘,拒绝运行。在程序运行过程中,一般不再检查光盘是否在光驱中。在Windows下的具体实现一般是:先用GetLogicalDriveStrings()或GetLogicalDrives()函数得到系统中安装的所有驱动器的列表,然后用GetDriveType()函数检查每个驱动器,如果是光驱,则用CreateFile()或FindFirstFile()函数检查特定的文件是否存在,甚至可能进一步检查文件的属性、大小、内容等。</p><p>这种光盘检测方式是比较容易被破解的。解密者只要利用上述函数设置断点,找到程序启动时检查光驱的地方,然后修改判断指令,就可以跳过光盘检测。上述保护的一种增强类型就是把程序运行时需要的关键数据放在光盘中。这样,即使解密者能够强行跳过程序启动时的检查,但由于没有使用正版光盘,也就没有程序运行时所需要的关键数据,程序自然会崩溃,从而在一定程度上起到了防破解的作用。<br>对付这种增强型光盘保护还是有办法的,可以简单地利用刻录和复制工具将光盘复制多份,也可以采用虚拟光驱程序来模拟正版光盘。常用的虚拟光驱程序有Virtual CD、Virtual Drive、Daemon Tools等。值得一提的是Daemon Tools,它不仅是免费的,而且能够模拟一些加密光盘。这些光盘加密工具一般都会在光轨上做文章,例如做暗记等。有的加密光盘可用工作在原始模式(Raw mode)的光盘拷贝程序原样复制,例如Padus公司的DiscJuggler和Elaborate Bytes公司的CloneCD等。</p><ol><li>相关函数</li></ol><p>下面介绍与光盘检测有关的函数</p><p>(1)GetDriveType()函数</p><p>该函数用于获取磁盘驱动器的类型,示例如下。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">UINT <span class="title function_">GetDriveTypeA</span><span class="params">(</span></span><br><span class="line"><span class="params"> LPCSTR lpRootPathName</span></span><br><span class="line"><span class="params">)</span>;</span><br></pre></td></tr></table></figure><p>返回值<br>0:驱动器不能识别<br>1:根目录不存在<br>2:移动存储器<br>3:固定驱动器(硬盘)。<br>4:远程驱动器(网络)。<br>5:CD-ROM驱动器<br>6:RAM disk</p><p>(2)GetLogicalDrives()函数</p><p>该函数用于获取逻辑驱动器符号,没有参数。<br>返回值:如果失败就返回0,否则返回由位掩码表示的当前可用驱动器,示例如下。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">bit <span class="number">0</span> drive A</span><br><span class="line">bit <span class="number">1</span> drive B</span><br><span class="line">bit <span class="number">2</span> drive C</span><br></pre></td></tr></table></figure><p>(3)GetLogicalDriveStrings()函数</p><p>该函数用于获取当前所有逻辑驱动器的根驱动器路径,示例如下。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">DWORD <span class="title function_">GetLogicalDriveStringsW</span><span class="params">(</span></span><br><span class="line"><span class="params"> DWORD nBufferLength,</span></span><br><span class="line"><span class="params"> LPWSTR lpBuffer</span></span><br><span class="line"><span class="params">)</span>;</span><br></pre></td></tr></table></figure><p>返回值:如果成功就返回实际的字符数,否则返回0。</p><p>(4)GetFileAttributes()函数</p><p>用于判断指定文件的属性,示例如下</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">DWORD <span class="title function_">GetFileAttributesW</span><span class="params">(</span></span><br><span class="line"><span class="params"> [in] LPCWSTR lpFileName</span></span><br><span class="line"><span class="params">)</span>;</span><br></pre></td></tr></table></figure><ol start="2"><li>拆解光盘保护</li></ol><p>实例文件CD_Check</p><p>这个程序先用GetDriveType()函数检测文件是否在光驱里,再用CreateFile()函数尝试打开光盘文件,示例如下。如果存在,则成功。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240725111339082.webp" alt="image-20240725111339082"></p><p>最后改成“jmp 00401485“即可</p><h3 id="只运行一个实例"><a href="#只运行一个实例" class="headerlink" title="只运行一个实例"></a>只运行一个实例</h3><p>Windows是一个多任务操作系统,应用程序可以多次运行以形成多个运行实例。但有时基于对某些方面的考虑(例如安全性),要求程序只能运行1个实例。</p><ol><li>实现方法</li></ol><p>(1)查找窗口法</p><p>这是最为简单的一种方法。在程序运行前,用FindWindowA、Get WindowText函数查找具有相同窗口类名和标题的窗口,示例如下。如果找到,就说明已经存在一个实例。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">HWND <span class="title function_">FindWindowA</span><span class="params">(</span></span><br><span class="line"><span class="params"> [in, optional] LPCSTR lpClassName,</span></span><br><span class="line"><span class="params"> [in, optional] LPCSTR lpWindowName</span></span><br><span class="line"><span class="params">)</span>;</span><br></pre></td></tr></table></figure><p>返回值:如未找到相符的窗口,则返回0。<br>程序代码的形式如下。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">TCHAR AppName[] = TEXT (<span class="string">"只运行1个实例"</span>);</span><br><span class="line">hWnd=FindWindow(<span class="literal">NULL</span>,AppName);</span><br><span class="line"><span class="keyword">if</span> (hWnd ==<span class="number">0</span>)初始化程序</span><br><span class="line"><span class="keyword">else</span> 退出</span><br></pre></td></tr></table></figure><p>(2)使用互斥对象</p><p>尽管互斥对象通常用于同步连接,但用在这里也是很方便的。一般用CreateMutexA函数实现,它的作用是创建有名或者无名的互斥对象,示例如下。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">HANDLE <span class="title function_">CreateMutexW</span><span class="params">(</span></span><br><span class="line"><span class="params"> [in, optional] LPSECURITY_ATTRIBUTES lpMutexAttributes,</span></span><br><span class="line"><span class="params"> [in] BOOL bInitialOwner,</span></span><br><span class="line"><span class="params"> [in, optional] LPCWSTR lpName</span></span><br><span class="line"><span class="params">)</span>;</span><br></pre></td></tr></table></figure><p>返回值:如果函数调用成功,返回值是互斥对象句柄。<br>程序代码的形式一般如下。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">TCHAR AppName[] = TEXT (<span class="string">"只运行1个实例"</span>);</span><br><span class="line">Mutex = CreateMutex(<span class="literal">NULL</span>,FALSE,AppName)</span><br><span class="line"><span class="keyword">if</span> GetLastError<>ERROR_ALREADY_EXISTS</span><br><span class="line"> 初始化</span><br><span class="line"><span class="keyword">else</span> 退出</span><br><span class="line"> ReleaseMutex(Mutex);</span><br></pre></td></tr></table></figure><p>(3)使用共享区块</p><p>创建一个共享区块(Section)。该区块拥有读取、写入和共享保护属性,可以让多个实例共享同一内存块。将一个变量作为计数器放到该区块中,该应用程序的所有实例可以共享该变量,从而通过该变量得知有没有正在运行的实例。</p><ol start="2"><li>实例</li></ol><p>Serial.exe只能同时运行1个实例。该程序利用Find Window函数查找指定字串来确定程序是否运行,示例如下。对付这类保护最有效的办法是修改应用程序的窗口标题。当然,修改Find Window函数的返回值也能取消其限制。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240725115246074.webp" alt="image-20240725115246074"></p><h3 id="常用断点设置技巧"><a href="#常用断点设置技巧" class="headerlink" title="常用断点设置技巧"></a>常用断点设置技巧</h3><p>WIN32常用断点</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240725115327780.webp" alt="image-20240725115327780"></p>]]></content>
</entry>
<entry>
<title>逆向分析技术</title>
<link href="/2024/07/26/%E9%80%86%E5%90%91%E5%88%86%E6%9E%90%E6%8A%80%E6%9C%AF/"/>
<url>/2024/07/26/%E9%80%86%E5%90%91%E5%88%86%E6%9E%90%E6%8A%80%E6%9C%AF/</url>
<content type="html"><![CDATA[<h1 align = "center">逆向分析技术</h1><p>将可执行程序反汇编,通过分析反汇编代码来理獬其代码功能(例如各接口的数据结构等),然后用高级语言重新描述这段代码,逆向分析原始软件的思路,这个过程就称作逆向工程(ReverseEngineering),有时也简单地称作逆向(Reversing)。这是一项很重要的技能,需要扎实的编程功底和汇编知识。逆向分析的首选工具是IDA,它的插件Hex-Rays Decompiler能完成许多代码反编译工作,在逆向时可以作为一款辅助工具使用。<br>逆向工程可以让我们了解程序的结构及程序的逻辑,因此,利用逆向工程可以洞察程序的运行过程。一般的所谓“软件破解”只是逆向工程中非常初级的部分.</p><h3 id="一、32位软件逆向技术"><a href="#一、32位软件逆向技术" class="headerlink" title="一、32位软件逆向技术"></a>一、32位软件逆向技术</h3><p>示例使用的是vc6.0编译的32位程序</p><ol><li>启动函数</li></ol><p>在编写Win32应用程序时,都必须在源码里实现一个WinMain函数。但Windows程序的执行并不是从WinMain函数开始的,首先被执行的是启动函数的相关代码,这段代码是由编译器生成的。在启动代码初始化进程完成后,才会调用WinMain函数。</p><p>对Visual C++程序来说,它调用的是C/C+运行时启动函数,该函数负责对C/C+运行库进行初始化。Visual C+配有C运行库的源代码,可以在crtlsre\crto.c文件中找到启动函数的源代码(在安装时,Visual C++必须启用安装源代码选项)。用于控制台程序的启动代码存放在crt\src\wincmdln.c中。</p><p>所有C/C++程序运行时,启动函数的作用基本相同,包括检索指向新进程的命令行指针、检索指向新进程的环境变量指针、全局变量初始化和内存栈初始化等。当所有的初始化操作完成后,启动函数就会调用应用程序的进人点函数(main和WinMain)。调用WinMain函数的示例如下。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240715114444225.webp" alt="image-20240715114444225"></p><p>进入点返回时,启动函数便调用C运行库的exit函数,将返回值(nMainRetVal)传递给它,进行一些必要的处理,最后调用系统函数ExitProcess退出。</p><p>一个用Visual C+编译的程序,其程序启动代码的汇编代码如下。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240715114659936.webp" alt="image-20240715114659936"></p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240715114743978.webp" alt="image-20240715114743978"></p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240715114913979.webp" alt="image-20240715114913979"></p><p>开发人员可以修改启动源代码,但这样做会导致即使是同一编译器,生成的启动代码也不同。其他编译器都有相应的启动代码。</p><ol start="2"><li>函数</li></ol><p>程序都是由具有不同功能的函数组成的,因此在逆向分析中将重点放在函数的识别及参数的传递上是明智的,这样做可以将注意力集中在某一段代码上。函数是一个程序模块,用来实现一个特定的功能。一个函数包括函数名、入口参数、返回值、函数功能等部分。</p><p><code>函数的识别:</code>程序通过调用程序来调用函数,在函数执行后又返回调用程序继续执行。函数如何知道要返回的地址呢?实际上,调用函数的代码中保存了一个返回地址,该地址会与参数一起传递给被调用的函数。有多种方法可以实现这个功能,在绝大多数情况下,编译器都使用call和ret指令来调用函数及返回调用位置。</p><p>call指令与跳转指令功能类似。不同的是,call指令保存返回信息,即将其之后的指令地址压入栈的顶部,当遇到ret指令时返回这个地址。也就是说,call指令给出的地址就是被调用函数的起始地址。ret指令则用于结束函数的执行(当然,不是所有的ret指令都标志着函数的结束)。通过这一机制可以很容易地把函数调用和其他跳转指令区别开来。<br>因此,可以通过定位call机器指令或利用ret指令结束的标志来识别函数。call指令的操作数就是所调用函数的首地址。看一个例子,代码如下。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">int</span> <span class="title function_">Add</span><span class="params">(<span class="type">int</span> x, <span class="type">int</span> y)</span>;</span><br><span class="line">main( )</span><br><span class="line">{</span><br><span class="line"> <span class="type">int</span> a = <span class="number">5</span>, b = <span class="number">6</span>;</span><br><span class="line"> Add(a, b);</span><br><span class="line"> <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">Add(<span class="type">int</span> x, <span class="type">int</span> y)</span><br><span class="line">{</span><br><span class="line"> <span class="keyword">return</span>(x + y);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>编译结果如下</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240715141146105.webp" alt="image-20240715141146105"></p><p>这种函数直接调用方式使程序变得很简单一所幸大部分情况都是这样的。但也有例外,程序调用函数是间接调用的,即通过寄存器传递函数地址或动态计算函数地址调用。例如<code>CALL [4*eax+10h]</code></p><p><code>函数的参数:</code>函数传递参数有3种方式,分别是栈方式、寄存器方式及通过全局变量进行隐含参数传递的方式。如果参数是通过栈传递的,就需要定义参数在栈中的顺序,并约定函数被调用后由谁来平衡栈。如果参数是通过寄存器传递的,就要确定参数存放在哪个寄存器中。每种机制都有其优缺点,且与使用的编译语言有关。</p><p>(1)利用栈传递参数</p><p>栈是一种“后进先出”的存储区,栈顶指针esp指向栈中第1个可用的数据项。在调用函数时,调用者依次把参数压入栈,然后调用函数。函数被调用以后,在栈中取得数据并进行计算。函数计算结束以后,由调用者或者函数本身修改栈,使栈恢复原样(即平衡栈数据)。<br>在参数的传递中有两个很重要的问题:当参数个数多于1个时,按照什么顺序把参数压人栈?函数结束后,由谁来平衡栈?这些都必须有约定。这种在程序设计语言中为了实现函数调用而建立的协议称为调用约定(Calling Convention)。这种协议规定了函数中的参数传送方式、参数是否可变和由谁来处理栈等问题。不同的语言定义了不同的调用约定,常用的调用约定如下。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240715141839888.webp" alt="image-20240715141839888"></p><p>C规范(即__cdecl)函数的参数按照从右到左的顺序人栈,由调用者负责清除栈。__cdecl是C和C++程序的默认调用约定。C/C+和MFC程序默认使用的调用约定是、__cdecl,也可以在函数声明时加上__cdecl关键字来手动指定。<br>pascal规范按从左到右的顺序压参数人栈,要求被调用函数负责清除栈。<br>stdcall调用约定是Win32API采用的约定方式,有“标准调用”(Standard CALL)之意,采用C调用约定的入栈顺序和pascal调用约定的调整栈指针方式,即函数入口参数按从右到左的顺序入栈,并由被调用的函数在返回前清理传送参数的内存栈,函数参数的个数固定。由于函数体本身知道传入的参数个数,被调用的函数可以在返回前用一条retn指令直接清理传递参数的栈。在Win32API中,也有一些函数是__cdecl调用的,例如wsprintf。</p><p>为了了解不同类型约定的处理方式,我们来看一个例子。假设有调用函数test1(Parl,Par2,Par3)按__cdecl、pascal和stdeall的调用约定,其汇编代码如下。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">cdecl:</span><br><span class="line">push par3 ;参数从右到左传递</span><br><span class="line">push par2</span><br><span class="line">push par1</span><br><span class="line">call test1</span><br><span class="line">add esp,0c ;平衡栈</span><br></pre></td></tr></table></figure><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">pascal:</span><br><span class="line">push par1 ;参数从左到右传递</span><br><span class="line">push par2</span><br><span class="line">push par3</span><br><span class="line">call test1 ;函数内平衡栈</span><br></pre></td></tr></table></figure><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">stdcall:</span><br><span class="line">push par3 ;参数从右到左传递</span><br><span class="line">push par2</span><br><span class="line">push par1</span><br><span class="line">call test1 ;函数内平衡栈</span><br></pre></td></tr></table></figure><p>可以清楚地看到,__cdecl类型和stdcall类型先把右边的参数压人栈,pascal则相反。在栈平衡上,__cdecl类型由调用者用“add esp,0c”指令把12字节的参数空间清除,pascal和stdcall类型则由子程序负责清除。<br>函数对参数的存取及局部变量都是通过栈来定义的,非优化编译器用一个专门的寄存器(通常是ebp)对参数进行寻址。C、C+、pascal等高级语言的函数(子程序)执行过程基本一致,情祝如下。 </p><p>调用者将函数(子程序)执行完毕时应返回的地址、参数压入栈。</p><p>子程序使用“ebp指针+偏移量”对栈中的参数进行寻址并取出,完成操作。<br>子程序使用ret或retf指令返回。此时,CPU将eip置为栈中保存的地址,并继续执行它。</p><p>栈在整个过程中发挥着非常重要的作用。栈是一个先进后出的区域,只有一个出口,即当前栈顶。栈操作的对象只能是双操作数(占4字节)。例如,按stdcall约定调用函数test2(Parl,Par2)(有2个参数),其汇编代码大致如下。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">push par2 ;参数2</span><br><span class="line">push par1 ;参数1</span><br><span class="line">call test2 ;调用子程序test2</span><br><span class="line">{</span><br><span class="line">push ebp ;保护现场,原来的ebp指针</span><br><span class="line">mov ebp, esp ;设置新的ebp,使其指向栈顶</span><br><span class="line">mov eax, dword ptr [ebp+0c] ;调用参数2</span><br><span class="line">mov ebx, dword ptr [ebp+08] ;调用参数1</span><br><span class="line">sub esp, 8 ;若函数要使用局部变量,则要再栈中留出一部分空间</span><br><span class="line">......</span><br><span class="line">add esp, 8 ;释放局部变量占用的栈</span><br><span class="line">pop ebp ;恢复现场的ebp</span><br><span class="line">ret 8 ;返回(相当于ret;add esp,8)</span><br><span class="line"> ;ret后面的值等于参数个数乘4h</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>因为esp是栈指针,所以一般使用ebp来存取栈。其栈建立过程如下。</p><p>①此例函数中有2个参数,假设执行函数前栈指针的esp为K。<br>②根据stdcall调用约定,先将参数Par2压进栈,此时esp为K-04h。<br>③将参数Par1压人栈,此时esp为K-08h.</p><p>④参数入栈后,程序开始执行call指令。cal指令把返回地址压人栈,这时esp为K-0Ch。<br>⑤现在已经在子程序中了,可以开始使用ebp来存取参数了。但是,为了在返回时恢复ebp的值,需要使用“push ebp”指令来保存它,这时esp为K-10h。<br>⑥执行“mov ebp,.esp”指令,ebp被用来在栈中寻找调用者压人的参数,这时[ebp+8]就是参数1,[ebp+c]就是参数2。<br>⑦“sub esp,8”指令表示在栈中定义局部变量。局部变量1和局部变量2对应的地址分别是[ebp-4和[ebp-8]。函数结束时,调用“add esp,8”指令释放局部变量占用的栈。局部变量的作用域是定义该变量的函数 ,也就是说,当函数调用结束后局部变量便会消失。<br>⑧调用“ret 8”指令来平衡栈。在ret指令后面加一个操作数,表示在ret指令后给栈指针esp加上操作数,完成同样的功能。<br>处理完毕,就可以用ebp存取参数和局部变量了,这个过程如图所示。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240715151420270.webp" alt="image-20240715151420270"></p><p>此外,指令enter和leave可以帮助进行栈的维护。enter语句的作用就是“push ebp”“mov ebp,esp”“sub esp,xxx”,而leave语句则完成“add esp,xxx”“pop ebp”的功能。所以,上面的程序可以改成如下形式。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">enter xxxx,0 ;0表示创建xxxx空间来放置局部变量</span><br><span class="line">......</span><br><span class="line">leave ;恢复现场</span><br><span class="line">ret 8;返回</span><br></pre></td></tr></table></figure><p>在许多时候,编译器会按优化方式来编译程序,栈寻址稍有不同。这时,编译器为了节省ebp寄字器或尽可能减少代码以提高速度,会直接通过esp对参数进行寻址。esp的值在函数执行期间会发生变化,该变化出现在每次有数据进出栈时。要想确定对哪个变量进行了寻址,就要知道程序当前位置的esp的值,为此必须从函数的开始部分进行跟踪。<br>同样,对上例中的test2(Parl,Par2)函数,在VC6.0里将优化选项设置为“Maximize Speed’”。重新编译该函数,其汇编代码可能如下。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">push par2 ;参数2</span><br><span class="line">push par1 ;参数1</span><br><span class="line">call test2 ;调用子程序test2</span><br><span class="line">{</span><br><span class="line">mov eax, dword ptr [esp+04] ;调用参数1</span><br><span class="line">mov ebx, dword ptr [esp+08] ;调用参数1</span><br><span class="line">......</span><br><span class="line">ret 8 ;返回</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这时,程序就用esp来传递参数了。其栈建立情况如图所示,过程如下。<br>①假设执行函数前栈指针esp的值为K。<br>②根据stdcall调用约定,先将参数Par2压入栈,此时esp为K-04h。<br>③将Par1压入栈,此时esp为K-08h。<br>④参数入栈后,程序开始执行call指令。call指令把返回地址压人栈,这时esp为K-OCh。<br>⑤现在已经在子程序中了,可以使用esp来存取参数了。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240715152157868.webp" alt="image-20240715152157868"></p><p>(2)利用寄存器传递参数</p><p>寄存器传递参数的方式没有标准,所有与平台相关的方式都是由编译器开发人员制定的。尽管没有标准,但绝大多数编译器提供商都在不对兼容性进行声明的情况下遵循相应的规范,即Fastcall规范。Fastcall,顾名思义,特点就是快(因为它是靠寄存器来传递参数的)。</p><p>不同编译器实现的Fastcall稍有不同。Microsoft Visual C++编译器在采用Fastcall规范传递参数时,左边的2个不大于4字节(DWORD)的参数分别放在ecx和edx寄存器中,寄存器用完后就要使用栈,其余参数仍然按从右到左的顺序压入栈,被调用的函数在返回前清理传送参数的栈。浮点值,远指针和int64类型总是通过栈来传递的。而Borland Delphi/C+编译器在采用Fastcall规范传递参数时,左边的3个不大于4字节(DWORD)的参数分别放在eax、edx和ecx寄存器中,寄存器用完后,其余参数按照从左至右的PASCAL方式压人栈。</p><p>另有一款编译器Watcom C总是通过寄存器来传递参数,它严格为每一个参数分配一个寄存器,默认情况下第1个参数用eax,第2个参数用edx,第3个参数用ebx,第4个参数用ecx。如果寄存器用完,就会用栈来传递参数。Vatcom C可以由程序员指定任意一个寄存器来传递参数,因此,其参数实际上可能通过任何寄存器进行传递。来看一个用Microsoft Visual C++6.0编译的Fastcall调用实例,代码如下。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">int</span> __fastcall <span class="title function_">Add</span><span class="params">(<span class="type">char</span>, <span class="type">long</span>, <span class="type">int</span>, <span class="type">int</span>)</span>;</span><br><span class="line"></span><br><span class="line">main(<span class="type">void</span>)</span><br><span class="line">{</span><br><span class="line"> Add(<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>, <span class="number">4</span>);</span><br><span class="line"> <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="type">int</span> __fastcall <span class="title function_">Add</span><span class="params">(<span class="type">char</span> a, <span class="type">long</span> b, <span class="type">int</span> c, <span class="type">int</span> d)</span></span><br><span class="line"></span><br><span class="line">{</span><br><span class="line"> <span class="keyword">return</span> (a + b + c + d);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>使用Visual C++进行编译,将“Optimizations’”选项设置为“Default’”。编译后查看其反汇编代码,具体如下。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240715160336803.webp" alt="image-20240715160336803"></p><p>Add()函数<br><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240715160347231.webp" alt="image-20240715160347231"></p><p>另一个调用规范thiscall也用到了寄存器传递参数。thiscall是C++中的非静态类成员函数的默认调用约定,对象的每个函数隐含接收this参数。采用thiscall约定时,函数的参数按照从右到左的顺序人栈,被调用的函数在返回前清理传送参数的栈,仅通过ecx寄存器传送一个额外的参数——this指针。</p><p>定义一个类,并在类中定义一个成员函数,代码如下</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string"><stdio.h></span></span></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">CSum</span></span></span><br><span class="line"><span class="class">{</span></span><br><span class="line">public:</span><br><span class="line"> <span class="type">int</span> <span class="title function_">Add</span><span class="params">(<span class="type">int</span> a, <span class="type">int</span> b)</span> <span class="comment">//实际Add原型具有如下形式:Add(this,int a,int b)</span></span><br><span class="line"> {</span><br><span class="line"> <span class="keyword">return</span> (a + b);</span><br><span class="line"> }</span><br><span class="line">};</span><br><span class="line"></span><br><span class="line"><span class="type">void</span> <span class="title function_">main</span><span class="params">()</span></span><br><span class="line">{</span><br><span class="line"> CSum sum;</span><br><span class="line"> sum.Add(<span class="number">1</span>, <span class="number">2</span>);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>使用Visual C++进行编译,将“Optimizations”选项设置为“”Default’”。编译后查看其反汇编代码。<br><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240715162537397.webp" alt="image-20240715162537397"></p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240715162640456.webp" alt="image-20240715162640456"></p><p>(3)名称修饰约定</p><p>为了允许使用操作符和函数重载,C++编译器往往会按照某种规则改写每一个入口点的符号名,从而允许同一个名字(具有不同的参数类型或者不同的作用域)有多个用法且不会破坏现有的基于C的链接器。这项技术通常称为名称改编(Name Mangling)或者名称修饰(Name Decoration)。许多C++编译器厂商都制定了自己的名称修饰方案。<br>在VC++中,函数修饰名由编译类型(C或C++)、函数名、类名、调用约定、返回类型、参数等因素共同决定。关于名称修饰的内容很多,下面仅简单谈一下常见的C编译、C++编译函数名的修饰。</p><p>C编译时函数名修饰约定规则如下。</p><p>stdcall调用约定在输出函数名前面加一个下画线前缀,在后面加一个“@”符号及其参数的字节数,格式为“functionname(@number”。<br>__cdecl调用约定仅在输出函数名前面加一个下画线前缀,格式为”_functionname”。<br>Fastcall调用约定在输出函数名前面加一个“@”符号,在后面加一个“@”符号及其参数的字节数,格式为“@functionname@number”。</p><p>它们均不改变输出函数名中的字符大小写。这和pascall调用约定不同。pascal约定输出的函数名不能有任何修饰且全部为大写。<br>C++编译时函数名修饰约定规则如下。</p><p>stdcall调用约定以“”标识函数名的开始,后跟函数名;在函数名后面,以“@@YG”标识参数表的开始,后跟参数表;参数表的第1项为该函数的返回值类型,其后依次为参数的数据类型,指针标识在其所指数据类型前:在参数表后面,以“@Z”标识整个名字的结束(如果该函数没有参数,则以“Z”标识结束)。其格式 为“? functionname@@YC****@Z”或“?functionname@@YG*XZ。<br>__cdecl调用约定规则与上面的stdcall调用约定规则相同,只是参数表的开始标识由“@@YG”变成了“@@YA”。<br>Fastcall调用约定规则与上面的stdcall调用约定规则相同,只是参数表的开始标识由“@@YG”变成了“@@YT”。</p><p><code>函数的返回值:</code>函数被调用执行后,将向调用者返回1个或多个执行结果,称为函数返回值。返回值最常见的形式是return操作符,还有通过参数按传引用方式返回值、通过全局变量返回值等。</p><p>(1)用return操作符返回值</p><p>在一般情况下,函数的返回值放在eax寄存器中返回,如果处理结果的大小超过eax寄存器的容量,其高32位就会放到edx寄存器中,例如下面这段C程序。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">MyAdd(<span class="type">int</span> x, <span class="type">int</span> y)</span><br><span class="line">{</span><br><span class="line"> <span class="type">int</span> temp;</span><br><span class="line"> temp = x + y;</span><br><span class="line"> <span class="keyword">return</span> temp;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这是一个普通的函数,它将两个整数相加。这个函数有两个参数,并使用一个局部变量临时保存结果。其汇编实现代码所下。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240715173428513.webp" alt="image-20240715173428513"></p><p>(2)通过参数按传引用方式返回值</p><p>给函数传递参数的方式有两种,分别是传值和传引用。进行传值调用时,会建立参数的一份副本,并把它传给调用函数,在调用函数中修改参数值的副本不会影响原始的变量值。传引用调用允许调用函数修改原始变量的值。调用某个函数,皆把变量的地址传递给函数时,可以在函数中用间接引用运算符修改调用函数内存单元中该变量的值。例如,在调用函数max时,需要用两个地址(或者两个指向整数的指针)作为参数,函数会将结果较大的数放到参数a所在的内存单元地址中返回,代码如下。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string"><stdio.h></span></span></span><br><span class="line"></span><br><span class="line"><span class="type">void</span> <span class="title function_">max</span><span class="params">(<span class="type">int</span> *a, <span class="type">int</span> *b)</span>;</span><br><span class="line">main( )</span><br><span class="line">{</span><br><span class="line"> <span class="type">int</span> a = <span class="number">5</span>, b = <span class="number">6</span>;</span><br><span class="line"> max(&a, &b);</span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"a、b中较大的数是%d"</span>, a); <span class="comment">//将最大的数显示出来</span></span><br><span class="line"> <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="type">void</span> <span class="title function_">max</span><span class="params">( <span class="type">int</span> *a, <span class="type">int</span> *b)</span></span><br><span class="line">{</span><br><span class="line"> <span class="keyword">if</span>(*a < *b)</span><br><span class="line"> *a = *b; <span class="comment">//经比较后,将较大的数放到a变量之中</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>其汇编代码如下</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240716141604216.webp" alt="image-20240716141604216"></p><ol start="3"><li>数据结构</li></ol><p>数据结构是计算机存储、组织数据的方式。在进行逆向分析时,确定数据结构以后,算法就很容易得到了。有些时候,事情也会反过来,即根据特定算法来判断数据结构。本节将讨论常见的数据结构及它们在汇编语言中的实现方式。</p><p><code>局部变量:</code>局部变量(Local Variables)是函数内部定义的一个变量,其作用域和生命周期局限于所在函数内。使用局部变量使程序模块化封装成为可能。从汇编的角度来看,局部变量分配空间时通常会使用栈和寄存器。</p><p>(1)利用栈存放局部变量</p><p>局部变量在栈中进行分配,函数执行后会释放这些栈,程序用“sub esp,8”语句为局部变量分配空间,用[ehp-xxxx]寻址调用这些变量,而参数调用相对于ebp偏移量是正的,即[ebp+xxxx],因此在逆向时比较容易区分。编译器在优化模式时,通过esp寄存器直接对局部变量和参数进行寻址。当函数退出时,用“add esp,8”指令平衡栈,以释放局部变量占用的内存。有些编译器(例如Delphi)通过给esp加一个负值来进行内存的分配。另外,编译器可能会用“push reg’”指令取代“sub esp,4”指令,以节省几字节的空间。<br>局部变量分配与清除栈的形式如表所示。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240716144036292.webp" alt="image-20240716144036292"></p><p>下面这个实例是用“push reg””指令来取代“sub esp,4”指令的。 </p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">int</span> <span class="title function_">add</span><span class="params">(<span class="type">int</span> x, <span class="type">int</span> y)</span>;</span><br><span class="line"><span class="type">int</span> <span class="title function_">main</span><span class="params">(<span class="type">void</span>)</span></span><br><span class="line">{</span><br><span class="line"> <span class="type">int</span> a = <span class="number">5</span>, b = <span class="number">6</span>;</span><br><span class="line"> add(a, b);</span><br><span class="line"> <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="type">int</span> <span class="title function_">add</span><span class="params">(<span class="type">int</span> x, <span class="type">int</span> y)</span></span><br><span class="line">{</span><br><span class="line"> <span class="type">int</span> z;</span><br><span class="line"> z = x + y;</span><br><span class="line"> <span class="keyword">return</span>(z);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>用Microsoft Visual C+6.0进行编译,不进行忧化,其汇编代码如下。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240716144329185.webp" alt="image-20240716144329185"></p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240716144347269.webp" alt="image-20240716144347269"></p><p>add函数里不存在“sub esp,n”之类的指令,程序通过“push ecx’”指令来开辟一块栈,然后用[ebp-04]来访问这个局部变量。局部变量的起始值是随机的,是其他函数执行后留在栈中的垃圾数据,因此需要对其进行初始化。初始化局部变量有两种方法:一种是通过mov指令为变量赋值,例如“mov[ebp-04],5”;另一种是使用push指令直接将值压人栈,例如“push 5”。</p><p>(2)利用寄存器存放局部变量</p><p>除了栈占用2个寄存器,编译器会利用剩下的6个通用寄存器尽可能有效地存放局部变量,这样可以少产生代码,提高程序的效率。如果寄存器不够用,编译就会将变量放到栈中。在进行逆向分析时要注意,局部变量的生存周期比较短,必须及时确定当前寄存器的变量是哪个变量。</p><p><code>全局变量:</code>全局变量作用于整个程序,它一直存在,放在全局变量的内存区中。局部变量则存在于函数的栈区中,函数调用结束后便会消失。在大多数程序中,常数一般放在全局变量中,例如一些注册版标记、测试版标记等。在大多数情况下,在汇编代码中识别全局变量比在其他结构中要容易得多。全局变量通常位于数据区块(.data)的一个固定地址处,当程序需要访问全局变量时,一般会用一个固定的硬编码地址直接对内存进行寻址,示例如下。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">mov eax, dword ptr [40874c0h]</span><br></pre></td></tr></table></figure><p>全局变量可以被同一文件中的所有函数修改,如果某个函数改变了全局变量的值,就能影响其他函数(相当于函数间的传递通道),因此,可以利用全局变量来传递参数和函数返回值等。全局变量在程序的整个执行过程中占用内存单元,而不像局部变量那样在需要时才开辟内存单元。</p><p>看一个利用全局变量传递参数的实例,代码如下。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">int</span> z;</span><br><span class="line"></span><br><span class="line"><span class="type">int</span> <span class="title function_">add</span><span class="params">(<span class="type">int</span> x, <span class="type">int</span> y)</span>;</span><br><span class="line"><span class="type">int</span> <span class="title function_">main</span><span class="params">(<span class="type">void</span>)</span></span><br><span class="line">{</span><br><span class="line"> <span class="type">int</span> a = <span class="number">5</span>, b = <span class="number">6</span>;</span><br><span class="line"> z = <span class="number">7</span>;</span><br><span class="line"> add(a, b);</span><br><span class="line"> <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="type">int</span> <span class="title function_">add</span><span class="params">(<span class="type">int</span> x, <span class="type">int</span> y)</span></span><br><span class="line">{</span><br><span class="line"> <span class="keyword">return</span>(x + y + z);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>用Microsoft Visual C+6.0进行编译,但不进行优化,其汇编代码如下。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240716151649239.webp" alt="image-20240716151649239"></p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240716151700772.webp" alt="image-20240716151700772"></p><p>用PEID打开编译后的程序,查看区块,区块信息如图所示。全局变量004084C0h在.data区块中,该区块的属性为可读写。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240716153017410.webp" alt="image-20240716153017410"></p><p>使用这种对内存直接寻址的硬编码方式,比较容易识别出这是一个全局变量。一般编译器会将全局变量放到可读写的区块里(如果放到只读区块里,就是一个常量)。<br>与全局变量类似的是静态变量,它们都可以按直接方式寻址等。不同的是,静态变量的作用范围是有限的,仅在定义这些变量的函数内有效。</p><p><code>数组:</code>数组是相同数据类型的元素的集合,它们在内存中按顺序连续存放在一起。在汇编状态下访问数组一般是通过基址加变址寻址实现的。<br>请看下面这个数组访问实例。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string"><stdio.h></span></span></span><br><span class="line"></span><br><span class="line"><span class="type">int</span> <span class="title function_">main</span><span class="params">(<span class="type">void</span>)</span></span><br><span class="line">{</span><br><span class="line"> <span class="type">static</span> <span class="type">int</span> a[<span class="number">3</span>] = {<span class="number">0x11</span>, <span class="number">0x22</span>, <span class="number">0x33</span>};</span><br><span class="line"> <span class="type">int</span> i, s = <span class="number">0</span>, b[<span class="number">3</span>];</span><br><span class="line"> <span class="keyword">for</span>(i = <span class="number">0</span>; i < <span class="number">3</span>; i++)</span><br><span class="line"> {</span><br><span class="line"> s = s + a[i];</span><br><span class="line"> b[i] = s;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">for</span>(i = <span class="number">0</span>; i < <span class="number">3</span>; i++)</span><br><span class="line"> {</span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"%d\n"</span>, b[i]);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>用Microsoft Visual C+6.0进行编译,将优化选项设置为“Maximize Speed”,其汇编代码如下。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240716153737093.webp" alt="image-20240716153737093"></p><p>在内存中,数组可存在于栈、数据段及动态内存中。本例中的a[ ]数组就保存在数据段.data中,其寻址用“基址+编移量”实现。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">mov edi, dword_407030[eax]</span><br></pre></td></tr></table></figure><p>这种间接寻址一般出现在给一些数组或结构赋值的情况下,其寻址形式一般是[基址+偏移量]。基址可以是常量,也可以是寄存器,为定值。根据n值的不同,可以对结构中的相应单元赋值。<br>b[ ]数组放在栈中,这些栈在编译时分配。数组在声明时可以直接计算偏移地址,针对数组成员寻址是采用实际的偏移量完成的。</p><ol start="4"><li>虚函数</li></ol><p>C++是一门支持面向对象的语言,为面向对象的软件开发提供了丰富的语言支持。要想高效、正确地使用C++中的继承、多态等语言特性,就必须对这些特性的底层实现有一定的了解。其实,C++的对象模型的核心概念并不多,最重要的概念是虚函数。虚函数是在程序运行时定义的函数。虚函数的地址不能在编译时确定,只能在调用即将进行时确定。所有对虚函数的引用通常都放在一个专用数组——虚函数表(Virtual Table,VTBL)中,数组的每个元素中存放的就是类中虚函数的地址。调用虚函数时,程序先取出虚函数表指针(Virtual Table Pointer,VPTR),得到虚函数表的地址,再根据这个地址到虚函数表中取出该函数的地址,最后调用该函数,整个过程如图所示。VPTR是一个虚函数表指针,所有虚函数的入口都列在虚函数表(VTBL)中。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240716164056624.webp" alt="image-20240716164056624"></p><p>将实例thiscall.exe的普通成员函数改为虚函数调用,看看VC是如何处理虚函数的,代码如下。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string"><stdio.h></span></span></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">CSum</span></span></span><br><span class="line"><span class="class">{</span></span><br><span class="line">public:</span><br><span class="line"> virtual <span class="type">int</span> <span class="title function_">Add</span><span class="params">(<span class="type">int</span> a, <span class="type">int</span> b)</span></span><br><span class="line"> {</span><br><span class="line"> <span class="keyword">return</span> (a + b);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> virtual<span class="type">int</span><span class="title function_">Sub</span><span class="params">(<span class="type">int</span> a, <span class="type">int</span> b )</span></span><br><span class="line"> {</span><br><span class="line"> <span class="keyword">return</span> (a - b);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line">};</span><br><span class="line"></span><br><span class="line"><span class="type">void</span> <span class="title function_">main</span><span class="params">()</span></span><br><span class="line">{</span><br><span class="line"> CSum*pCSum = new CSum ;</span><br><span class="line"></span><br><span class="line"> pCSum->Add(<span class="number">1</span>, <span class="number">2</span>);</span><br><span class="line"> pCSum->Sub(<span class="number">1</span>, <span class="number">2</span>);</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>用Microsoft Visual C+6.0进行编译,将优化选项设置为“Maximize Speed”,其汇编代码如下。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240716164408970.webp" alt="image-20240716164408970"></p><p>这段代码先调用new函数分配class所需的内存((new函数是由IDA来识别的)。调用成功后,eax保存分配到内存的指针,然后将对象实例指向CSum类虚函数表(VTBL)004050A0h。004050A0h处的数据如图所示。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240716165459471.webp" alt="image-20240716165459471"></p><p>里面有两组数据</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240716175940740.webp" alt="image-20240716175940740"></p><p>查看这两个指针的内容</p><p>add()</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240716180007092.webp" alt="image-20240716180007092"></p><p>sub()</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240716180447954.webp" alt="image-20240716180447954"></p><p>原来虚函数是通过指向虚函数表的指针间接地加以调用的。程序仍以ecx作为this指针的载体传递给虚成员函数,并利用两次间接寻址得到虚函数的正确地址从而执行,代码如下。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240716180545916.webp" alt="image-20240716180545916"></p><ol start="5"><li>控制语句</li></ol><p>在高级语言中,用IF-THEN-ELSE、SWITCH-CASE等培句来构建程序的判新流程,不仅条理清楚,而且可维护性强。但是,其汇编代码比较复杂,我们会看到cmp等指令后面跟着各类跳转指令,例如jz、jnz。识别关键跳转是软件解密的一项重要技能,许多软件用一个或多个跳转实现了注册或非注册功能。</p><p><code>IF-THEN-ELSE语句</code></p><p>将语句IF-THEN-ELSE编译成汇编代码后,整数用cmp指令进行比较,浮点值用fcom、fcomp等指令进行比较。将语句IF-THEN-ELSE编译后,其汇编代码形式通常如下。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">cmp a,b</span><br><span class="line">jz(jnz) xxx</span><br></pre></td></tr></table></figure><p>cmp指令不会修改操作数。两个操作数相减的结果会影响处理的几个标志,例如零标志、进位标志、符号标志和溢出标志。jz等指令就是条件跳转指令,根据a、b的值决定跳转方向。实际上,在许多情况下编译器都使用tesl或or之类较短的逻辑指令来替换cmp指令,形式通常为“test eax,,eax”。如果eax的值为0,则其逻辑与运算结果为0,设置ZF为1,否则设置ZF为0。我们来看一个实例,代码如下。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string"><stdio.h></span></span></span><br><span class="line"></span><br><span class="line"><span class="type">int</span> <span class="title function_">main</span><span class="params">(<span class="type">void</span>)</span></span><br><span class="line">{</span><br><span class="line"> <span class="type">int</span> a, b = <span class="number">5</span>;</span><br><span class="line"> <span class="built_in">scanf</span>(<span class="string">"%d"</span>, &a);</span><br><span class="line"> <span class="keyword">if</span>(a == <span class="number">0</span>)</span><br><span class="line"> a = <span class="number">8</span>;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> a + b;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>用Microsoft Visual C+6.0进行编译,将优化选项设置为“Maximize Speed’”,其汇编代码如下</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240717101451631.webp" alt="image-20240717101451631"></p><p><code>SWITCH-CASE语句</code><br>SWITCH语句是多分支选择语句。编译后的SWITCH语句,其实质就是多个IF-THEN语句的嵌套组合。编译器会将SWITCH语句编译成一组由不同的关系运算组成的语句。我们来看一个例子,代码如下。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string"><stdio.h></span></span></span><br><span class="line"></span><br><span class="line"><span class="type">int</span> <span class="title function_">main</span><span class="params">(<span class="type">void</span>)</span></span><br><span class="line">{</span><br><span class="line"> <span class="type">int</span> a;</span><br><span class="line"> <span class="built_in">scanf</span>(<span class="string">"%d"</span>, &a);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">switch</span>(a)</span><br><span class="line"> {</span><br><span class="line"> <span class="keyword">case</span> <span class="number">1</span> :</span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"a=1"</span>);</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> <span class="keyword">case</span> <span class="number">2</span> :</span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"a=2"</span>);</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> <span class="keyword">case</span> <span class="number">10</span>:</span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"a=10"</span>);</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> <span class="keyword">default</span> :</span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"a=default"</span>);</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>用Microsoft Visual C++6.0进行编译,但不进行优化,其反汇编代码如下。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240717102015303.webp" alt="image-20240717102015303"></p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240717102027311.webp" alt="image-20240717102027311"></p><p>如果编译时设置优化选项为“Maximize Speed”,其汇编代码如下。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240717103859379.webp" alt="image-20240717103859379"></p><p>编译器在优化时用“dec eax””指令代替cmp指令,使指令更短、执行速度更快。而且,在优化后,编译器会合理排列switch后面的各个case节点,以最优方式找到需要的节点。</p><p>如果各case的取值表示一个算术级数,那么编译器会利用一个跳转表(ump Table)来实现,示例如下。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string"><stdio.h></span></span></span><br><span class="line"></span><br><span class="line"><span class="type">int</span> <span class="title function_">main</span><span class="params">(<span class="type">void</span>)</span></span><br><span class="line">{</span><br><span class="line"> <span class="type">int</span> a;</span><br><span class="line"> <span class="built_in">scanf</span>(<span class="string">"%d"</span>, &a);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">switch</span>(a)</span><br><span class="line"> {</span><br><span class="line"> <span class="keyword">case</span> <span class="number">1</span> :</span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"a=1"</span>);</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> <span class="keyword">case</span> <span class="number">2</span> :</span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"a=2"</span>);</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> <span class="keyword">case</span> <span class="number">3</span>:</span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"a=3"</span>);</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> <span class="keyword">case</span> <span class="number">4</span>:</span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"a=4"</span>);</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> <span class="keyword">case</span> <span class="number">5</span>:</span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"a=5"</span>);</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> <span class="keyword">case</span> <span class="number">6</span>:</span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"a=6"</span>);</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> <span class="keyword">case</span> <span class="number">7</span>:</span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"a=7"</span>);</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">default</span> :</span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"a=default"</span>);</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>由编译器编译后,“jmp dword ptr[4*eax+004010B0]”指令相当于switch(a),根据eax的值进行索引,计算出指向相应case处理代码的指针。其汇编代码如下。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240717111255179.webp" alt="image-20240717111255179"></p><p><code>转移指令机器码的计算</code></p><p>在软件分析过程中,经常需要计算转移指令机器码或修改指定的代码。虽然有许多工具可以完成这项工作,但掌握其原理和技巧仍然很有必要。</p><p>根据转移距离的不同,转移指令有如下类型。</p><p>短转移(Short Jump):无条件转移和条件转移的机器码均为2字节,转移范围是-128~127字节。<br>长转移(Long Jump):无条件转移的机器码为5字节,条件转移的机器码为6字节。这是因为,条件转移要用2字节表示其转移类型(例如je、jg、js),其他4字节表示转移偏移量,而无条件转移仅用1字节就可表示其转移类型(jmp),其他4字节表示转移偏移量。</p><p>子程序调用指令(call):call指令调用有两类。一类调用是我们平时经常接触的,类似于长转移;另一类调用的参数涉及寄存器、栈等值,比较复杂,例如“call dword ptr[eax+2]”。<br>条件转移指令的转移范围是16位模式遗留下来的。当时,为了使代码紧凑一些,CPU开发人员只给目的地址分配了1字节,这样就将跳转的长度限制在255字节之内。<br>表中列出了常用的转移指令机器码,通过该表就可根据转移偏移量计算出转移指令的机器码了。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240717115416996.webp" alt="image-20240717115416996"></p><p>有两个因素可以制约转移指令机器码,一个是表中列出的转移类型,另一个是转移的位移量。</p><p>(1)短转移指令机器码计算实例<br>代码段中有一条无条件转移指令,具体如下。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">.....</span><br><span class="line">401000 jmp 401005</span><br><span class="line">.....</span><br><span class="line">401005 xor eax,eax</span><br></pre></td></tr></table></figure><p>无条件短转移的机器码形式为“EBxx”,其中EB00h~EB7Fh是向后转移,EB8Oh~EBFFh是向前转移。该转移指令的机器语言及用位移量来表示转向地址的方法如图所示。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240717150704615.webp" alt="image-20240717150704615"></p><p>可以看出,位移量为3h,CPU执行”jmp401005”指令后eip的值为00401002h,执行“(EIP)←(EIP)+位移量”指令,就会跳转到00401005h处,即“jmp401005”指令的机器码形是“EB 03” 。也就是说,转移指令的机器码形式是</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">位移量 = 目的地址 - 起始地址 - 跳转指令本身的长度</span><br><span class="line">转移指令机器码 = 转移类别机器码 + 位移量</span><br></pre></td></tr></table></figure><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240717150711035.webp" alt="image-20240717150711035"></p><p>(2)长转移指令机器码计算实例</p><p>在代码段中有一条无条件转移指令,具体如下。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">.....</span><br><span class="line">401000 jmp 402398</span><br><span class="line">.....</span><br><span class="line">402398 xor eax,eax</span><br></pre></td></tr></table></figure><p>无条件长转移指令的长度是5字节,机器码是“E9”。根据上面的公式,此例中转移的位移量为<em>00402398h-00401000h-5h=00001393h</em></p><p>如图所示,00001393弘在内存中以双字(32位)存储。存储时,低位字节存入低地址,高位字节存人高地址,也就是说,“00001393”以相反的顺序存入,形成了“93130000”的存储形式。</p><p>上面两个实例演示了转移指令向后转移(由低地址到高地址)的计算方法,向前转移(由高地址到低地址)的计算方法与此相同。<br>在代码段中有一条向前转移的无条件转移指令,具体如下。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">.....</span><br><span class="line">401000 xor eax,eax</span><br><span class="line">.....</span><br><span class="line">402398 jmp 401000</span><br></pre></td></tr></table></figure><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240717152059367.webp" alt="image-20240717152059367"></p><p><code>条件设置指令</code></p><p>条件设置指令的形式是“SETcc r/m8”,其中“r/m8”表示8位寄存器或单字节内存单元。</p><p>条件设置指令根据处理器定义的16种条件测试一些标志位,把结果记录到目标操作数中。当条件满足时,目标操作数置1,否则置0。这16种条件与条件转移指令jcc中的条件是一样的,如表所示。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240717152300624.webp" alt="image-20240717152300624"></p><p>条件设置指令可以用来消除程序中的转移指令。在C语言里,经常会见到执行如下功能的语句。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">c = (a < b)? c1:c2;</span><br></pre></td></tr></table></figure><p>如果允许出现条件分支,编译器会产生如下代码或者类似的代码。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">cmp a,b</span><br><span class="line">mov eax,c1</span><br><span class="line">jl L1</span><br><span class="line">mov eax,c2</span><br></pre></td></tr></table></figure><p><code>循环语句</code></p><p>循环是高级语言中可以进行反向引用的一种语言形式,其他类型的分支语句(例如IF-THEN-EISE等)都是由低地址向高地址区域引用的。通过这一点可以方便地将循环语句识别出来。<br>如果确定某段代码是循环代码,就可分析其计数器。一般将ecx寄存器作为计数器,也有用其他方法来控制循环的,例如“test eax,eax”指令。一段最简单的循环代码如下。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">xor ecx,ecx</span><br><span class="line">:0044000</span><br><span class="line">inc ecx ;计数</span><br><span class="line">cmp ecx,05 ;循环6次</span><br><span class="line">jbe 0044000 ;重复</span><br></pre></td></tr></table></figure><p>再来看一段比较复杂的循环,例如下面这段C程序。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">int</span> <span class="title function_">main</span><span class="params">(<span class="type">void</span>)</span></span><br><span class="line">{</span><br><span class="line"> <span class="type">int</span> sum = <span class="number">0</span>, i = <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">for</span>(i = <span class="number">0</span>; i <= <span class="number">100</span>; i++)</span><br><span class="line"> sum = sum + i;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>用Microsoft Visual C.+6.0进行编译,但不进行优化,其反汇编代码如下。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240717155355514.webp" alt="image-20240717155355514"></p><p>如果编译时设置优化选项为“Maximize Speed”,看看汇编代码是如何变化的</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240717160558920.webp" alt="image-20240717160558920"></p><p><code>数学运算符</code></p><p>高级语言中的运算符范围很广,这里只介绍整数的加、减、乘、除运算。如果编译器没有进行优化,则这些运算符很容易理解,下面主要介绍经编译器优化的运<br>算符。</p><p>(1)整数的加减法</p><p>在一般情况下,整数的加法和减法会分别被编译成add和sub指令。在进行编译优化时,很多人喜欢用lea指令来代替add和sub指令。lea指令允许用户在1个时钟内完成对c=a+b+78h的计算,其中a、b与c都是在有寄存器的情况下才有效的,会被编译成“lea c,[a+b+78]”指令。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string"><stdio.h></span></span></span><br><span class="line"></span><br><span class="line"><span class="type">int</span> <span class="title function_">main</span><span class="params">(<span class="type">void</span>)</span></span><br><span class="line">{</span><br><span class="line"> <span class="type">int</span> a, b;</span><br><span class="line"> <span class="comment">//scanf("%d",&a);</span></span><br><span class="line"> <span class="comment">//scanf("%d",&b);</span></span><br><span class="line"></span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"%d"</span>, a + b + <span class="number">0x78</span>);</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>用Microsoft Visual C++6.0进行编译,设置优化选项为“Maximize Speed”,反汇编代码如下。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240717162434386.webp" alt="image-20240717162434386"></p><p>在这段代码中,lea指令是一条纯算术指令,它的实际意义等价于edx=ecx+eax+78h。</p><p>(2)整数的乘法</p><p>乘法运算符一般被编译成mul、imul指令,这些指令的运行速度比较慢。编译器为了提高代码的效率,倾向于使用其他指令来完成同样的计算。如果一个数是2的幂,那么会用左移指令shl来实现乘法运算。另外,加法对于提高3、5、6、7、9等数的乘法运算效率非常有用,示例如下。例如,“eax*5”可以写成“lea eax,[eax+4*eax]”。lea指令可以实现寄存器乘以2、4或8的运算。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string"><stdio.h></span></span></span><br><span class="line"></span><br><span class="line"><span class="type">int</span> <span class="title function_">main</span><span class="params">(<span class="type">void</span>)</span></span><br><span class="line">{</span><br><span class="line"> <span class="type">int</span> a;</span><br><span class="line"></span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"%d %d %d"</span>, a * <span class="number">11</span> + <span class="number">4</span>, a * <span class="number">9</span>, a * <span class="number">2</span>);</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>用Microsoft Visual C++6.0进行编译,设置优化选项为“Maximize Speed”,其反汇编代码如下。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240717162759003.webp" alt="image-20240717162759003"></p><p>(2)整数的除法</p><p>除法运算符一般被编译成div、idiv指令。除法运算的代价是相当高的,大概需要比乘法运算多消耗10倍的CPU时钟。</p><p>如果被除数是一个未知数,那么编译器会使用div指令,程序的执行效率将会下降。<br>除数/被除数有一个是常量的情况就复杂很多。编译器将使用一些技巧来更有效地实现除法运算。如果除数是2的幂,那么可以用处理速度较快的移位指令“shr a,n”来替换。移位指令只需花费1个时钟,其中a是被除数,n是基数2的指数。shr指令适合进行无符号数计算。若进行符号数计算,则使用sar指令。当然,也会根据一定的算法,用乘法运算来代替除法运算。<br>我们来看一个除法运算实例,代码如下。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string"><stdio.h></span></span></span><br><span class="line"></span><br><span class="line"><span class="type">int</span> <span class="title function_">main</span><span class="params">(<span class="type">void</span>)</span></span><br><span class="line">{</span><br><span class="line"> <span class="type">int</span> a;</span><br><span class="line"> <span class="built_in">scanf</span>(<span class="string">"%d"</span>, &a);</span><br><span class="line"></span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"%d "</span>, a / <span class="number">11</span>);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>用Microsoft Visual C+6.0进行编译,但不进行优化,其反汇编代码如下。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240717164448183.webp" alt="image-20240717164448183"></p><p>除法指令需要使用符号扩展指令cdq,其作用是把eax寄存器中的数视为有符号的数,将其符号位(即eax的最高位)扩展到edx寄存器中,即若eax的最高位是1,则执行后edx的每个位都是1h,edx=FFFFFFFFh;若eax的最高位是0,则执行后edx的每个位都是0,edx=00000000h。这样,就把eax中32位带符号的数变成了edx:eax中64位带符号的数,满足了64位运算指令的需要,但转换后的值没有变化。<br>编译器在优化时,会用乘法运算代替除法运算,这样能提高数倍的效率。不过,对逆向分析来说,这样的代码较难理解。<br>用于优化的公式比较多,最常用的就是倒数相乘,举例如下。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240717165252186.webp" alt="image-20240717165252186"></p><p>用Microsoft Visual C+6.0进行编译,设置优化选项为“Maximize Speed”,其反汇编代码如下。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240717165419718.webp" alt="image-20240717165419718"></p><p>这段代码就是一个简单的除法运算,编译器优化后的代码比一个v指令长,但运行速度提高</p><p>了3倍。还有很多除法优化算法,不同编译器采取的方法也有所不同。<br> <code>文本字符串</code></p><p>字符的识别和分析是软件逆向的一个重要步骤,特别是在序列号分析过程中,经常会遇到各类字符操作。</p><p>(1)字符串存储格式</p><p>在程序中,一般将字符串作为字符数组来处理。但是,不同的编程语言,其字符存储格式是不同的。常见的字符串类型有C字符串、PASCAL字符串等。</p><p>C字符串:也称“ASCIIZ字符串”,广泛应用于Windows和UNIX操作系统中,“Z”表示其以“\0”为结束标志。“\0”代表ASCII码为0的字符,如图所示。ASCⅡ码为0的字符不是可以显示的字符,而是“空操作符”。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240717170343394.webp" alt="image-20240717170343394"></p><p>DOS字符串:在DOS中,输出行的函数以“$”字符作为终止字符</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240717170417827.webp" alt="image-20240717170417827"></p><p>PASCAL字符串:没有终止符,但在字符串的头部定义了1字节,用于指示当前字符串的长度。由于只用了1字节来表示字符串的长度,字符串不能超过255个字符,如图所示。字符串中的每个字符都属于AnsiChar类型(标准字符类型)</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240717170508999.webp" alt="image-20240717170508999"></p><p>Delphi字符串:为克服传统PASCAL字符串的局限性,32位Delphi增加了对长字符串的支持。</p><p>●双字节Delphi字符串:表示长度的字段扩展为2字节,使字符串的最大长度值达到65535,</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240717170600034.webp" alt="image-20240717170600034"></p><p>●四字节Delphi字符串:表示长度的字段扩展为4字节,使字符串长度达到4GB。目前这种字符类型很少使用。</p><p>(2)字符寻址指令</p><p>80x86系统支持寄存器直接寻址与寄存器间接寻址等模式。与字符指针处理相关的指令有mov、lea等。<br>mov指令将当前指令所在内存复制并放到目的寄存器中,其操作数可以是常量,也可以是指针</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">mov eax, [401000h]</span><br><span class="line">mov eax, [ecx]</span><br></pre></td></tr></table></figure><p>“lea”的意思是“装入有效地址”(Load Effective Address),它的操作数就是地址,所以“lea eax,[addr]”就是将表达式addr的值放入eax寄存器,示例如下。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">lea eax, [401000h]</span><br></pre></td></tr></table></figure><p>lea指令右边的操作数表示一个近指针,指令“lea eax,[401000h]”与“mov eax,401000h”是等价的。<br>在计算索引与常量的和时,编译器一般将指针放在第1个位置,而不考虑它们在程序中的顺序,例如以下初始化代码。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">mov dword ptr [eax+8],67452301</span><br><span class="line">mov dowrd ptr [eax+c],EFCDAB89</span><br></pre></td></tr></table></figure><p>编译器不仅广泛地使用lea指令来传递指针,而且经常用lea指令来计算常量的和,其等价于add指令。也就是说,“lea eax,[eax+8]”等价于“add eax,8”。不过,lea指令的效率远高于add指令,这种技巧可以使多个变量的求和在1个指令周期内完成,同时可以通过任何寄存器将结果返回。</p><p>(3)字母大小写转换</p><p>大写字母的ASCII码范围是41h一5Ah,小写字母的ASCⅡ码范围是6Ih~7Ah,它们之间的转换方式就是将原ASCII码的值加/减20h。</p><p>如下汇编代码的功能是将小写字母转换成大写字母。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">Labe101:</span><br><span class="line">mov al, byte ptr [edx]</span><br><span class="line">cmp al, 61</span><br><span class="line">jb Labe102</span><br><span class="line">cmp al, 7A</span><br><span class="line">ja Labe102</span><br><span class="line">sub al, 20</span><br><span class="line">Labe102:</span><br><span class="line">mov byte ptr [esi] , al</span><br><span class="line">inc edx</span><br><span class="line">inc esi</span><br><span class="line">dec ebx</span><br><span class="line">test ebx,ebx</span><br><span class="line">jnz Labe101</span><br></pre></td></tr></table></figure><p>这段代码先用“a”来作比较,如果小于“a”,可能是大写字母或其他字符,再与“z”作比较,如果大于“z”,则不是小写字母,不处理。如果确定是小写字母,则将该字符的ASCII码减20,即可转换成大写字母。<br>还有一种转换大小写字母的方法。如图所示是大写字母“A”与小写字母“:”的二进制形式。如果第5位是0,则是大写字母;如果第5位是1,则是小写字母。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240717175153232.webp" alt="image-20240717175153232"></p><p>因此,如下代码也能实现大小写字母的转换。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">proc near</span><br><span class="line">lea bx, title+1</span><br><span class="line">mov cx, 31</span><br><span class="line">B20:</span><br><span class="line">mov ah, [bx]</span><br><span class="line">cmp ah, 61h</span><br><span class="line">jb B30</span><br><span class="line">cmp ah, 7Ah</span><br><span class="line">jb B30</span><br><span class="line">and ah, 1101 1111b</span><br><span class="line">mov [bx],ah</span><br><span class="line">B30:</span><br><span class="line">inc bx</span><br><span class="line">loop B20</span><br><span class="line">ret</span><br></pre></td></tr></table></figure><p>(4)计算字符串的长度</p><p>在高级语言里,会有特定的函数来计算字符串的长度,例如C语言中经常用strlen()函数计算字符串的长度。strlen()函数在优化编译模式下的汇编代码如下。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">mov ecx, FFFFFFFF</span><br><span class="line">sub eax, eax</span><br><span class="line">repnz</span><br><span class="line">scasb</span><br><span class="line">not ecx</span><br><span class="line">dec ecx</span><br><span class="line">je xxxxxx</span><br></pre></td></tr></table></figure><p>这段代码使用串扫描指令scasb把AL的内容与edi指向的附加段中的字节逐一比较,把edi指向的字符串长度保存在ecx中。</p><h3 id="二,64位软件逆向技术"><a href="#二,64位软件逆向技术" class="headerlink" title="二,64位软件逆向技术"></a>二,64位软件逆向技术</h3><ol><li>寄存器</li></ol><p>x64是AMD64与Intel64的合称,是指与现有x86兼容的64位CPU。在64位系统中,内存地址为64位。x64位环境下寄存器有比较大的变化,如图所示。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240718095231721.webp" alt="image-20240718095231721"></p><p>x64系统通用寄存器的名称,第1个字母从“E”改为“R”(例如“RAX”),大小扩展到64位,数量增加了8个(R8~R15),扩充了8个128位XMM寄存器(在64位程序中,XMM寄存器经常被用来优化代码)。64位寄存器与x86下的32位寄存器兼容,例如RAX(64位)、EAX(低32)AX(低16位)、AL(低8位)和AH(8~15位)。x64新扩展的寄存器高低位访问,使用WORD、BYTE、DW0RD后缀,例如R8(64位)、R8D(低32位)、R8W(低16位)和R8B(低8位),如图所示。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240718095242960.webp" alt="image-20240718095242960"></p><ol start="2"><li>函数</li></ol><p>在64位Windows操作系统上可以运行32位和64位程序。</p><p>(1)栈平衡</p><p>栈是程序在内存中的一块特殊区域,它的存储特点是先进后出,即先存储进去的数据最后被释放。RSP用来保存当前的栈顶指针,每8字节的栈空间用来保存一个数据。在汇编指令中,通常使用push和pop来人栈和出栈。栈在内存中的结构如图所示。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240718095438530.webp" alt="image-20240718095438530"></p><p>栈中存储的数据主要包括局部变量、函数参数、函数返回地址等。每当调用一个函数时,就会根据函数的需要申请相应的栈空间。当函数调用完成时,就需要释放刚才申请的栈空间,保证栈顶与函数调用前的位置一致。这个释放栈空间的过程称为栈平衡。<br>为什么需要栈平衡?在程序运行过程中,栈内存空间会被各函数重复利用,如果函数调用只申请栈空间而不释放它,那么随着函数调用次数的增加,栈内存很快就会耗光,程序会因此无法正常运行。平衡栈的操作,目的是保证函数调用后的栈顶位置和函数调用前的位置一致,这样就可以重复利用栈的内存空间了。过多或者过少地释放栈空间都会影响其他函数对栈空间数据的操作,进而造成程序错误或者崩溃。需要注意的是,在x64环境下,某些汇编指令对栈顶的对齐值有要求,因此,Visual Studio编译器在申请栈空间时,会尽量保证栈顶地址的对齐值为16(可以被16整除)。如果在逆向过程中发现申请了栈空间却不使用的情况,可能就是为了实现对齐。</p><p>(2)启动函数</p><p>程序在运行时,先执行初始化函数代码,再调用main函数执行用户编写的代码。在上节中已经分析了用VC生成的32位程序启动代码,此处不再重复。下<br>面通过一个例子来说明如何快速定位64位程序的入口函数(main和WinMain)</p><p>如下示例程序调用printf函数输出了一串字符。用Visual C++2010将其编译成x64程序,用IDA Pro打开示例程序,在函数窗口中找到名为“start’”的函数,如图4.17所示。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">"stdafx.h"</span></span></span><br><span class="line"></span><br><span class="line"><span class="type">int</span> _tmain(<span class="type">int</span> argc, _TCHAR *argv[])</span><br><span class="line">{</span><br><span class="line"></span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"Hello World!"</span>);</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>ida打开,找start函数</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240718144647188.webp" alt="image-20240718144647188"></p><p>根进“jmp__tmainCRTStartup”,持续翻页,就能找到main函数了,代码如下。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240718144818594.webp" alt="image-20240718144818594"></p><p>也可以直接在图中找到名为“main”的函数,快速定位到main函数内部。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240718145648515.webp" alt="image-20240718145648515"></p><p>在编译器的项目属性中选择“C/C++”+“所有选项”→“运行库”→“多线程DLL(MD)”选项,IDA就会显示main符号。当运行库设置为“/MT(多线程)”时,IDA不会显示main符号。在第2种情况下,可以通过代码特征定位main函数,当main函数执行完成时,通常会调用库函数exit退出进程。根据此特征,在入口代码中找到第1处“call cs:exit”代码,该处上面的第1个“call”通常就是main函数(在更高版本的Visual Studio中,可能该处上面的第1个“call”内部的“call”才是main函数)。</p><p>(3)调用约定</p><p>x86应用程序的函数调用有stdcall、_edecl、Fastcall等方式,但x64应用程序只有1种寄存器快速调用约定。前4个参数使用寄存器传递,如果参数超过4个,多余的参数就放在栈里,人栈顺序为从右到左,由函数调用方平衡栈空间。前4个参数存放的寄存器是固定的,分别是第1个参数RCX、第2个参数RDX、第3个参数R8、第4个参数R9,其他参数从右往左依次人栈。任何大于8字节或者不是1字节、2字节、4字节、8字节的参数必须由引用来传递(地址传递)。所有浮点参数的传递都是使用XMM寄存器完成的,它们在XMM0、XMM1、XMM2和XMM3中传递,如表所示。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240718152316864.webp" alt="image-20240718152316864"></p><p>函数的前4个参数虽然使用寄存器来传递,但是栈仍然为这4个参数预留了空间(32字节),为方便描述,这里称之为预留栈空间。在x64环境里,前4个参数使用寄存器传递,因此在函数内部这4个寄存器就不能使用了,相当于函数少了4个可用的通用寄存器。当函数功能比较复杂时,这可能导致寄存器不够用。为了避免这个问题,可以使用预留栈空间,方法是函数调用者多申请32字节的栈空间,当函数寄存器不够用时,可以把寄存器的值保存到刚才申请的栈空间中。预留栈空间由函数调用者提前申请。由函数调用者负责平衡栈空间。</p><p>函数调用后,寄存器和内存的情况如图所示。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240718153558509.webp" alt="image-20240718153558509"></p><p>(4)参数传递</p><p>下面通过一个实例来分析x64环境下参数的传递。</p><p><code>2个参数的传递</code></p><p>当参数个数小于4时的示例代码如下。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">"stdafx.h"</span></span></span><br><span class="line"></span><br><span class="line"><span class="type">int</span> <span class="title function_">Add</span><span class="params">(<span class="type">int</span> nNum1, <span class="type">int</span> nNum2)</span></span><br><span class="line">{</span><br><span class="line"> <span class="keyword">return</span> nNum1 + nNum2;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="type">int</span> _tmain(<span class="type">int</span> argc, _TCHAR *argv[])</span><br><span class="line">{</span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"%d\r\n"</span>, Add(<span class="number">1</span>, <span class="number">2</span>));</span><br><span class="line"> <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>用Visual Studio2010进行编译后,用IDA Pro打开示例的Debug版。main函数的代码如下。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240718160923875.webp" alt="image-20240718160923875"></p><p>Add()</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240718164142490.webp" alt="image-20240718164142490"></p><p>在本实例中,两个参数通过寄存器进行传递,第1个参数为ECX、第2个参数为EDX,但在栈中仍为它们预留了4个参数大小的空间,申请了32字节(20h=32d=4个参数×8字节)的预留栈空间。<br>这个例子中使用的是Debug版的汇编代码。当程序被编译成Release版时,函数参数的传递并无本质区别。当开启内联函数扩展编译优化选项时,函数可能会进行内联扩展优化,编译器会在编译时将可计算结果的变量转换成常量,代码如下。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240718164511982.webp" alt="image-20240718164511982"></p><p><code>4个以上参数的传递</code></p><p>再来分析一下参数多于4个时程序是如何传递的,代码如下。</p> <figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">"stdafx.h"</span></span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="type">int</span> <span class="title function_">Add</span><span class="params">(<span class="type">int</span> nNum1, <span class="type">int</span> nNum2, <span class="type">int</span> nNum3, <span class="type">int</span> nNum4, <span class="type">int</span> nNum5, <span class="type">int</span> nNum6)</span></span><br><span class="line">{</span><br><span class="line"> <span class="keyword">return</span> nNum1 + nNum2 + nNum3 + nNum4 + nNum5 + nNum6;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="type">int</span> _tmain(<span class="type">int</span> argc, _TCHAR *argv[])</span><br><span class="line">{</span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"%d\r\n"</span>, Add(<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>, <span class="number">4</span>, <span class="number">5</span>, <span class="number">6</span>));</span><br><span class="line"> <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>编译后,打开Debug版程序。main函数的代码如下。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240719093718853.webp" alt="image-20240719093718853"></p><p>add()</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240719094023558.webp" alt="image-20240719094023558"></p><p>从本例中可以看出,如果参数多于4个,前4个参数通过寄存器传递,从第5个参数开始使用栈传递,指令为“mov dword ptr[rsp+20h],5”。由于栈为前4个参数预留了大小相同的栈空间,申请了32字节(20h=32d=4个参数×8字节)的预留栈空间,第5个参数从栈的[rsp+20h]处开始保存。参数使用的栈空间由函数调用者负责平衡。</p><p><code>参数为结构体</code></p><p>当参数为结构体时,参数的大小就有可能超过8字节。先看一下当参数为结构体,并且结构体大小不超过8字节的时候,参数是如何传递的,代码如下。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">"stdafx.h"</span></span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">tagPoint</span></span></span><br><span class="line"><span class="class">{</span></span><br><span class="line"> <span class="type">int</span> x1;</span><br><span class="line"> <span class="type">int</span> y1;</span><br><span class="line">};</span><br><span class="line"></span><br><span class="line"><span class="type">void</span> <span class="title function_">fun</span><span class="params">(tagPoint pt)</span></span><br><span class="line">{</span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"x=%d y=%d\r\n"</span>, pt.x1, pt.y1);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="type">int</span> _tmain(<span class="type">int</span> argc, _TCHAR *argv[])</span><br><span class="line">{</span><br><span class="line"> tagPoint pt = { <span class="number">1</span>, <span class="number">2</span> };</span><br><span class="line"> fun(pt);</span><br><span class="line"> <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>编译后,打开Debug版程序。main函数的汇编代码如下。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240719094348185.webp" alt="image-20240719094348185"></p><p>fun()</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240719095509920.webp" alt="image-20240719095509920"></p><p>如果参数为结构体且结构体小于8字节,在传递结构体参数时,应直接把整个结构体的内容放在寄存器中。在函数里,通过访问寄存器的高32位和低32位来分别访问结构体的成员。在进行逆向分析时,应根据函数对参数的使用特征来判断函数参数是否为一个结构体类型。<br>下面看看当结构体大小超过8字节时参数是如何传递的,代码如下。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">"stdafx.h"</span></span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">tagPoint</span></span></span><br><span class="line"><span class="class">{</span></span><br><span class="line"> <span class="type">int</span> x1;</span><br><span class="line"> <span class="type">int</span> y1;</span><br><span class="line"> <span class="type">int</span> x2;</span><br><span class="line"> <span class="type">int</span> y2;</span><br><span class="line">};</span><br><span class="line"><span class="type">void</span> <span class="title function_">fun</span><span class="params">(tagPoint pt)</span></span><br><span class="line">{</span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"x1=%d y1=%d x2=%d y2=%d\r\n"</span>, pt.x1, pt.y1, pt.x2, pt.y2);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="type">int</span> _tmain(<span class="type">int</span> argc, _TCHAR *argv[])</span><br><span class="line">{</span><br><span class="line"> tagPoint pt = { <span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>, <span class="number">4</span> };</span><br><span class="line"> fun(pt);</span><br><span class="line"> <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>编译后,打开Debug版程序。main函数的汇编代码如下。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240719100555724.webp" alt="image-20240719100555724"></p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240719100615166.webp" alt="image-20240719100615166"></p><p>fun()</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240719101044631.webp" alt="image-20240719101044631"></p><p>通过以上代码可以看出,如果参数是结构体且大于8字节,在传递参数时,会先把结构内容复制到栈空间中,再把结构体地址当成函数的参数来传递(引用传递)。在函数内部通过“结构体地址+偏移”的方式访问结构体的内容。</p><p><code>thiscall传递</code></p><p>在VC++环境下,还有一种特殊的调用约定,叫作thiscall。它是C++类的成员函数调用约定</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">"stdafx.h"</span></span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">CAdd</span></span></span><br><span class="line"><span class="class">{</span></span><br><span class="line">public:</span><br><span class="line"> <span class="type">int</span> <span class="title function_">Add</span><span class="params">(<span class="type">int</span> nNum1, <span class="type">int</span> nNum2)</span></span><br><span class="line"> {</span><br><span class="line"> <span class="keyword">return</span> nNum1 + nNum2;</span><br><span class="line"> }</span><br><span class="line">};</span><br><span class="line"></span><br><span class="line"><span class="type">int</span> _tmain(<span class="type">int</span> argc, _TCHAR *argv[])</span><br><span class="line">{</span><br><span class="line"> CAdd Object;</span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"%d\r\n"</span>, Object.Add(<span class="number">1</span>, <span class="number">2</span>));</span><br><span class="line"> <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>编译后,用IDA Pro打开Debug版程序。main函数的汇编代码如下。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240719102836270.webp" alt="image-20240719102836270"></p><p>add()</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240719103043350.webp" alt="image-20240719103043350"></p><p>通过这个实例我们可以知道,类的成员函数调用、参数传递方式与普通函数没有很大的区别。唯一的区别是,成员函数调用会隐含地传递一个this指针参数。</p><p>(5)函数返回值</p><p>在64位环境下,使用RAX寄存器来保存函数返回值。返回值类型由浮点类型使用MMX0寄存器返回。RAX寄存器可以保存8字节的数据。当返回值大于8字节时,可以将栈空间的地址作为参数间接访问,进而达到目的。</p><ol start="3"><li>数据结构</li></ol><p>x64程序的数据结构和x86类似,主要是对局部变量、全局变量、数组等的识别。</p><p>(1)局部变量</p><p>局部变量是函数内部定义的变量,其存放的内存区域为栈区,其生命周期为进入函数时分配、函数返回时释放。下面通过一个例子来看看应用程序是如何分配和释放局部变量空间,以及如何访问局部变量的,代码如下。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">"stdafx.h"</span></span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="type">int</span> _tmain(<span class="type">int</span> argc, _TCHAR *argv[])</span><br><span class="line">{</span><br><span class="line"> <span class="type">int</span> nNum1 = argc;</span><br><span class="line"> <span class="type">int</span> nNum2 = <span class="number">2</span>;</span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"%d\r\n"</span>, nNum1 + nNum2);</span><br><span class="line"> <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>编译后,用DA Pro打开Debug版程序。main函数的汇编代码如下。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240719103752739.webp" alt="image-20240719103752739"></p><p>函数在入口处申请了预留栈空间和局部变量空间,指令为“sub rsp,30h”。其中,从rsp+0h到rsp+20h为32字节预留栈空间,从rsp+20h到rsp+30h为局部变量空间。也就是说,预留栈空间在低地址,局部变量空间在高地址。当应用程序编译为Release版时,因为程序访问寄存器比访问内存时有更高的性能,所以编译器会尽可能使用寄存器来存放局部变量,当寄存器不够用时才把局部变量存放在栈空间中。</p><p>(2)全局变量</p><p>全局变量的地址在编译期就会固定下来,因为一般会用固定的地址去访问全局变量。下面通过一个例子来看看如何访问全局变量。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">"stdafx.h"</span></span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="type">int</span> g_nNum1;</span><br><span class="line"><span class="type">int</span> g_nNum2;</span><br><span class="line"><span class="type">int</span> _tmain(<span class="type">int</span> argc, _TCHAR *argv[])</span><br><span class="line">{</span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"%d\r\n"</span>, g_nNum1 + g_nNum2);</span><br><span class="line"> <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>编泽后,用IDA Pro打开Debug版程序。main函数的汇编代码如下。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240719105026165.webp" alt="image-20240719105026165"></p><p>全局变量的地址也是先定义的在低地址,后定义的在高地址。根据此特征可以还原全局变量在源代码中的定义顺序。</p><p>(3)数组</p><p>数组是相同数据类型的集合,以线性方式连续存储在内存中。数组中的数据在内存中的存储是线性连续的,数组中的数据是从低地址到高地址顺序排列的。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">int</span> ary[<span class="number">4</span>]={<span class="number">1</span>,<span class="number">2</span>,<span class="number">3</span>,<span class="number">4</span>}</span><br></pre></td></tr></table></figure><p>此数组中有4个类型为it的集合,其占用内存大小为:</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">sizeof</span>(类型)*个数</span><br></pre></td></tr></table></figure><p>此数组大小为:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sizeof(int)*4</span><br></pre></td></tr></table></figure><p>因此,此数组占用的内存空间为16字节。假设ary数组的首地址为0x1000,那么数组元素ary[0]的地址为0x1000,数组元素ary[1]的地址为0x1004,数组元素ary[2]的地址为0x1008,数组元素ary[3]的地址为0x100C。</p><p><code>数组寻址公式</code></p><p>编译器在访问数组元素时,要先定位数组元素的地址,再访问数组元素的内容。编译器采用数组寻址公式定位一个数组元素的地址。因此,掌握数组寻址公式,在进行软件逆向分析时可以快速识别编译器访问的是数组哪个元素。先来看一维数组的寻址公式,具体如下。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">数组元素的地址=数组首地址+sizeof(数组类型)×下标</span><br></pre></td></tr></table></figure><p>多维数组也可以看成一个一维数组。例如,有一个二维数组int ary[2][3],可以将其看成一个一 维数组,其数组元素类型为一维数组。假设数组的首地址为0x1000,现在想访问ary[1][2],下标1访问第1个一维数组,下标2访问第2个一维数组。根据一维数组寻址公式0x1000+sizeof(int[3])×1=0x1000+0xC=0x100C,得到第1个一维数组的数组元素。因为数组元素的类型为一维数组,所以需要再次寻址。通过计算,0x100C+sizeof(int)×2=0x100C+0x8=0x1014,因此ary[1][2]的数组元素的地址为0x1014。把两个一维数组的寻址公式加起来就是二维寻址公式,具体如下。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">数组元素的地址=数组首地址+sizeof(一维数组类型)×下标1+sizeof(数组类型)×下标2</span><br></pre></td></tr></table></figure><p><code>一维数组</code></p><p>下面通过一个例子来看看编译器是如何访问一维数组元素的,代码如下。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">"stdafx.h"</span></span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="type">int</span> g_ary[<span class="number">4</span>] = { <span class="number">4</span>, <span class="number">5</span>, <span class="number">6</span>, <span class="number">7</span> };</span><br><span class="line"><span class="type">int</span> _tmain(<span class="type">int</span> argc, _TCHAR *argv[])</span><br><span class="line">{</span><br><span class="line"> <span class="type">int</span> ary[<span class="number">4</span>] = { <span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>, <span class="number">4</span> };</span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"%d %d\r\n"</span>, ary[<span class="number">2</span>], ary[argc]);</span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"%d %d\r\n"</span>, g_ary[<span class="number">3</span>], g_ary[argc]);</span><br><span class="line"> <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>编译后,用IDA Pro打开Debug版程序。main函数的汇编代码如下。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240719110453781.webp" alt="image-20240719110453781"></p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240719110516690.webp" alt="image-20240719110516690"></p><p>从本例中可以看出,编译器访问数组的代码就是利用数组寻址公式去访问的。当访问的数组下标为常量时,编译器会根据数组一维寻址公式直接计算出数组相对于数组首地址的偏移。例如,[g_ary+3*4]会直接被优化成【g_ary+12]。如果数组下标未知(下标通常是变量),就会用一维数组寻址公式去定位数组元素。</p><p>访问二维数组时使用的也是数组寻址公式。数组的特征总结如下。</p><pre><code> [数组首地址+n] [数组首地址+寄存器×n]</code></pre><ol start="4"><li>控制语句</li></ol><p>(1)if</p><p>if语句是分支结构的重要组成部分。if语句的功能是对表达式的结果进行判定,根据表达式结果的真假跳转到对应的语句块执行。其中,“真”表示表达式结果非 0,“假”表示表达式结果为0,示例如下。因为逻辑问题,编译器生成的汇编代码会对表达式的结果进行取反操作。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">"stdafx.h"</span></span></span><br><span class="line"><span class="type">int</span> _tmain(<span class="type">int</span> argc, _TCHAR *argv[])</span><br><span class="line">{</span><br><span class="line"> <span class="keyword">if</span> (argc > <span class="number">1</span>)</span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"argc > 1\r\n"</span>);</span><br><span class="line"> <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240719142305964.webp" alt="image-20240719142305964"></p><pre><code> 特征识别:首先会有一个jxx指令用于向下跳转,且跳转的目的近end中没有jmp指令。根据以上特征,把jxx指令取反后,即可还原if语句的代码,如图所示。</code></pre><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240719143636466.webp" alt="image-20240719143636466"></p><p>图形识别:在逆向分析工具中,为了方便地表示跳转的位置,使用虚线箭头表示条件跳转jxx,使用实线箭头表示无条件跳转jmp。if语句中有一个jxx跳转,因此会有一个向下的虚线箭头,看到此图形即可判断其为f语句,虚线箭头之间的代码为if代码。IDA中的if语句图形如图所示。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240719143906057.webp" alt="image-20240719143906057"></p><p>(2)if……else 语句</p><p>if….eles语句比f语句多出了一个“else”,当if表达式结果为真时跳过else分支语句块,当if表达式结果为假时跳转到else分支语句块中,示例如下。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">"stdafx.h"</span></span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="type">int</span> _tmain(<span class="type">int</span> argc, _TCHAR *argv[])</span><br><span class="line">{</span><br><span class="line"> <span class="keyword">if</span> (argc == <span class="number">1</span>)</span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"argc == 1\r\n"</span>);</span><br><span class="line"> <span class="keyword">else</span></span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"argc != 1\r\n"</span>);</span><br><span class="line"> <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>编译后,用IDA Pro打开Debug版程序。main函数的汇编代码如下。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240719152431961.webp" alt="image-20240719152431961"></p><p>特征识别:首先会有一个jxx指令用于向下跳转,且跳转的目的else中有jmp指令。else代码的结尾没有jmp指令,else的代码也会执行if_else_end的代码。根据以上特征,把jxx指令取反后,即可还原if…else语句的代码,如图所示。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240719152536016.webp" alt="image-20240719152536016"></p><p>图形识别:因为f语句中有一个jxx指令用于向下跳转,所以会有一个向下的虚线箭头;又因为else语句中有jmp跳转,所以虚线箭头中会有一个向下的实线箭头。看到此图形即可判断其为if…..else语句,虚线箭头之间的代码为f代码,实线箭头之间的代码为else代码。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240719152700102.webp" alt="image-20240719152700102"></p><p>(3)if…else if…else语句</p><p>在if….else语句的“else”之后再嵌套if语句,就形成了一个多分支结构,示例如下。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">"stdafx.h"</span></span></span><br><span class="line"><span class="type">int</span> _tmain(<span class="type">int</span> argc, _TCHAR *argv[])</span><br><span class="line">{</span><br><span class="line"> <span class="keyword">if</span> (argc > <span class="number">2</span>)</span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"argc > 2\r\n"</span>);</span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">if</span> (argc == <span class="number">2</span>)</span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"argc == 2\r\n"</span>);</span><br><span class="line"> <span class="keyword">else</span></span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"argc <= 1\r\n"</span>);</span><br><span class="line"> <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>编译后,用IDA Pro打开Debug版程序。main函数的汇编代码如下。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240719153050349.webp" alt="image-20240719153050349"></p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240719153107909.webp" alt="image-20240719153107909"></p><p>特征识别:首先会有一个jxx指令用于向下跳转,且跳转的目的else if中有jmp指令。else if的跳转目的else中有jmp指令,且else代码的结尾没有jmp指令,所有jmp的目标地址一致。根据以上特征,把jxx指令取反,即可还原if…else if…else语句的代码,如图所示。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240719153244088.webp" alt="image-20240719153244088"></p><p>图形识别:因为if语句中有一个jxx指令用于向下跳转,所以会有一个向下的虚线箭头;又因为else_if中有jm即跳转,所以虚线箭头中会有一个向下的实线箭头。在else if代码中有个jxx跳转和一个jmp即跳转,因此有一个虚线箭头和一个实线箭头,它们相互交叉。看到此图形即可判断其为f…else if…else语句,第1个虚线箭头之间的代码为if代码,第2个虚线箭头之间的代码为else if代码,最后一个实线箭头之间的代码为else代码。如果第2个虚线箭头和最后一个实线箭头的跳转目标地址一致,就是没有else,那么该语句就是一个if….else if控制语句。可以将不同的控制语句相互嵌套,并使用不同的工具来观察图形的样式。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240719153415917.webp" alt="image-20240719153415917"></p><p>(4)switch-case语句</p><p>switch是比较常用的多分支结构。switch语句通常比if语句有更高的效率。编译器有多种优化方案,在进行逆向分析时要注意识别。当switch分支数小于6时会直接用if…else语句来实现,当switch分支数大于等于6时编译会进行优化,示例如下。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">"stdafx.h"</span></span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="type">int</span> _tmain(<span class="type">int</span> argc, _TCHAR *argv[])</span><br><span class="line">{</span><br><span class="line"> <span class="keyword">switch</span> (argc)</span><br><span class="line"> {</span><br><span class="line"> <span class="keyword">case</span> <span class="number">1</span>:</span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"argc == 1"</span>);</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> <span class="keyword">case</span> <span class="number">2</span>:</span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"argc == 2"</span>);</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> <span class="keyword">case</span> <span class="number">3</span>:</span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"argc == 3"</span>);</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> <span class="keyword">case</span> <span class="number">6</span>:</span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"argc == 6"</span>);</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> <span class="keyword">case</span> <span class="number">7</span>:</span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"argc == 7"</span>);</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> <span class="keyword">case</span> <span class="number">8</span>:</span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"argc == 8"</span>);</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>编译后,用IDA Pro打开Debug版程序。main函数的汇编代码如下。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240719153755402.webp" alt="image-20240719153755402"></p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240719153809596.webp" alt="image-20240719153809596"></p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240719153826234.webp" alt="image-20240719153826234"></p><p>case表的代码如下</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240719154025871.webp" alt="image-20240719154025871"></p><p>当case≥6,且case值的间隔比较小时,编译器会采用case表的方式实现siwtch语句。这是编译器优化siwtch语句的一种方法,其优化原则就是避免使用if语句。编译器实现的思路是先把所有要跳转的case位置偏移放在一个一维数组的表中(这个表叫作case表),然后把case的值当成数组下标进行跳转,这样就可以避免使用if语句,从而提高性能了。</p><p>case表的结构体如表所示。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240719154147957.webp" alt="image-20240719154147957"></p><p>例如,switche(argc)只需要把argc-l当成case表的数组下标,得出偏移,直接跳转过去。为什么要把argc-1当成数组下标呢?直接把argc当成数组下标不行吗?看看如下switch代码。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">switch</span> (argc) {</span><br><span class="line"><span class="keyword">case</span> <span class="number">100</span>: <span class="built_in">printf</span>(<span class="string">"argc == 100"</span>); <span class="keyword">break</span>;</span><br><span class="line"><span class="keyword">case</span> <span class="number">101</span>: <span class="built_in">printf</span>(<span class="string">"argc == 200"</span>); <span class="keyword">break</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>如果把上面的代码做成case表,数组的项数是101项,而实际上只用了2项,其他项中填写的是switch结束地址偏移,这非常浪费内存空间。因此,将argc的值减I00,再做一个switch表,只要2项就够了。</p><p>当case项较多时,编译器直接用if语句来实现switch语句。为了减少if语句的判断次数,采用了另一种优化方案一判定树。将每个case值作为一个节点,从这些节点中找到一个中间值作为根节点,形成一棵二叉平衡树,以每个节点为判定值,大于和小于关系分别对应于左右子树,从而提高效率,减少if语句的判断次数,如图所示。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240719161328917.webp" alt="image-20240719161328917"></p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">"stdafx.h"</span></span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="type">int</span> _tmain(<span class="type">int</span> argc, _TCHAR *argv[])</span><br><span class="line">{</span><br><span class="line"> <span class="keyword">switch</span> (argc)</span><br><span class="line"> {</span><br><span class="line"> <span class="keyword">case</span> <span class="number">1</span>:</span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"argc == 1"</span>);</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> <span class="keyword">case</span> <span class="number">3</span>:</span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"argc == 3"</span>);</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> <span class="keyword">case</span> <span class="number">5</span>:</span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"argc == 5"</span>);</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> <span class="keyword">case</span> <span class="number">10</span>:</span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"argc == 10"</span>);</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> <span class="keyword">case</span> <span class="number">35</span>:</span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"argc == 35"</span>);</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> <span class="keyword">case</span> <span class="number">50</span>:</span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"argc == 50"</span>);</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> <span class="keyword">case</span> <span class="number">300</span>:</span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"argc == 300"</span>);</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>编译后,用IDA Pro打开Release版程序。main函数的汇编代码如下。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240719161612125.webp" alt="image-20240719161612125"></p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240719161642978.webp" alt="image-20240719161642978"></p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240719161652781.webp" alt="image-20240719161652781"></p><p>(5)转移指令机器码的计算</p><p><code>call/jmp direct</code></p><p>机器码的计算仍与x86应用程序类似,示例如下。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">00000001400018c1 E9 D2 00 00 00 jmp 140001998</span><br></pre></td></tr></table></figure><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">位移量=目的地址-起始地址-跳转指令长度=<span class="number">140001998</span>h<span class="number">-1400018</span>C1h<span class="number">-5</span>h=D2h</span><br><span class="line">转移指令机器码=转移类别机器码+位移量=<span class="string">"E9"</span>+<span class="string">"D2 00 00 00"</span>=<span class="string">"E9 D2 00 00 00"</span></span><br></pre></td></tr></table></figure><p><code>call/jmp memory direct</code></p><p>这种方式在x86和x64下稍有不同。在32位系统里,代码如下。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">004014F6 FF15 3C414200 CALL DWORD PTR DS:[42413C]</span><br></pre></td></tr></table></figure><p>“FF15 3C414200”这行指令用于调用某地址,其中“42413C”为绝对地址。x64应用程序使用相同的指令,但解析方法不同。</p><p>在64位系统里,指令地址由原来的4字节变为8字节。若x64也采用与x86相同的方式,FF15后跟着绝对地址,指令的长度就会增加。为了解决这个问题,在x64系统中,指令后面仍然是4字节指令,只不过该地址为“相对地址”,示例如下。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">00000001400018CB FF 15 B7 9A 00 00 CALL QWORD PTR CS: [14000B388]</span><br></pre></td></tr></table></figure><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">相对地址=<span class="number">14000B</span>388h<span class="number">-1400018</span>CBh-跳转指令长度=<span class="number">9</span>ABDh<span class="number">-6</span>h=<span class="number">9</span>AB7h</span><br><span class="line">机器码=<span class="string">"FF15"</span>+相对地址=FF15B79A0000h</span><br></pre></td></tr></table></figure><ol start="5"><li>循环语句</li></ol><p>在C+中有3种循环语法,分别为do、while、for。虽然它们完成的功能都是循环,但是每种语法有不同的执行流程。</p><p>(1)do</p><p>do循环的流程是:先执行语句块,再进行表达式判断。当表达式结果为真时,会继续执行语句块,示例代码如下。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">"stdafx.h"</span></span></span><br><span class="line"></span><br><span class="line"><span class="type">int</span> _tmain(<span class="type">int</span> argc, _TCHAR *argv[])</span><br><span class="line">{</span><br><span class="line"> <span class="type">int</span> nCount = <span class="number">0</span>;</span><br><span class="line"> <span class="keyword">do</span></span><br><span class="line"> {</span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"%d\r\n"</span>, nCount);</span><br><span class="line"> nCount++;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">while</span> (nCount < argc);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>编译后,用IDA Pro打开Debug版程序。main函数的汇编代码如下。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240719162820352.webp" alt="image-20240719162820352"></p><p>特征识别:首先会有一个jxx指令用于向上跳转(循环与if语句的最大区别就是循环可以向上跳转),且跳转的目的do…..while…stat语句中设有jxx跳转指令。根据以上特征,jxx指令不取反,即可还原do……while语句的代码,如图所示。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240719162915568.webp" alt="image-20240719162915568"></p><p>图形识别:因为do…..while语句中有一个jxx指令用于向上跳转,所以会有一个向上的虚线箭头。看到此图形即可判断其为do…while语句,虚线箭头之间的代码为do….while代码。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240719162939531.webp" alt="image-20240719162939531"></p><p>(2)while循环</p><p>while循环的流程是:先进行表达式判断,再执行语句块。当表达式结果为真时,会继续执行语句块,示例如下。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">"stdafx.h"</span></span></span><br><span class="line"></span><br><span class="line"><span class="type">int</span> _tmain(<span class="type">int</span> argc, _TCHAR *argv[])</span><br><span class="line">{</span><br><span class="line"> <span class="type">int</span> nCount = <span class="number">0</span>;</span><br><span class="line"> <span class="keyword">while</span> (nCount < argc)</span><br><span class="line"> {</span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"%d\r\n"</span>, nCount);</span><br><span class="line"> nCount++;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>编译后,用IDA Pro打开Debug版程序。maim函数的汇编代码如下。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240719163303777.webp" alt="image-20240719163303777"></p><p>循环的特点是会向低地址跳转。在while循环中出现的向低地址跳转的情况与do循环中的不同,while循环使用的是jmp跳转,while循环的jxx汇编指令需要取反。需要注意的是,while循环比do循环多一次f语句判断,因此性能上while循环不如do循环高。在Release版本中,编译器会把while循环优化成等价的do循环。</p><p>特征识别:首先会有一个jmp向上跳转指令,且跳转的目的while_stat下面有jxx跳转指令。while代码也会执行while_end的代码。根据以上特征,把jxx指令取反后,即可还原while语句的代码,如图所示。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240719163451739.webp" alt="image-20240719163451739"></p><p>图形识别:因为if语句中有一个jmp向上跳转指令,所以会有一个向上的实线箭头;又因为跳转的目的while_start下面有条件跳转指令,所以实线箭头内部会有一个向下的虚线箭头。看到此图形即可判断其为while语句,虚线箭头之间的代码为while代码。在Release版中,while语句会被优化成if加do…..while语句,因此图形会变成在外部有一个向下的虚线箭头,在虚线箭头内部有一个向上的虚线箭头。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240719163538860.webp" alt="image-20240719163538860"></p><p>(3)for循环</p><p>for语句由赋初值、循环条件、循环步长3条语句组成,示例如下。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">"stdafx.h"</span></span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="type">int</span> _tmain(<span class="type">int</span> argc, _TCHAR *argv[])</span><br><span class="line">{</span><br><span class="line"> <span class="keyword">for</span> (<span class="type">int</span> nCount = <span class="number">0</span>; nCount < argc; nCount++)</span><br><span class="line"> {</span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"%d\r\n"</span>, nCount);</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>编译后,用IDA Pro打开Debug版程序。main函数的汇编代码如下。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240719163826309.webp" alt="image-20240719163826309"></p><p>特征识别:for循环也会出现向上跳转的情况。与while循环不同的是,在这里前面多了一个jmp跳转。for循环的jxx汇编指令需要取反。根据以上特征,即可还原for循环语句的代码,如图所示。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240719163934411.webp" alt="image-20240719163934411"></p><p>图形识别:因为for语句前面比while语句多了一个jmp跳转,所以在图形中会比while语句多一个向下的实线箭头。在Release版中,while语句会被优化成if加do….while语句,因此图形会变成在外部有一个向下的虚线箭头,在虚线箭头内部有一个向上的虚线箭头。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240719164012079.webp" alt="image-20240719164012079"></p><ol start="6"><li>数学运算符</li></ol><p>计算机中的四则运算和数学中的四侧运算有些不同。四则运算符都有对应的汇编指令,这些指令在逆向分析过程中很容易识别。本节主要讨论在Release版本中由编译器优化后的四则运算。</p><p>(1)整数的加法与减法</p><p><code>加法与减法</code></p><p>加法对应的指令为add,减法对应的指令为sub。编译器在优化时经常使用lea指令来优化加法和减法,以缩短指令的执行周期,示例如下。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">"stdafx.h"</span></span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="type">int</span> _tmain(<span class="type">int</span> argc, _TCHAR *argv[])</span><br><span class="line">{</span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"%d\r\n"</span>, argc + <span class="number">3</span>);</span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"%d\r\n"</span>, argc - <span class="number">5</span>);</span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"%d\r\n"</span>, argc + argc + <span class="number">4</span>);</span><br><span class="line"> <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>编译后,用IDA Pro打开Release版程序。main函数的汇编代码如下。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240719164252464.webp" alt="image-20240719164252464"></p><p><code>常量折叠</code></p><p>常量折叠优化是指当表达式中出现2个以上常量进行计算的情况时,编译器可以在编译期间算出结果,用计算结果替换表达式,这样在程序运行期间就不需要计算,从而提高了程序的性能,示例如下。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">"stdafx.h"</span></span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="type">int</span> _tmain(<span class="type">int</span> argc, _TCHAR *argv[])</span><br><span class="line">{</span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"%d\r\n"</span>, argc + <span class="number">10</span> + <span class="number">2</span> * <span class="number">3</span>);</span><br><span class="line"> <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>编译后,用IDA Pro打开Release版程序。main函数的汇编代码如下。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240719164609847.webp" alt="image-20240719164609847"></p><p>编译器在编译期间直接把10+2×3的结果计算出来了。</p><p>(2)整数的乘法</p><p>乘法运算所对应的汇编指令分为有符号(imul)和无符号(mul)两种。乘法指令的执行周期较长,编译器在优化时经常使用Iea比例因子寻址来优化乘法指令,示例如下。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">"stdafx.h"</span></span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="type">int</span> _tmain(<span class="type">int</span> argc, _TCHAR *argv[])</span><br><span class="line">{</span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"%d\r\n"</span>, argc * <span class="number">4</span>);</span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"%d\r\n"</span>, argc * <span class="number">7</span>);</span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"%d\r\n"</span>, argc * <span class="number">9</span>);</span><br><span class="line"> <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>编译后,用IDA Pro打开Release版程序。main函数的汇编代码如下。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240719164824197.webp" alt="image-20240719164824197"></p><p>(3)整数的除法</p><p>除法指令的执行周期较长,因此编译器会尽可能使用其他汇编指令来代替除法指令,通常的优化方法是转换成等价移位运算或者乘法运算。但是,计算机中的除法和数学中的除法有些不同,计算机中的除法是取整除法,因此在移位时可能需要做一些修正。</p><p><code>有符号除法,除数为2^n</code></p><p>当除数为2^n时,编译器一般会进行移位优化,示例如下。数学优化公式为:如果x≥0,则x/2^n=x>>n;如果x≥0,则x/2^n=(x+(2^n-1))>>n</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">"stdafx.h"</span></span></span><br><span class="line"></span><br><span class="line"><span class="type">int</span> _tmain(<span class="type">int</span> argc, _TCHAR *argv[])</span><br><span class="line">{</span><br><span class="line"> <span class="type">long</span> <span class="type">long</span> nNum;</span><br><span class="line"> <span class="built_in">scanf</span>(<span class="string">"%ld"</span>, &nNum);</span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"%d\r\n"</span>, argc / <span class="number">4</span>);</span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"%d\r\n"</span>, nNum / <span class="number">8</span>);</span><br><span class="line"> <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>编译后,用IDA Pro打开Release版程序。main函数的汇编代码如下。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240719165407057.webp" alt="image-20240719165407057"></p><p>当遇到包含以上公式的汇编指令时,根据公式,第1个除法的n为2,因此有argc/4;第2个除法的n为3,因此有nNum/8。</p><p><code>有符号除法,除数为-2^n</code></p><p>当除数为-2^n时,与上一个示例相比多了求补的过程,示例如下。数学优化公式为:如果x≥0,则x/-2^n=-(x>>n);如果x≥0,则x/-2^n=-((x+(2^n-1))>>n)</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">"stdafx.h"</span></span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="type">int</span> _tmain(<span class="type">int</span> argc, _TCHAR *argv[])</span><br><span class="line">{</span><br><span class="line"> <span class="type">long</span> <span class="type">long</span> nNum;</span><br><span class="line"> <span class="built_in">scanf</span>(<span class="string">"%ld"</span>, &nNum);</span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"%d\r\n"</span>, argc / <span class="number">-2</span>);</span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"%d\r\n"</span>, nNum / <span class="number">-8</span>);</span><br><span class="line"> <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>编译后,用IDA Pro打开Release版程序。main函数的汇编代码如下。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240719165909018.webp" alt="image-20240719165909018"></p><p>(4)整数的取模</p><p>取模运算可以通过除法指令计算实现。但因为除法指令的执行周期较长,所以通常的优化方法是将其转换成等价的位运算或者除法运算,再由除法运算进行优化。</p><p><code>取模运算,除数为2^n</code></p><p>对x%2”取模来说,有如下两种数学优化公式。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240719170128770.webp" alt="image-20240719170128770"></p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">"stdafx.h"</span></span></span><br><span class="line"><span class="type">int</span> _tmain(<span class="type">int</span> argc, _TCHAR *argv[])</span><br><span class="line">{</span><br><span class="line"> <span class="type">long</span> <span class="type">long</span> nNum;</span><br><span class="line"> <span class="built_in">scanf</span>(<span class="string">"%ld"</span>, &nNum);</span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"%d\r\n"</span>, argc % <span class="number">8</span>);</span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"%d\r\n"</span>, nNum % <span class="number">32</span>);</span><br><span class="line"> <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>编译后,用IDA Pro打开Release版程序。main函数的汇编代码如下。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240719171506137.webp" alt="image-20240719171506137"></p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240719171521713.webp" alt="image-20240719171521713"></p><p><code>取模运算,除数为非2^n</code></p><p>对除数为非2的取模来说,编译器一般采用“余数=被除数-商×除数”的方法优化,数学优化公式为x%c=x-x/c*c,示例如下。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">"stdafx.h"</span></span></span><br><span class="line"><span class="type">int</span> _tmain(<span class="type">int</span> argc, _TCHAR *argv[])</span><br><span class="line">{</span><br><span class="line"> <span class="type">long</span> <span class="type">long</span> nNum;</span><br><span class="line"> <span class="built_in">scanf</span>(<span class="string">"%ld"</span>, &nNum);</span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"%d\r\n"</span>, argc % <span class="number">3</span>);</span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"%d\r\n"</span>, nNum % <span class="number">10</span>);</span><br><span class="line"> <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>编译后,用IDA Pro打开Release版程序。main函数的汇编代码如下。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240719171746490.webp" alt="image-20240719171746490"></p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240719171755036.webp" alt="image-20240719171755036"></p><ol start="7"><li>虚函数</li></ol><p>C++的三大核心机制是封装、继承、多态,而虚函数就是多态的一种体现。由于面向对象语言是供了强大的代码管理机制,越来越多的软件都采用了面向对象的程序设计。在软件逆向过程中,免会碰到使用面向对象思想设计的软件,而虚函数就是在实际软件逆向过程中的一种还原面向对代码的重要手段。本节将探讨编译器实现虚函数的原理。</p><p>(1)虚表</p><p>VC++实现虚函数功能的方式是做表,我们称这个表为虚表。什么时候会产生虚表呢?如果一个类至少有一个虚函数,那么编译器就会为这个类产生一个虚表。不同的类虚表不同,相同的类对象共享一个虚表。在实际逆向过程中如何识别虚表呢?我们先看一个例子。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">"stdafx.h"</span></span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">CVirtual</span></span></span><br><span class="line"><span class="class">{</span></span><br><span class="line">public:</span><br><span class="line"> CVirtual()</span><br><span class="line"> {</span><br><span class="line"> m_nMember1 = <span class="number">1</span>;</span><br><span class="line"> m_nMember2 = <span class="number">2</span>;</span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"CVirtual()\r\n"</span>);</span><br><span class="line"> }</span><br><span class="line"> virtual ~CVirtual()</span><br><span class="line"> {</span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"~CVirtual()\r\n"</span>);</span><br><span class="line"> }</span><br><span class="line"> virtual <span class="type">void</span> <span class="title function_">fun1</span><span class="params">()</span></span><br><span class="line"> {</span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"fun1()\r\n"</span>);</span><br><span class="line"> }</span><br><span class="line"> virtual <span class="type">void</span> <span class="title function_">fun2</span><span class="params">()</span></span><br><span class="line"> {</span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"fun2()\r\n"</span>);</span><br><span class="line"> }</span><br><span class="line">private:</span><br><span class="line"> <span class="type">int</span> m_nMember1;</span><br><span class="line"> <span class="type">int</span> m_nMember2;</span><br><span class="line">};</span><br><span class="line"></span><br><span class="line"><span class="type">int</span> <span class="title function_">main</span><span class="params">(<span class="type">int</span> argc, <span class="type">char</span> *argv[])</span></span><br><span class="line">{</span><br><span class="line"> CVirtual object;</span><br><span class="line"> object.fun1();</span><br><span class="line"> object.fun2();</span><br><span class="line"> <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>编译后,用IDA Pro打开Debug版程序。main函数的汇编代码如下。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240719172756707.webp" alt="image-20240719172756707"></p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240719172812710.webp" alt="image-20240719172812710"></p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240719172826850.webp" alt="image-20240719172826850"></p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240719172905171.webp" alt="image-20240719172905171"></p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240719172914758.webp" alt="image-20240719172914758"></p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240719172925280.webp" alt="image-20240719172925280"></p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240719172943174.webp" alt="image-20240719172943174"></p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240719172951276.webp" alt="image-20240719172951276"></p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240719173000030.webp" alt="image-20240719173000030"></p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240719173037431.webp" alt="image-20240719173037431"></p><p>虚表</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240719173557379.webp" alt="image-20240719173557379"></p><p>首先,在main函数入口处申请了对象实例的内存空间,第1个call指令调用了构造函数。接下来,调用成员函数fun1和fum2。最后,调用析构函数。这些成员函数调用的第1个参数都是this指针,也就是rcx=this。C++语法规定,在实例化对象时会自动调用构造函数,对象作用域会自动调用析构函数。因此,这里的构造函数和析构函数的调用顺序符合C++的语法规定。<br>在逆向过程中,如果一个对象在某个作用域内调用的是第1个函数,就可以怀疑是构造函数的调用;如果一个对象在某个作用域内调用的是最后一个函数,就可以怀疑是析构函数的调用。<br>接下来分析构造函数的实现。在构造函数中,首先初始化虚表指针,然后初始化数据成员,构造函数完成,返回this指针。为什么要返回this指针呢?这是C++编译器为了判断一个构造是否被调用而设置的。在下一个例子中,我们会讲解这个返回值的应用。<br>在逆向过程中,如果一个函数在人口处使用“lea reg,of_140007970”和“mov[reg],reg”特征初始化虚表,且返回值为this指针,就可以怀疑这个函数是一个构造函数。再来看看析构函数的实现(sub_14000100F)。在析构函数里,首先也赋值了虚表,最后也返回了this指针。为什么析构函数还要赋值虚表,构造函数不是赋值了吗?这是因为C++语法规定,析构函数需要调用虚函数的无多态性。</p><p>在逆向过程中,如果一个函数在入口处使用“lea reg,off_140007970”和“mov [reg],reg”特征初始化虚表,并且返回值为this指针,就可以怀疑这个函数是一个析构函数。既然这个特征与构造函数是一致的,该如何区分呢?读者可根据调用的先后顺序确定。</p><p>接下来看看虚表的结构(.rdata:0000000140007970)。因为这个类有虚函数,所以编译器为这个类产生了一个虚表,其存储区域在全局数据区。虚表的每一项都是8字节,其中存储的是成员函数的地址。在这里要注意的是:因为虚表的最后一项不一定以0结尾,所以虚表项的个数会根据其他信息来确定。<br>虚表中的函数是按类中的成员函数声明顺序依次放人的。需要注意的是:函数分布顺序在某些情祝下不一定与声明顺序相同(例如虚函数重载),不过这个顺序对逆向还原代码没有影响。通过这个虚表,就可以还原这个类的虚函数个数及虚函数代码了。<br></p><p>虚表特征总结如下。</p><p>●如果一个类至少有一个虚函数,这个类就有一个指向虚表的指针。<br>●不同的类虚表不同,相同的类对象共享一个虚表。<br>●虚表指针存放在对象首地址处。<br>●虚表地址在全局数据区中。<br>●虚表的每个元素都指向一个类成员函数指针(8字节)。<br>●虚表不一定以0结尾。<br>●虚表的成员函数顺序,按照类声明的顺序排列。<br>●虚表在构造函数中会被初始化。<br>●虚表在析构函数中会被赋值。</p><p>根据以上特征就可以判断一个地址处的内容是否是一个虚表,并根据虚表的项数还原这个类编写的所有虚函数了。</p><p>对象内存布局总结如图所示。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240719174922334.webp" alt="image-20240719174922334"></p>]]></content>
</entry>
<entry>
<title>静态分析技术</title>
<link href="/2024/07/26/%E9%9D%99%E6%80%81%E5%88%86%E6%9E%90%E6%8A%80%E6%9C%AF/"/>
<url>/2024/07/26/%E9%9D%99%E6%80%81%E5%88%86%E6%9E%90%E6%8A%80%E6%9C%AF/</url>
<content type="html"><![CDATA[<p>用高级语言编写的程序有两种形式。一种程序是被编译成机器语言在CPU上执行的,例如VisualC++。机器语言与汇编语言几乎是对应的,因此,可以将机器语言转化成汇编语言,这个过程称为反汇编(Disassembler)。例如,在x86系统中,机器码“EB”对应的汇编语句是“jmp short xx’”。另一种程序是一边解释一边执行的,编写这种程序的语言称为解释性语言,例如Visual Basic3.0/4.0、Java。这类语言的编译后程序可以被还原成高级语言的原始结构,这个过程称为反编译(Decompiler)。</p><p>所谓静态分析,是指通过反汇编、反编译手段获得程序汇编代码或源代码,然后根据程序清单分析程序的流程,了解模块所完成的功能。</p><h3 id="一、文件类型分析"><a href="#一、文件类型分析" class="headerlink" title="一、文件类型分析"></a>一、文件类型分析</h3><p>逆向分析程序的第一步就是分析程序的类型,了解程序是用什么语言编写的或用什么编译器编译的,以及程序是否被某种加密程序处理过,然后才能有的放矢,进行下一步工作。这个分析过程需要文件分析工具的辅助。常见的文件分析工具有PEiD、Exeinfo PE等。此类工具可以检测大多数编译语言、病毒和加密软件,本节以PEiD为例简单讲解它们的用法。</p><p>PEiD是一款常用的文件检测分析工具,具有GUI界面。它能检测大多数编译语言、病毒和加密的壳。如图所示,被分析的文件是用Microsoft Visual C++6.0编译的,对无法分析出类型的文件可能报告“PE Win GUI”(“Win GUI”是Windows图形用户界面程序的统称)。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240710100201779.webp" alt="image-20240710100201779"></p><p>PEiD这类文件分析工具是利用特征串搜索来完成识别工作的。各种开发语言都有固定的启动代码,利用这一点就可以识别程序是由何种语言编译的。被加密程序处理过的程序中会留下加密软件的相关信息,利用这一点就可以识别程序是被何种软件加密的。</p><p>PEiD提供了一个扩展接口文件userdb.txt,用户可以自定义一些特征码,这样就可以识别新的文件类型了。签名的制作可以用Add Signature插件完成,必要时还要用OllyDbg等调试器配合进行修正。</p><p>有些外壳程序为了欺骗PEiD等文件识别软件,会将一些加壳信息去除,并伪造启动代码。例如,将入口代码改成与用Visual C+6.0所编程序入口处类似的代码,即可达到欺骗的目的。所以,文件识别工具给出的结果只能作为参考,至于文件是否被加壳处理过,要跟踪分析程序代码才能知道。</p><h3 id="二,反汇编引擎"><a href="#二,反汇编引擎" class="headerlink" title="二,反汇编引擎"></a>二,反汇编引擎</h3><p>在安全软件和保护软件的开发过程中经常会用到汇编引擎和反汇编引整,例如OllyDbg、IDA、VMProtect、加壳软件和反编译器等。反汇编引擎的作用是把机器码解析成汇编指令。开发反汇编引擎需要对Intel的i386机器指令编码有深入的了解。不过,一般不需要自己开发反汇编引擎,网上有很多开源的或收费的反汇编引擎可以使用。目前主流的开源x86-64汇编引擎和反汇编引擎,在不同的使用场景中各有优势。下面对常用的汇编引擎和反汇编引擎进行比较,反汇编引擎有ODDisasm、BeaEngine、Udis86、Capstone,汇编引擎有ODAssembler、Keystone、AsmJit。</p><ol><li><p>OllyDbg的ODDisasm</p><p><code>OllyDbg的ODDisasm</code>:OllyDbg自带的反汇编引擎ODDisasm,优点是具有汇编接口(即文本解析,将文本字符串解析并编码成二进制值),这个特性曾经独树一帜。近些年出现的调试器x64_dbg,功能与OllyDbg的文本解析功能相似,支持的指令集更加完整,Bug更少,同时支持x64平台。</p></li><li><p>BeaEngine</p></li></ol><p><code>BeaEngine</code>:能解析的扩展指令集有FPU、MMX、SSE、SSE2、SSE3、SSSE3、SSE4.1、SSE4.2、VMX、CLMUL、AES、MPX。BeaEngine对指令进行了分类,以便判断不同的指令。BeaEngine还有一个特点是可以解码每一条指令所使用和影响的寄存器,包括标志位寄存器,甚至能精确解码标志位寄存器的所有位置,这个功能用来做优化器和混淆器是很有优势的。BeaEngine除了支持对x86指令进行反汇编,还支持对x64指令进行反汇编。</p><ol start="3"><li><p>Udis86</p><p>Udis86是一款广受欢迎的反汇编引擎,支持的x86扩展指令集包括MMX、FPU(x87)、AMD3DNow!、SSE、SSE2、SSE3、SSSE3、SSE4.1、SSE4.2、AES、AMD-V、INTEL-VMX、SMX。Udis86除了支持对x86指令进行反汇编,还支持对x64指令进行反汇编。Udis86的代码风格精简,功能函数短小,变量命名和接口干净、简单、操作灵活。如果需要自行维护一个分支,使用Uis86几十分钟就能熟悉整个代码架构。</p><p>Udis86的优点是接口灵活,可以使用ud_decode函数对一条指令只进行解码操作,再对解码后的结构使用ud_translate_intel函数转换成文本格式,也可以直接使用ud_disassemble函数一次性完成所有操作,这些接口都只需要一行代码就能实现。</p></li><li><p>Capstone</p></li></ol><p>Capstone可以说是所有反汇编引擎中的集大成者。因为Capstone移植自LLVM框架的MC组件的一部分,所以LLVM支持的CPU架构它也都支持。Capstone支持的CPU架构有ARM、ARM64(ARMv8)、M68K、MIPS、PowerPC、SPARC,System z、TMS320C64X、XC0RE、x86(包括x86-64)而且,Capstone对x86架构指令集的支持是最全的,这一点是其他引擎比不上的,它支持的x86扩展指令集有3DNow、3 DNowa、x86-64、ADX、AES、Atom、AVX、AVX2、AVX512CD、AVX512ER、AVX512F、 AVX512PF、BMI、BMI2、FMA、FMA4、FSGSBASE、LZCNT、MMX、SGX、SHA、SLM、SSE、SSE2、SSE3、SSE4.1、SSE4.2、SSE4A、SSSE3、TBM、XOP。<br>在目前移动端开发火热的背景下,支持ARM的反汇编库却很少。如果要同时进行x86与ARM下编译器方面的开发,能使用统一的接口自然更好。仅从x86-64平台上的情况来看,无论是解码能力还是指令集支持,Capstone完全超越了BeaEngine。</p><p>因为Capstone是从LLVM中移植过来的,Capstone是C语言的项目,而LLVM是C++语言的项目,所以Capstone在移植过程中做了很多适配工作,显得很靡肿。举个例子,LLVM中的MCInst是一个单条底层机器指令的描述类,因为Capstone是C语言的项目,所以在移植时将这些类变成了结构,把成员函数变成了独立的C函数,例如MCInst_Init、MCInst_setOpcode。而且,由于LLVM框架的复杂性和高度兼容性,里面的所有的概念都进行了高度抽象,而Capstone通过适配接口将其转换到自己的架构中,也造成了解码时中间层过多、性能下降。<br>在一条指令的解码过程中,重要的中间层结构的使用顺序是MCInst-→InternalInstruction-→cs_insn。基础的解码工作依靠LLVM的架构,解码到Capstone的 InternalInstruction中(它是一个包含解码过程中所有细节的内部结构)。解码后,调用update_pub_insn将认为需要公开的内容复制到cs_insn中。其他反汇编引擎都是一次性解码到目标结构中的。</p><p>Capstone的解码过程如此复杂,自然会对性能造成影响。Capstone的性能耗时是Udis86的5~6倍。如果换一种方式来测试,Udis86只使用ud_decode函数进行解码,而Capstone没有独立的解码接口,需要进行一些修改(让它不生成汇编文本),那么Capstone的耗时大概是Udis86的2倍。由此可见,Capstone的文本操作比 Udis86慢得多。</p><p>此外,Capstone的内存消耗很大,解码一条指令时传入的指令结构cs_insn必须由动态分配函数来分配,而且要分配两次,一次是cs_insn,另一次是cs_detail,这会造成巨量的内存碎片。因为每一条指令的结构体都很大(sizeof(cs_.insn)+sizeof(cs_detail)训=1760字节),所以必须使用动态内存,这也是Capstone与其他反汇编引擎不一样的地方。如果要使用Capstone进行大量的指令分析,就要给它配置一个固定的对象内存分配器,从而稍稍减少内存碎片的生成,提高一点点的性能。可能是基于以上原因,x64dbg社区在一开始以BeaEngine为基础,但BeaEngine总是爆出Bug,所以后来用Capstone替换了BeaEngine(仅用Capstone进行GUI的文本反汇编)。Capstone虽然解码速度不高,但是Bug很少(LLVM有苹果那么大规模的公司支撑)。不过,Capstone的流图和指令分析功能还不完善,因此目前在这些方面仍在使用BeaEngine。</p><ol start="5"><li>Asmjit</li></ol><p>AsmJit是一个以C++封装的完整的JIT汇编器和编译器,它可以生成兼容x86和x64架构的原生汇编指令,支持x86和x64指令集,包括MMX、SSEx、BMx、ADX、 TBM、XOP、AVXx、FMAx、AVX512等。AsmJit与前面介绍的开源库都不一样,它不像BeaEngine、Udis86、Capstone那样能对二进制指令进行反汇编解析,它只是一个汇编器。指令都被封装成了类成员函数,通过调用函数的方式来编码:函数中的参数既可以直接使用指定的寄存器、内存操作数,也可以使用X86Gp、Labl等类型的占位符变量,根据不同的逻辑给这些占位符变量赋值不同的操作数。</p><ol start="6"><li><p>Keystone</p><p>Keystone和Capstone是同一系列的引擎,由同一维护者主导开发。Capstone主要负贵跨平台多指令集的反汇编工作,而Keystone主要负责跨平台多指令集的汇编工作。与OllyDbg的汇编器一样,Keystone也只支持文本汇编,不支持像AsmJit那样的函数式汇编。</p><p>Keystone也移植自LLVM框架中MC组件的一部分,所以LLVM支持的CPU架构Keystone也都支持。Keystone支持的CPU架构有ARM、ARM64 (AArch64/ARMv8) Hexagon、MIPS、PowerPC、SPARC、System z、x86(包括16位、32位、64位)。</p></li></ol><h3 id="三、静态反汇编"><a href="#三、静态反汇编" class="headerlink" title="三、静态反汇编"></a>三、静态反汇编</h3><p>本节主要介绍常见的反汇编工具及其用法。在进行反汇编前,建议用PEiD等检测工具分析一下文件是否加壳了。如果已经加壳,就要先利用脱壳技术进行脱壳,再进行反汇编。常用的反汇编工具有IDA Pro等。虽然OllyDbg也有反汇编功能,但其侧重动态调试,反汇编辅助分析功能有限。IDA Pro是一款商业软件,属于专家级产品,是逆向工程的必备工具。</p><ol><li>IDA Pro</li></ol><p>IDA安装成功后,会在桌面上生成两个图标,分别为IDA Pro(32-bit)和IDA Pro(64-bit),它们分别对应于32位和64位程序的分析。DA支持的文件类型非常丰富,除了常见的PE格式,还支持DOS、UNX、Mac、Java、NET等平台的文件格式。单击“File”→“Open”菜单项,打开目标文件ReverseMe.exe,IDA一般能自动识别其格式。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240710164506460.webp" alt="image-20240710164506460"></p><p>IDA是按区块装载PE文件的,例如,text(代码块)、.data(数据块入、rsrc(资源块)、.data(输人表)和.edata(输出表)等。IDA反汇编所消耗的时间与程序大小及复杂程度有关,通常需要等待一段时间才能完成。</p><p>此过程分为两个阶段。在第一阶段,将程序的代码和数据分开,分别标记函数并分析其参数调用,分析跳转、调用等指令关系并给标签赋值等。在第二阶段,如果IDA能够识别文件的编译类型,就装载对应的编译器特征文件,然后给各函数赋名。随后,IDA会创建一个数据库,其组件分别保存在扩展名为.id0、,idl、.nam和.til的4个文件里,这些文件的格式为IDA专用,在关闭当前项目时,这4个文件将被存档为一个IDB文件。一旦IDA创建了数据库,就不需要再访问这个可执行文件了,除非使用IDA的集成调试器调试这个可执行文件本身。再次分析该目标文件时,IDA只需要打开现有数据库,就会将界面恢复为上次关闭时的状态。<br>“Kernel optionl” “Kernel option2” “Processor option’”这3个选项可以控制反汇编引擎的工作状态,一般使用默认设置。IDA会自动识别程序类别与处理器类型,在大多数情况下,分析选项的默认值会在准确性与方便性之间提供一个折中的参数。如果IDA分析出了有问题的代码,将“KerneloptionI”中的“Make final analysis pass’”选项关闭是一一个很好的方法。在某些情况下,一些代码会因不在预计的位置而不被确认,这时选中“Kernel option2”域中的“Coagulate Data Segments in the finalpass”选项是有帮助的。</p><ol start="2"><li>ida的配置</li></ol><p>合理配置IDA文件可以大大提高工作效率。Windows图形界面的主程序是idag.exe,可通过“Options”(选项)菜单来配置IDA。但这种配置仅对当前的项目有效,在新建项目时会恢复成默认配置。要改变默认配置,必须编辑ida.cfg文件,该文件包含用于控制反汇编行的格式的选项。</p><p>在IDA的cfg目录下查找IDA配置文件ida.cfg和GUI配置文件idagui.cfg。ida.cfg是一个文本文件,不能用Windows的“记事本”程序进行编辑。因为“记事本”程序对一些特殊字符的识别效果不好,如果继续编辑和保存文件,文件将被破坏,所以,建议用EditPlus、UltraEdit等工具来编辑ida.cfg等配置文件。<br>ida.cfg文件由两部分组成。第一部分定义文件的扩展名、内存、屏幕等;第二部分配置普通参数,例如代码显示格式、ASCII字符串显示格式、脚本定义和处理器选项等。另外,一些问题的出现也与ida.cfg有关。例如,MAX_ITEM_LINES默认为5000行。对许多大文件来说,可能会因行数不够而发生错误。<br>要想显示与每个反汇编行有关的其他信息,可以通过“Options”→“General”命令打开IDA的常规选项,然后在“Disassembly’”选项卡中可用的反汇编行部分选择相应的选项。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240710173146875.webp" alt="image-20240710173146875"></p><p>要设置ASCII字符串风格,可单击“Options””→“ASCII String styles’”选项,打开字符串设置窗口。对应的ida,cfg的部分配置如下。</p><ol start="3"><li>IDA主窗口</li></ol><p>IDA分析完目标程序后进入主窗口,界面看上去专业且复杂。IDA相当智能,会尽量分析程序各模块的功能,并给出相应的提示,例如为API函数的参数自动加上注释,相当直观。对那些IDA不能正常分析的代码,则需要进行手工辅助分析。</p><p><code>反汇编窗口:</code>IDA-View是反汇编代码的显示窗口,它有两种形式,分别是图形视图(默认)和文本视图。在图形视图中,IDA以程序流程图的形式显示代码,将函数分解成许多基本块,从而生动显示该函数由一个块到另一个块的控制流程。用户可以使用“Ctrl键+鼠标滚轮”来调整图形的大小,使用空格键在图形视图和文本视图之间切换,或者选择右键快捷菜单中的“Text view’”选项切换到文本视图。选择“View”→“Open subviews’”→“Disassembly”选项,打开反汇编子窗口,就可以用多个子窗口来分析同一段程序,而不必来回翻页查看代码了。其他常用窗口,例如“Functions”和“Proximitybrowser”,也可以使用这个菜单打开。</p><p><code>导航栏:</code>单击菜单项“Viev”→“Toolbars”→“Navigation”,打开导航栏,可以看到被加载文件地址空间的线性视图,“Library function’”为库函数,“Data”为数据,“Regular function”为规则函数,“Unexplored”为未查过的,“Instruction”为指令,“External$ymbol”为外部符号,用户可根据需要快速跳转到相关代码处。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240710174829354.webp" alt="image-20240710174829354"></p><p>在导航栏中执行右键快捷菜单项“Zoom in”和“Zoom out’”,可以调整导航条的显示比例。对手工分析来说,导航栏的作用非常大,选择适当的倍率可以达到意想不到的效果。</p><p><code>注释:</code>使用IDA可以方便地在代码后面输入注释。在窗口右边空白处单击右键,将显示输人注释的快捷菜单项,一个是“Enter comment”(快捷键是冒号),另一个是“Enter repeatable comment’”(快键是分号)。按“;”键输人的注释在所有交叉参考处都会出现,按“:”键输入的注释只在该处出现。如果一个地址处有两种注释,将只显示非重复注释。</p><p><code>提示窗口:</code>IDA界面下方的提示窗口是IDA的输出控制台,主要用于反馈各种信息,例如文件分析进度、状态消息、错误消息及IDA脚本或插件信息等。</p><p><code>字符串窗口:</code>可以通过单击“View”+“Open Subviews”一→“Strings”选项打开该字符串窗口(Strings Window)。字符串窗口中显示的是从二进制文件中提取的一组字符串,双击窗口中的字符串,反汇编窗口将跳转到该字符串所在的地址处。将字符串窗口与交叉引用结合使用,可以快速定位程序中任何引用该字符串的位置。在字符串窗口单击右键,在弹出的快捷菜单中选择“Stup”选项,可以设置扫描的字符串类型。</p><p><code>输入窗口:</code>输入窗口(Imports)中列出了可执行文件调用的所有函数。输人窗口中的每个条目都列出了一个函数名称,以及包含该函数的库的名称,每个条目列出的地址为相关函数的虚拟地址,双击函数,IDA将跳转到反汇编窗口的函数地址处。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240711102223253.webp" alt="image-20240711102223253"></p><p><code>跳转到地址窗口:</code>可以在反汇编窗口上下滚动,直至看到想要访问的地址。若知道目标地址,可以用IDA提供的快捷键“G”打开“Jump to address”(跳转到地址)窗口,输入一个地址(十六进制值),IDA会立即显示该地址的代码。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240711102243042.webp" alt="image-20240711102243042"></p><p>执行跳转功能后,当需要返回时,只要在工具栏中单击←按钮或按“Esc”键(“Esc”键是一个非常有用的快捷键,功能与浏览器的“后退”按钮类似),列表便会往后翻一页。若要往前翻一页,可以单击→按钮或按“Ctrl+Enter’”组合键。</p><ol start="4"><li>交叉参考</li></ol><p>通过交叉参考(XREF)可以知道指令代码相互调用的关系。如图所示,“CODE XREF: sub_401120+B↑j”表示该调用地址是401120h,“j”表示跳转(jump)。此外,“o”表示偏移量(offset),“p”表示子程序(procedure)。双击此处或按“Enter”键可以跳转到调用该处的地方。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240711102715156.webp" alt="image-20240711102715156"></p><p>在“loc_401165”字符上按“X”键,将打开交叉参考窗口</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240711102815971.webp" alt="image-20240711102815971"></p><ol start="5"><li>参考重命名</li></ol><p>参考重命名(Renaming of reference)是IDA的一个极好的功能,它可以将反汇编清单中的一些默认名称更改为有意义的名称,增加了代码的可读性。要修改一个名称,只需单击希望修改的名称(使其突出显示),并使用快捷键“N”打开更名对话框。</p><p>如图所示,这段代码是窗口函数WndClass的开始处,IDA默认用“sub_401120”为其命名,但“sub_401120”这个字符没有太大的意义。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240711103121227.webp" alt="image-20240711103121227"></p><p>若加上了注释,则只有这一行才有意义。使用参考重命名功能便可一次性修改所有参考点。在“sub_401120”字符上单击右键,在弹出的快捷菜单中选择“Rename’”((重命名)选项,或者按“N”键,打开“Rename address’”对话框,如图所示。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240711103357636.webp" alt="image-20240711103357636"></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">Local name:局部符号名的作用域仅限于当前函数。</span><br><span class="line">Include in names list:勾选这个选项,将有一个名称被添加到名称窗口中,</span><br><span class="line">Public name:由二进制文件(例如DLL)输出的名称。</span><br><span class="line">Autogenerated name:自动创建符号名。</span><br><span class="line">Weak name:弱符号,是公共符号的一种特殊形式。</span><br></pre></td></tr></table></figure><p>在此处赋予它“WndProc’”这个有意义的名字,然后单击“OK”按钮,马上就可以看到所有“sub_401120”标签的名称都变成新名称了,如图所示。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240711105242279.webp" alt="image-20240711105242279"></p><ol start="6"><li>标签的用法</li></ol><p>单击菜单项“Jump”→“Mark position”,打开“标记当前位置”功能,会出现如图所示的对话框。为这个标记(当前光标位置)加上标签,“WndProc’”标签就是需要返回的位置。当离开这个标记并返回时,选择菜单项“Jump”→“Jump to marked position’”,或者按“Crl+M”快捷键,执行“跳转到标记位置”功能,如图所示。选择返回的标签并双击,即可跳转到指定代码处。</p><ol start="7"><li>格式化指令操作数</li></ol><p>IDA可以格式化指令使用的常量,因此应尽可能使用符号名称而非数字,从而使反汇编代码更具可读性。IDA根据被反汇编指令的上下文、所使用的数据作出格式化决定。对其他情况,IDA一般会将相关常量格式化成一个十六进制常量。</p><p>IDA可以提供多种进制显示。将光标移到需要转换进制的常量上,单击右键,会弹出如图所示的上下文菜单。该菜单提供的选项可将常量格式化成十进制、八进制或二进制的值。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240711112655728.webp" alt="image-20240711112655728"></p><p>在大部分情祝下,源代码中使用的是已命名的常量,例如define语句。IDA维护着大量的常见库(例如C标准库、Win32API),用户可以通过右键快捷菜单中的“Use standard symbolic constant”(使用标准符号常量)选项来设置常量,如图所示。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240711112910544.webp" alt="image-20240711112910544"></p><p>在本例中,根据Create Window参数,确定80000000h处对应的符号是CW_USEDEFAULT,并得到如图所示的反汇编行。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240711113050986.webp" alt="image-20240711113050986"></p><ol start="8"><li>函数的操作</li></ol><p>IDA允许手动干预创建、编辑、删除函数。新函数由不属于某个函数的现有指令创建,或者由未被IDA以任何方式定义的原始数据创建。将光标移到要创建函数的第1个字节上,选择“Edit”→“Functions’”+“Create Function”选项,创建一个函数(快捷键是“P”)。必要时,IDA会将数据转换成代码,以便分析函数的结构。如果能找到函数的结束部分,IDA将生成一个新的函数名,以函数的形式重组代码。如果无法确定函数的结束部分或者发现非法指令,这个操作将会终止。删除函数时,可以使用“Edi”→“Functions’”<br>+“Delete Function’”命令。</p><ol start="9"><li>代码和数据的转化</li></ol><p>很多工具在进行反汇编的时候可能无法正确区分数据和代码,IDA也不例外,数据字节可能会被错误地识别为代码字节,而代码字节可能会被错误地识别为数据字节。有些程序就是利用这一点来对抗静态反汇编的。IDA的交互性使用户可以将某段十六进制数据指定为代码或数据,即利用人脑来区分代码和数据。</p><p>如果确信某段十六进制数据是一段指令,只要将光标移到其第1个字节的偏移位置,执行菜单命令“Edit”→“Code”或按“C”键即可。按“P”键可以将某段代码定义为子程序,并列出参数调用。若要取消定义,可以执行菜单命令“Edit”→“Undefine’”或按“U”键,数据将重新以十六进制形式显示。这种交互式分析功能的介人,使IDA达到了非交互式软件无法达到的效果。</p><p>在代码行按“D”键,数据类型会在db、dw与dd之间转换。执行菜单命令“Options”→“Setupdata types’”,可以设置更多的数据类型,如图所示。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240711140958687.webp" alt="image-20240711140958687"></p><ol start="10"><li>字符串</li></ol><p>编程语言的不同造成了字符串格式的不同,例如以“0”结尾的C语言字符串及以“$”结尾的DOS字符串等。IDA支持所有字符串格式。如果确信某段十六进制数据是一个字符串,那么只要将光标移到其第1个字符的偏移位置,执行菜单命令“Edit”+“Strings’”→“ASCII”或按“A”键即可。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240711141305500.webp" alt="image-20240711141305500"></p><p>按“A”键设置默认是C语言字符串。也可以选择菜单项“Options”+“ASCII string style”,设置其他字符串格式的默认值。</p><p>IDA有时无法确定ASCII字符串,发生这种错误的原因是这个字符串在程序中没有被直接调用。在本例中,按“G”键,输入地址“0040478E”,会来到如下代码处。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240711143945397.webp" alt="image-20240711143945397"></p><p>将光标移到0040478Eh处并按“A”键,该处就会被定义并生成一个变量名。如果要将其恢复可按“U”键。IDA会给生成的字符变量加一个前缀“a”,例如“aGetfiletype db’GetFileType’,0”。可以在“Names”窗口看到这些字符串变量(单击按钮N或选择菜单项“View”→“Open subviews“→“Names”即可打开这个窗口)。</p><ol start="11"><li>数组</li></ol><p>IDA有着较强的数组聚合能力。它可以将一串数据声明变成一个反汇编行,按数组的形式显示,从而简化反汇编代码清单。</p><p>用IDA打开实例Arrays.exe,找到一个数组,其汇编代码为<code> mov edi, dword_407030[eax]</code></p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240711144459125.webp" alt="image-20240711144459125"></p><p>将光标移到需要处理的数据处,选择菜单项“Edit”→“Amy”或按“*”键,打开数组排列调整窗口,如图所示。若在“I tems on a line’”文本框中填“0”,每行项数会根据页面自动调整;若想让每行显示更多的数据,可以在反汇编选项中调整右边距(Right margin)。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240711144444698.webp" alt="image-20240711144444698"></p><p>设置完成,数据按1×3的形式排列</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240711144731224.webp" alt="image-20240711144731224"></p><ol start="12"><li>结构体</li></ol><p>在C语言中,结构体(struct)是一种数据结构,可以将不同类型的数据结构组合到一个复合的数据类型中。结构体可以被声明为变量、指针或数组等,从而实现比较复杂的数据结构。</p><p><code>创建结构体</code>对一些常见的文件类型,IDA会自动加载相应的类型库,例如vc6win(Visual C++6.0)。在进行底层分析时,可以增加mssdk(windows.h)、ntddk(ntddk.h)等。这些类型库中有相应的结构体,用户分析代码时可以直接引用。按“Shift+F11”快捷键,打开加载类型库窗口(Loaded Type Libraries),如图所示。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240711145504295.webp" alt="image-20240711145504295"></p><p>单击右键,在弹出的快捷菜单中选择“Load Type Library”选项(或按“Insert”键),在弹出的“Available Type Libraries’”窗口中选择类型库,如图所示。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240711145741545.webp" alt="image-20240711145741545"></p><p>此时就可以查看内置的结构体数据结构了,选择“View”→“Open subviews”→“Structures”菜单项,打开结构体管理窗口。按“Insert’”键,在弹出的窗口中单击“Add Standard Structure”按钮打开添加标准结构库窗口,查找需要的结构名,就可以正常使用这些库了。</p><p>在默认情况下,IDA会加载常用的结构。在结构体管理窗口按“Insert’”键,然后单击“Cancel”按钮,ReverseMe程序内常用的结构体数据结构就会显示出来。在WNDCLASSA结构一行双击,展开结构,在程序代码的相应位置会直接以结构体的形式显示,如图所示。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240711150256117.webp" alt="image-20240711150256117"></p><p>IDA会通过各种措施来改善结构体代码的可读性。如果程序正在使用某个结构体,而IDA并不了解其布局,IDA将允许用户自定义结构体,并将自定义的结构体放到反汇编代码清单中,例如下面这段c程序</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string"><stdio.h></span> </span></span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">student</span></span></span><br><span class="line"><span class="class">{</span><span class="type">int</span> id;</span><br><span class="line"><span class="type">char</span> name[<span class="number">20</span>];</span><br><span class="line"><span class="type">int</span> age;</span><br><span class="line">};</span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">student</span> <span class="title">stu</span>[2]=</span>{{<span class="number">01</span>,<span class="string">"Mary"</span>,<span class="number">14</span>},{<span class="number">02</span>,<span class="string">"Angela"</span>,<span class="number">15</span>}};</span><br><span class="line"><span class="type">int</span> <span class="title function_">main</span><span class="params">(<span class="type">void</span>)</span></span><br><span class="line">{</span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">student</span> *<span class="title">p</span>;</span></span><br><span class="line"><span class="keyword">for</span>(p=stu;p<stu+<span class="number">2</span>;p++)</span><br><span class="line"><span class="built_in">printf</span>(<span class="string">"%5d %-20s%4d\n"</span>,p->id,p->name,p->age);</span><br><span class="line"><span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>如图所示的代码是IDA在反汇编时由于没有定义结构体而自动生成的。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240711151331553.webp" alt="image-20240711151331553"></p><p>[esi+18h]等是调用的结构体中的数据,可以用有意义的名字来代替这些无意义的数字。双击“unk_407030”字符,来到结构体数据处,利用“D”键、“A”键或<img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240711151514542.webp" alt="image-20240711151514542">按钮(数组的项数设置为20)重新定义数据,结果如下。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240711151640347.webp" alt="image-20240711151640347"></p><p>打开结构体窗口,按“Insert’”键增加一个结构体类型“student”。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240711151736793.webp" alt="image-20240711151736793"></p><p>按“D”键加入数据(例如id、age)。重复按“D”键,在db、dw和dd之间切换,直至变成“dd”(表示dword类型)。按“A”键加入的ASCII字符(例如name)为结构的成员。此处数组大小为20,</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240711153117129.webp" alt="image-20240711153117129"></p><p>如果要创建一个大小可变的结构体,可以将此处自定义的数组元素大小设为0。新增结构成员时,IDA会自动为其命名。</p><p>现在,将光标定位在00407030h处,执行菜单命令“Edit”→“Structs’”→“Struet var’”,将出现如图所示的窗口,供用户选择结构体类型。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240711153256314.webp" alt="image-20240711153256314"></p><p>选择student结构体,单击“OK”按钮,即可将数据纠正过来。使用同样的方法,重复执行“Struct var’”命令,将0040704Ch处的数据转换成student类型。转换后的数据如图所示。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240711153213965.webp" alt="image-20240711153213965"></p><p>最后,可以在操作数类型中重新定义现有数据。选中需要重新定义的数据,例如[esi+18h],单击菜单项“Edit”→“Operand types”→“Offset””→“Oset(Struct)”或按“T”键,执行结构偏移功能,选择student结构体,依次将[esi],[esi+4]重新定义效果。即使结构体中的成员较多,也不必逐一替换。IDA提供了批处理操作,可以通过一次操作完成全部工作。选择所有需要替换的代码,执行“Oset(Struct)”菜单命令或按“T”键,打开结构偏移设置窗口,如图所示。窗口右边显示了与si有关的所有操作,成员名前面不同的符号表示计算后的状态,“√”表示完全匹配。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240711153658753.webp" alt="image-20240711153658753"></p><p>IDA还可以在已经分析好的数据中建立结构体。在00407030处选择一块已经重新组织的数据,使用菜单命令“Edit”→“Structs’”+“Create struct from data’”创建结构体。</p><ol start="13"><li>枚举类型</li></ol><p>可以在反汇编时用IDA动态定义和操作枚举类型(Enumerated Types)。看看下面这段简单的C程序,在用IDA进行反汇编后,得到了一些没有意义的数字。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string"><stdio.h></span> </span></span><br><span class="line"><span class="type">int</span> <span class="title function_">main</span><span class="params">(<span class="type">void</span>)</span></span><br><span class="line">{</span><br><span class="line"><span class="class"><span class="keyword">enum</span> <span class="title">weekday</span> {</span> MONDAY, TUESDAY, WEDNESDAY, THUSDAY, FRIDAY, SATURDAY, SUNDAY }; </span><br><span class="line"></span><br><span class="line"><span class="built_in">printf</span>(<span class="string">"%d,%d,%d,%d,%d,%d,%d"</span>,MONDAY,TUESDAY, WEDNESDAY, THUSDAY, FRIDAY, SATURDAY, SUNDAY );</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240711162955968.webp" alt="image-20240711162955968"></p><p>可以用枚举类型来表示这些数字。执行“View”→“Open subviews”→“Enumerations’”选项,打开枚举窗口,按“Insert’”键插入一个新的枚举类型“weekday”。在新建的weekday枚举类型中按“N”键添加枚举成员,如图所示,“0”对应于“MONDAY”,“1”对应于“TUESDAY”,依此类推。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240711163238153.webp" alt="image-20240711163238153"></p><p>可以在操作数类型中重新定义现有数据。将光标移到需要重新定义的数据处,可以执行菜单项“Edit”→“Operand types””→“Enum member”或按“M”键将其转换成指定的枚举成员,也可以在选中数字后执行右键快捷菜单中的“Symbolic constant”命令。处理后的代码如图所示,IDA用“MONDAY”“TUESDAY”等代替了无意义的数字0、1等,使代码变得易读了。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240711163511846.webp" alt="image-20240711163511846"></p><ol start="14"><li>变量</li></ol><p>先来看一段用W32Dasm反汇编的ReverseMe的代码。在如下代码中,参数的传递过程不够明确,只知道一些数据传人了这个函数,因此可以进行改善。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240711163709990.webp" alt="image-20240711163709990"></p><p>IDA会自动认出哪些参数被放到了栈中,代码如下。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240711164441734.webp" alt="image-20240711164441734"></p><p>与前面一样,在IDA里可以给传递的变量赋予有意义的名称。在任何函数栈(例如Msg)上双击或按“Ctl+K”快捷键,打开栈窗口,将光标移到tagMSG上,即可显示各结构成员。</p><ol start="15"><li>IDC脚本</li></ol><p>IDA集成了一个脚本引擎,可以让用户从编程的角度对IDA的操作进行全面控制。脚本的存在极大地提高了IDA的可扩展性,使IDA中许多重复的任务可以由脚本来完成,用户可以在使其自动化的同时对一些特殊情况进行控制。IDA支持使用两种语言编写脚本,分别是IDC和Python。IDA的原始嵌入式脚本语言叫作IDC。IDC本身是一种类C的语言脚本控制器,语法与C语言类似,简单易学。IDA从6.8版本开始直接支持Python集成式脚本,更加灵活、方便。所有的IDC脚本中都有一条包含idc.idc文件的语句,这是IDA的标准库函数,变量定义形式为“auto var’”,其他逻辑、循环等语句与C语言类似。相关语法和函数功能,请查看IDA帮助系统中的相关主题。</p><ol start="16"><li>IDA调试器</li></ol><p>IDA支持调试器功能,因此很好地弥补了静态分析能力的不足,将静态分析与动态分析结合起来,提高了分析的效率。</p><p><code>加载目标文件</code>使用IDA打开目标软件。这时使用菜单项“Debugger’”→“Select Debugger’”,将根据当前的文件类型显示适合的调试器列表.此时,“Debugger’”菜单会以其他形式展开,可以单击菜单项“Debugger’”一“Start Process’”调试目标文件。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240711173636697.webp" alt="image-20240711173636697"></p><p>另一种调试目标文件的方式是附加到一个正在运行的进程上。能否使用IDA调试器附加进程的方式,取决于目前IDA是否打开了可执行文件。如图所示,单击菜单项“Debugger’”→“Attachto process..”即可附加进程。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240711173947923.webp" alt="image-20240711173947923"></p><p>IDA调试器除了能进行本地调试,还能进行远程调试。对于远程调试,IDA自带大量的调试服务器,包括用于Windows3264、Windows CE/ARM、Mac OS X32/64、Linux32/64/ARM和Android的服务器。运行远程调试服务器后,IDA将与该服务器通信,在远程计算机上启动目标进程。</p><p><code>调试器界面:</code>IDA进人调试器模式后,界面上将显示几个默认的窗口</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240711175250107.webp" alt="image-20240711175250107"></p><p>除此外,在“Modules’”窗口中显示了所有加载到进程内存空间中的可执行文件和共享库。双击模块名称将打开该模块输出的符号列表。</p><p><code>调试跟踪:</code>调试器的基本功能是跟踪所调试目标进程的行为。IDA在“Debugger”菜单里提供了相应的调试命令,每个命令都有相应的快捷键,常用的快捷键如表所示。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240711180908715.webp" alt="image-20240711180908715"></p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240711180918985.webp" alt="image-20240711180918985"></p><p><code>断点:</code>断点是调试器的必备功能。设置断点的目的是在程序中的特定位置中断。设置断点的快捷键是“F2”,右键快捷菜单中的对应选项是“Add Breakpoint”。已经设置断点的地址将以一条红色的光条突出显示。再次按下“F2”键将关闭断点,从而删除它。使用菜单项“Debug8er”→“Breakpoints“→”Breakpoint List”可以查看当前设置的所有断点。</p><p>IDA调试器支持条件断点。设置断点后,在右键快捷菜单中选择“Edit Breakpoint”选项,打开如图所示的对话框。“Location”设置框中是断点的地址。勾选“Enabled”复选框,说明该断点当前处于活动状态。勾选“Hardware”复选框,表示以硬件断点的方式实现该断点。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240711181745462.webp" alt="image-20240711181745462"></p><p>在“Condition’”设置框中输入一个表达式,即可创建条件断点。如表所示,可以为软件断点和硬件断点指定条件。IDA断点的条件通过IDC表达式指定。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240711181902234.webp" alt="image-20240711181902234"></p><p><code>跟踪:</code>IDA的跟踪(Tracing)功能类似于OllyDbg的Run trace功能,可以将调试程序执行过程中的事件记录下来。跟踪分为两类:一类是指令跟踪,通过菜单项“Debugger’”→“Tracing”→“Instruction Tracing”调用,IDA负责记录地址、指令和寄存器的值;另一类是函数跟踪,通过菜单项“Debugger”→“Tracing”+“Function Tracing’”调用,用于跟踪call指令的调用,并将结果记录下来。</p><ol start="17"><li>远程调试</li></ol><p>IDA Pro支持通过TCP/IP网络对应用程序进行远程调试,例如远程调试Windows、Linux、Android和Mac OS X二进制文件。IDA附带了用于实现远程调试会话的服务器组件。运行IDA Pro界面的系统称为调试器客户端,运行被调试应用程序的系统称为调试器服务端。除了设置并建立远程调试服务器连接,远程调试与本地调试没有太大的区别。</p><p>在进行远程调试前,要在远程计算机上启动相应的调试服务器组件,它会处理所有底层执行和调试器操作。在IDA文件目录dbgsrv里提供了服务器组件。</p><p>此外,IDA可以与使用gdb server的远程gdb会话进行连接。连接远程gdb服务器的过程与连接远程IDA调试服务器的过程基本相同。因为连接gdb_server时不需要密码,IDA无法获知运行gdb_server的计算机的体系结构,所以,需要为其指定处理器类型(默认为Intel x86),可能还需要指定该处理器的字节序。</p><h3 id="三、十六进制工具"><a href="#三、十六进制工具" class="headerlink" title="三、十六进制工具"></a>三、十六进制工具</h3><p>常用的十六进制工具有Hex Workshop、WinHex、Hiew等,它们各有特色:Hex Workshop提供了文件比较功能;WinHex可以查看内存映像文件;Hiew可以在汇编状态下修改代码。</p><h3 id="四,静态分析技术应用实例"><a href="#四,静态分析技术应用实例" class="headerlink" title="四,静态分析技术应用实例"></a>四,静态分析技术应用实例</h3><ol><li>解密初步</li></ol><p>现在的软件一般采取人机对话方式,因此,从提示信息入手很快就能找到要害。运行CrackMe.exe程序,随意输入几个字符,将出现如图所示的提示信息。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240712144052925.webp" alt="image-20240712144052925"></p><p>用IDA对CrackMe.exe进行反汇编。文件不大,向下翻查,很快就来到了如下代码处。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240712144434829.webp" alt="image-20240712144434829"></p><p>注意以下几句,它们比较关键</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240712145155140.webp" alt="image-20240712145155140"></p><p>要想让程序接受任何注册码,只要把”je”(不相等就跳转)改成“je”(相等就跳转)或空指令“nop”即可。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240712150147388.webp" alt="image-20240712150147388"></p><p>然后,单击菜单项“Patch program”一“Apply patches to input file”,将修改保存到文件中。此时,输入任何序列号,CrackMe.exe均提示注册成功。这种跳过算法分析直接修改关键跳转指令使程序注册成功的方法,通常被解密者称为“爆破法”。</p><p>此例的算法只是将用户输入的序列号与参照值进行比较,以判断真伪,其核心就是这句比较指令。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240712150341228.webp" alt="image-20240712150341228"></p><p>这种直接比较的程序,其参照值一般会存储在程序中。在大多数情祝下,编译器会将初始变量放在数据区块(.data区块)中。用十六进制工具打开文件,跳到.data区块处,会发现一个字符串“9981”,这就是正确的密码,如图所示。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240712151917648.webp" alt="image-20240712151917648"></p><p>所以,在编写注册码验证功能的过程中,不要让正确的注册码直接出现在程序中。另外,不要使用明显的提示信息,以防止信息被解密者利用并快速找到判断的核心。</p><ol start="2"><li>逆向工程初步</li></ol><p>通常将为了练习逆向工程而特别编写的程序命名为“ReverseMe’”。本例ReverseMe01.exe有如下要求。<br>● 移去“Okay,for now,mission failed”对话框。<br>● 显示一个MessageBox对话框,上面显示了用户输入的字符。<br>● 再显示一个对话框,用于告知用户输入的序列号是正确的还是错误的(“Good”或“Bad serial”)<br>● 将按钮标题由“Not Reversed”改为“-Reversed-”。<br>● 使序列号为“pediy”。</p><p><code>移去“Okay,for now,mission failed”对话框:</code>用IDA打开ReverseMe(01.exe并进行反汇编。查看“Strings”窗口,双击“Okay,for now,missionaled!”转到定义此字符串的代码处。双击后面的交叉参考,转到调用此字符串的代码处,</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240712152400207.webp" alt="image-20240712152400207"></p><p>按要求,可以用nop指令代替此处的代码,也可用一句跳转指令跳过此提示窗口部分即可。</p><p><code>将输入的字符显示到对话框中:</code>用于获取编辑框字符的函数有Get WindowText、GetDIgItemText等(可在程序的输人表中查看)。在IDA中,输人、输出等函数显示在“Name”窗口中。在“Name”窗口中双击GetWindowTextA函数,来到调用处,会发现有两处调用了此函数,其中第一处比较可疑,具体如下。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240712160139183.webp" alt="image-20240712160139183"></p><p>从上面的分析可知,只要将00401211h处的指令nop掉(即将机器码改成“9090”),ReverseMel01就可以执行这段代码了。用Get WindowTextA函数将文本控件中的内容取出,放入缓冲区4030CCh。用MessageBoxA函数从该缓冲区中读取文本并将其显示到消息框中。</p><p>至此我们已经基本掌握了静态分析相关工具的使用,但这只是第一步。我们还需要掌握一定的逆向分析技能,才能更好地调试和分析程序,探索软件的最深处。</p>]]></content>
</entry>
<entry>
<title>动态分析技术</title>
<link href="/2024/07/26/%E5%8A%A8%E6%80%81%E5%88%86%E6%9E%90%E6%8A%80%E6%9C%AF/"/>
<url>/2024/07/26/%E5%8A%A8%E6%80%81%E5%88%86%E6%9E%90%E6%8A%80%E6%9C%AF/</url>
<content type="html"><![CDATA[<p>动态分析技术中最重要的工具是调试器,分为用户模式和内核模式两种类型。用户模式调试器是指用来调试用户模式应用程序的调试器,工作在<code>Ring3</code>级,例如<code>OllyDbg</code>、<code>x64dbg</code>、<code>Visual C++</code>等编译器自带的调试器。内核模式调试器是指能调试操作系统内核的调试器,例如<code>WinDbg</code>。</p><h3 id="一、OllyDbg"><a href="#一、OllyDbg" class="headerlink" title="一、OllyDbg"></a>一、OllyDbg</h3><ol><li><p>OllyDbg调试器</p><p>OllyDbg结合了动态调试和静态分析,具有GUI界面,非常容易上手,对异常的跟踪处理相当灵活。它的反汇编引擎很强大,可识别数千个被G和Windows烦繁使用的函数,并能将其参数注释出来。它会自动分析函数过程、循环语句、代码中的字符串等。此外,开放式的设计给了这个软件很强的生命力。通过爱好者们的不断修改和扩充,OllyDg的脚本执行能力和开放插件接口使其变得越来越强大。</p></li><li><p>OllyDbg界面</p></li></ol><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240704113302914.webp" alt="image-20240704113302914"></p><p>具体各面板功能请参考OllyDbg的帮助文档。</p><ol start="3"><li><p>基本操作</p><p>此处以(TraceMe.exe)为例。</p><p>分析一个Windows程序要比分析一个DOS程序容易得多,因为在Windows中,只要API函数被使用了,再想对要寻找蛛丝马迹的人隐藏一些信息就比较因难了。因此,在分析一个程序时,以哪个API函数作为切入点就显得比较重要了。如果有一些编程经验,在这方面就更加得心应手了。</p><p>为了便于理解,先简单地看一下TraceMe的序列号验证流程,将用户名与序列号输人文本框,程序调用GetDIgItemTextA函数把字符读出,然后进行计算,最后用lstrcmp函数进行比较。因此,这些调用的函数就是解密跟踪的目标,将这些函数作为断点,跟踪程序的序列号验证过程,就能找出正确的序列号。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240704141526058.webp" alt="image-20240704141526058"></p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240704141456398.webp" alt="image-20240704141456398"></p><p>为了让OllyDbg中断在程序的入口点,在加载程序前必须进行相应的设置。运行OllyDbg,单击“Options”→“Debugging options’”选项,打开调试选项配置对话框。单击“Event”标签,设置OllyDbg对中断入口点、模块加载卸载、线程创建/结束等事件的处理方式,一般只需要将断点设置在“WinMain”处。</p><p>载入程序</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240704145341830.webp" alt="image-20240704145341830"></p><p>从左到右依次为:</p><p>虚拟地址:在一般情况下,同一程序的同一条指令在不同系统环境下此值相同。<br>机器码:就是CPU执行的机器代码。<br>汇编指令:与机器码对应的程序代码。</p><p>寄存器面板上显示了各个寄存器的当前值。寄存器有EAX、ECX、EDX、EBX、ESP、EBP、ESI、EDI和EIP等,它们统称为32位寄存器。ESP为栈指针,指向栈顶,在OllyDbg界面右下角的栈面板上显示了栈的值。另一个重要的寄存器是EIP,它指向当前将要执行的指令,按一下“F7”键将执行一条指令,然后EIP将指向下一条将要执行的指令。在调试时,可以双击这些寄存器,修改寄存器里的值。但是,对EIP寄存器,不能直接修改,需要在反汇编窗口选择新的指令起始地址,例如004013AAh,在其上单击右键,在弹出的快捷菜单中选择“New origin here”(此处为新的EIP)选项,EIP的值将变成40I3AAh,程序将从这条指令开始执行。寄存器下方显示的是标志寄存器,分别是C、P、A、Z、S、T、D、O,它们的值只能是两个数字值0和1,双击数字可以在0和1之间切换。</p></li></ol><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240704145607948.webp" alt="image-20240704145607948"></p><p>单步跟踪:调试器的一个最基本的功能就是动态跟踪。OllyDbg在“Debug”菜单里控制运行的命令,各菜单项都有相应的快捷键。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240704150309636.webp" alt="image-20240704150309636"></p><p>“F7”键和“F8”键的主要区别在于,若遇到call、loop等指令,按“F8”键会路过,按“F7”键会跟进。</p><p>在进入子程序的过程中,若想回看之前单步跟踪的代码,可以按“-”(减号)键;若想让光标回到当前EIP所指向的语句,可以单击C按钮或双击EIP寄存器。</p><p>如果已经进人系统DLL提供的API函数,当要返回应用程序领空时,可以按快捷键“A1t+F9”执行“Execute till user code’”(执行到用户代码)命令。</p><p></p><ol start="4"><li><p>常用断点</p><p>设置断点:断点(breakpoint)是调试器的一个重要功能,可以让程序中断在指定的地方,从而方便地对其进行分析。F2可设置断点,也可取消,也可以双击“Hex dump’”列中相应的行设置断点,再次双击可以取消断点,</p><p>设置断点后,按“Alt+B”快捷键或单击B按钮,打开断点窗口,查看断点明细,这里显示了除硬件断点外的其他断点,其中“Always’”表示断点处于激活状态,“Disable”表示断点停用。按空格键可切换其状态。也可以通过右键快捷菜单管理这些断点。删除断点的快捷是“Del”键。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240704173718233.webp" alt="image-20240704173718233"></p></li></ol><p> </p><p>下面给出一个完整的调试分析过程。取消已经设置的所有断点,在OllyDbg里按“F9”键,运行实例<code>TraceMe.exe</code>。因为Win32程序大量调用了系统提供的API函数,所以,使用合适的API函数设置断点就能很快定位关键代码。获取文本框中的字符,通常使用的API是GetDlgItemText或者Get WindowText函数,也可以发送消息直接获取文本框中的文本。</p><p> <img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240704173905169.webp" alt="image-20240704173905169"> </p><p>在一般情况下,我们事先不会知道程序具体调用了什么函数来处理字符,因此,只能多试几次,找出相关的函数。</p><p>首先,需要在OllyDbg中设一个“陷阱”(或称“断点”)。因为TraceMe是32位ANSI版的程序,所以要在GetDIgItemTextA处设一个断点。按“Ctrl+G”快捷键打开<br>跟随表达式窗口,在文本框中输人“GetDlgItemTextA”</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240704175814241.webp" alt="image-20240704175814241"></p><p>单击“OK”按钮,会来到系统USER32.DLL中的GetDlgItemTextA函数入口处,将地址栏这一列拉宽,会在地址771FDC90h后看到“USER32.GetDlgItemTextA”。程序就是通过这种方式调用Windows操作系统动态链接库USER32.DLL的API的。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240704180324456.webp" alt="image-20240704180324456"></p><p>在771FDC90h处按“F2”键设一个断点,就在GetDlgItemTextA函数入口设了断点。操作系统版本不同,这个函数的入口地址也不同,这与系统动态链接库的版本有关。如果这个函数被调用了,OllyDbg就会中断。</p><p>设定了断点,就可以捕捉任何对GetDlgItemTextA函数的调用了。输入姓名和序列号,单击“Check”按钮,程序将中断在OllyDbg中,位置就在函数GetDlgItemTextA开始的地方。或者按“Ctrl+N”快捷键,获取TraceMe的API名称列表。这个窗口中列出了TraceMe调用的所有系统动态链接库的函数,可切换到相应的代码处。接下来,按“F2”键设置断点。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240705095616926.webp" alt="image-20240705095616926"></p><p>若O川yDbg中已有CmdBar.dll插件,会显示命令行环境。直接在命令行环境中使用bp命令就可以设置断点</p><p></p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240705095641576.webp" alt="image-20240705095641576"></p><ol start="5"><li>调试分析</li></ol><p>按“F8”键单步走出GetDIgItemTextA函数。当然,也可以按“Alt+F9”快捷键回到调用函数的地方。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240708095559387.webp" alt="image-20240708095559387"></p><p>运行Win32 Programmer’s Reference(微软提供的HLP文件,见附件)查看GetDlgItemText函数的参数</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240708095943573.webp" alt="image-20240708095943573"></p><p>此函数的作用是获取对话框文本,函数原型如下。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">UINT <span class="title function_">GetDlgItemText</span><span class="params">(</span></span><br><span class="line"><span class="params"></span></span><br><span class="line"><span class="params">HWND hDlg,</span></span><br><span class="line"><span class="params"> <span class="comment">// handle of dialog box</span></span></span><br><span class="line"><span class="params"> </span></span><br><span class="line"><span class="params"><span class="type">int</span> nIDDlgItem,</span></span><br><span class="line"><span class="params"> <span class="comment">// identifier of control</span></span></span><br><span class="line"><span class="params"> </span></span><br><span class="line"><span class="params">LPTSTR lpString,</span></span><br><span class="line"><span class="params"> <span class="comment">// address of buffer for text</span></span></span><br><span class="line"><span class="params"> </span></span><br><span class="line"><span class="params"><span class="type">int</span> nMaxCount</span></span><br><span class="line"><span class="params"> <span class="comment">// maximum size of string</span></span></span><br><span class="line"><span class="params"> </span></span><br><span class="line"><span class="params">)</span>;</span><br></pre></td></tr></table></figure><p>返回值:如果成功就返回文本长度;如果失败则返回零。</p><p>ANSI版是GetDlgItemTextA函数,Unicode版是GetDlgItemTextW函数。<br>来到TraceMe的领空后,可以按“Alt+B”快捷键打开断点窗口,将GetDIgItemTextA函数处的断点禁止。</p><p>在很多时候,我们必须反复跟踪同一段代码,因此可以先设置一个断点。将光标移到004011B4h处,按“F2”键设置新的断点,以便反复跟踪调试。 </p><p>在阅读这些代码时,需要注意以下几点。</p><p>● 要清楚各API函数的定义(查看相关API手册)。<br>●API函数大都采用stdcall调用约定,即函数入口参数按从右到左的顺序人栈,由被调用者清理栈中的参数,返回值放在eax寄存器中。因此,对相关的API函数,要分析其前面的push指令,这些指令将参数放入栈,以传送给API调用。在整个跟踪过程中要关注栈数据的变化。<br>●C代码中的子程序采用的是C调用约定,函数入口参数按从右到左的顺序人栈,由调用者清理栈中的参数。</p><p>当程序执行到004011B4h处时,栈窗口的数据如图所示。这时,函数所需参数都已经压入栈了。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240708101043078.webp" alt="image-20240708101043078"></p><p>GetWindowText函数执行后,会把取出的文本放到由IpString(LPTSTR是个长指针,指向由空字符终止的字符串)指定的位置。若想看到输入的字符串,跟踪时可在004011B0h处停下,在命令行中执行“deax”命令,或者在eax寄存器上单击右键,执行快捷菜单中的“Follow in Dump”命令,查看数据窗口中的内容。当然,此时数据窗口中没有有价值的东西。继续按“F8”键,单步执行<code>call edi</code></p><p>此时,GetDIgItemTextA函数已将字符串取出,放到eax指向的地址里。数据窗口右边的字符段显示了刚输人的字符“text”,如图所示。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240708101436258.webp" alt="image-20240708101436258"></p><pre><code> 继续往下,有一个对输入长度的判断</code></pre><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240708102335564.webp" alt="image-20240708102335564"></p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240708102537903.webp" alt="image-20240708102537903"></p><p>弹出一个新的窗口</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240708102618265.webp" alt="image-20240708102618265"></p><p>故重新输入“newtext”</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240708102730702.webp" alt="image-20240708102730702"></p><ol start="6"><li>爆破法</li></ol><p>重新回到上面,继续f8</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240708104039129.webp" alt="image-20240708104039129"></p><p>这里是序列号判断的核心</p><p><code>004011f3</code>处若eax=0则注册失败,=1则注册成功</p><p><code>004011f5</code>不跳转则表示注册成功</p><p>此时有两种方法:</p><p>●在OllyDbg寄存器面板上单击标志寄存器ZF(即“Z”),双击1次ZF的数值取反。如果原来的值是1,则执行后的值为0</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240708104651117.webp" alt="image-20240708104651117"></p><p>●在004011F5h处双击或按空格键,输入指令“nop”。这个指令的机器码是“90”,此处用“9090”取代“7437”</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240708104708177.webp" alt="image-20240708104708177"></p><p>修改后的代码如下</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240708104802306.webp" alt="image-20240708104802306"></p><p>此时,输入任意用户名与序列号,TraceMe都会提示注册成功。</p><p>目前修改的是内存中的数据,为了使修改一直有效,必须将这个变化写入磁盘文件。OllyDbg也提供了这个功能。选中修改后的代码,单击右键,执行快捷菜单中的“Copy to executable’”→“Selection”命令</p><p>执行复制到可执行文件中的命令后,将打开文件编辑窗口,点击右键,执行快捷菜单中的“Save File’”命令,即可将修改保存到文件中。这种通过屏蔽程序的某些功能或改变程序流程使程序的保护方式失效的方法称为“爆破”。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240708105051539.webp" alt="image-20240708105051539"></p><p></p><ol start="7"><li><p>算法分析</p><p>接下来去看序列号核心计算部分</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240708105732506.webp" alt="image-20240708105732506"></p></li></ol><p>解释如下</p><p> <img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240708110018334.webp" alt="image-20240708110018334"></p><p>转化为c语言如下</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240708105910191.webp" alt="image-20240708105910191"></p><p>当停到这一句时</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240708111101994.webp" alt="image-20240708111101994"></p><p>来到数据窗口,按“Cr+G”快捷键,输入地址“00405030”,</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240708111034232.webp" alt="image-20240708111034232"></p><p>TraceMe最后调用了Istrcmp函数来比较字符,它的原型如下。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">int</span> <span class="title function_">lstrcmp</span><span class="params">(</span></span><br><span class="line"><span class="params"> LPCSTR lpString1,</span></span><br><span class="line"><span class="params"> LPCSTR lpString2</span></span><br><span class="line"><span class="params">)</span>;</span><br></pre></td></tr></table></figure><p>返回值:如果相等,返回零。</p><p>调用代码如下。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240708111724243.webp" alt="image-20240708111724243"></p><p>因此,当执行到0040138Fh处时,栈窗口中就会显示正确的序列号“5726”。如图所示左边是数据窗口显示的数据,右边是栈窗口,直接将指向的字符串显示出来了。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240708111957808.webp" alt="image-20240708111957808"></p><p>验证一下,果然是这样。</p><p></p><h3 id="二,常用断点"><a href="#二,常用断点" class="headerlink" title="二,常用断点"></a>二,常用断点</h3><p>常用的断点有INT3断点、硬件断点、内存断点、消息斯点等。在调试时,合理使用断点能大大提高效率。</p><ol><li><p>INT3断点</p><p>这是一个常用的断点。在OllyDbg中可以使用bp命令或者“F2”快捷键来设置/取消断点。当执行一个INT3断点时,该地址处的内容被调试器用INT3指令替换了,此时OllyDbg将INT3隐藏,显示出来的仍是中断前的指令。</p><p>这个INT3指令,因其机器码是0xCC,也常被称为“CC指令”。当被调试进程执行INT3指令导致一个异常时,调试器就会捕捉这个异常,从而停在断点处,然后将断点处的指令恢复成原来的指令。当然,如果自己编写调试器,也可用其他指令代替INT3来触发异常。</p><p>使用NT3断点的优点是可以设置无数个断点,缺点是改变了原程序机器码,容易被软件检测到。例如,为了防范API被下断,一些软件会检测API的首地址是否为0xCC(以此判断是否被下断)。用C语言来实现这个检测,方法是取得检测函数的地址,然后读取它的第1个字节,判断它是否等于“CC”。下面这段代码就是对MessageBoxA函数进行的断点检测。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240708112830413.webp" alt="image-20240708112830413"></p><p>程序编译后,对MessageBoxA函数下断,程序将发现自己被设断跟踪了。躲过检测的方法是将断点设在函数内部或末尾,例如将断点设在函数入口的下一行。</p></li><li><p>硬件断点</p><p>硬件断点和DRx调试寄存器有关。在Intel CPU体系架构手册中可以找到对DRx调试寄存器的介绍</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240708113040210.webp" alt="image-20240708113040210"></p><p>DRx调试寄存器共有8个(DR0<del>DR7),每个寄存器的特性如下:<br>● DR0</del>DR3:调试地址寄存器,用于保存需要监视的地址,例如设置硬件断点。<br>● DR4~DR5:保留,未公开具体作用。<br>● DR6:调试寄存器组状态寄存器。<br>● DR7:调试寄存器组控制寄存器。</p><p>硬件断点的原理是使用DRO、DR1、DR2、DR3设定地址,并使用DR7设定状态,因此最多设置4个断点。硬件执行断点与CC断点的作用一样,但因为硬件执行断点不会将指令首字节修改为“CC”,所以更难检测。设断方法是在指定的代码行单击右键,执行快捷菜单中的“Breakpoint’”→“Hardware,,on execution’”(“断点”→“硬件执行”)命令(也可以在命令行中设置“HE地址”)。</p></li></ol><p>为了便于理解,这里演示一下设置硬件断点的过程。加载实例TraceMe.exe,右键单击寄存器面板窗口,执行快捷菜单中的“View debug registers”(查看调试寄存器)命令,接着在004013AAh处设置硬件断点。按“F9”键执行程序,程序就会中断在004013AAh处。查看调试寄存器,发现DR0的值为4013AAh,设置断点后,OllyDbg实际上是将DR0~DR3中的一个设置为“004013AA”,然后在DR7中设置相应的控制位。这样,当被调试进程运行到004013AAh处时,CPU就会向OllyDbg发送异常信息,OllyDbg对该信息进行初步处理后,中断程序,让用户继续操作。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240708114247540.webp" alt="image-20240708114247540"></p><p>删除硬件断点稍有些麻烦。单击菜单项“Debug’”→“Hardware breakpoints’”(“调试”→“硬件断点”),打开硬件断点面板,单击“Delete x”按钮删除相应的硬件断点。<br>OllyDbg提供了一个快捷键“F4”,可以执行到光标所在的行。这也是利用调试寄存器的原理,在中断后自动删除,相当于执行了一次性硬件断点。硬件断点的优点是速度快,在INT3断点容易被发现的地方使用硬件断点会有很好的效果,缺点是最多只能使用4个断点。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240708114554149.webp" alt="image-20240708114554149"></p><ol start="3"><li><p>内存断点</p><p>OllyDbg可以设置内存访问断点或内存写入断点,原理是对所设的地址赋予不可访问不可写属性,这样当访问/写入的时候就会产生异常。OllyDbg截获异常后,比较异常地址是不是断点地址,如果是就中断,让用户继续操作。</p><p>因为每次出现异常时都要通过比较来确定是否应该中断,所以内存断点会降低OllyDbg的执行速度一也许OllyDbg是考虑到执行速度才规定只能下1个内存断点吧。</p><p>程序运行时有3种状态,分别是读取、写入和执行。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240708115026150.webp" alt="image-20240708115026150"></p><p>用OllyDbg重新加载实例TraceMe.exe,看到004013D0h处有一个写内存的指令,代码如下。</p></li></ol><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240708142638318.webp" alt="image-20240708142638318"></p><p>下面用这个地址来演示如何下内存断点。在数据窗口中对00405528h处下内存写断点,将光标移到00405528h处,选中需要下断点的地址区域,单击右键,执行快捷菜单中的“Breakpoint’→“Memory,.on write”(“断点”→“内存写入”)命令</p><p>下内存写断点后,按“F9”键让程序运行,程序会马上中断在“4013D0mov[405528,edx”这行。如果要清除内存断点,可以单击右键,执行快捷菜单中的“Breakpoint’”→“Remove memorbreakpoint’”(“断点”→“删除内存断点”)命令。内存访问断点的操作与此类似。</p><p>在这个场景中,硬件断点也可以实现与内存断点相同的效果。单个硬件写入访问断点可以设置为1字节、2字节或4字节,而且不论选择的数据范围有多大,只有前4个字节会起作用。打开数据窗口,选中需要下断点的地址区域,单击右键,执行快捷菜单中的“Breakpoint’”→“Hardware,onwrite”→“Dword”(“断点”·“硬件写入”→“Dword’”)命令。</p><p>重新加载TraceMe,会发现程序中断在触发硬件写人断点的下一条指令处,所以请记住:硬件访问/写入断点是在触发硬件断点的下一条指令处下断,而内存断点是在触发断点的指令处下断。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240708143917773.webp" alt="image-20240708143917773"></p><p>对代码也可下内存访问断点。在OllyDbg里重新加载实例TraceMe..exe,任意定位一行代码,例如004013D6h处,单击右键,执行快捷菜单中的“Breakpoint’”一→“Memory,on access”(“断点”“内存访问”)命令</p><p>当然,执行内存004013D6h处的代码时需要“访问”它,因此,按“F9”键让实例在OllyDbg里运行,就会中断在004013D6h处的内存访问断点上。这个实验表明,在内存执行的地方也可以通过内存访问来中断。内存断点不修改原始代码,不会像INT3断点那样因为修改代码被程序校验而导致下断失败。因此,在遇到代码校验且硬件断点失灵的情况下,可以使用内存断点。</p><ol start="4"><li>内存访问一次性断点</li></ol><p>Windows对内存使用段页式的管理方式。在OllyDbg里按“Alt+M”快捷键显示内存,可以看到许多个段,每个段都有不可访问、读、写、执行属性。在相应的段上单击右键,会在快捷菜单中发现一个命令“Set break-on-access’”(在访问上设置断点,其快捷键是“F2”,用于对整个内存块设置该类断点。这个断点是一次性断点,当所在段被读取或执行时就会中断。中断发生以后,断点将被删除。如果想捕捉调用或返回某个模块,该类断点就显得特别有用了。右键快捷菜单中的“Set memory breakpoint on access”(设置内存访问断点)命令和“Set break-on-access”命令的功能大致相同,所不同的是前者不是一次性断点。这类断点仅在NT架构下可用。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240708145000914.webp" alt="image-20240708145000914"></p><ol start="5"><li><p>消息断点</p><p>Windows本身是由消息驱动的,如果调试时没有合适的断点,可以尝试使用消息断点。当某个特定窗口函数接收到某个特定消息时,消息断点将使程序中断。消息断点与INT3断点的区别在于:INT3断点可以在程序启动之前设置,消息断点只有在窗口被创建之后才能被设置并拦截消息。<br>当用户单击一个按钮、移动光标或者向文本框中输人文字时,一条消息就会发送给当前窗体。所有发送的消息都有4个参数,分别是1个窗口句柄(hwnd)、1个消息编号(msg)和2个32位长(long)的参数。Windows通过句柄来标识它所代表的对象。例如,在单击某个按钮时,Windows通过句柄来判断单击了哪一个按钮,然后发送相应的消息来通知程序。</p><p>下面用实例TraceMe演示如何设置消息断点。在OllyDbg里运行实例,输入用户名和序列号,单击莱单项“View”→“Windows’”(“查看”→“窗口”)或工具栏中的W按钮,列出窗口相关参数。如果界面上没有内容,应执行右键快捷菜单中的“Actualize’”(刷新)命令。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240708151530825.webp" alt="image-20240708151530825"></p><p>这里列出了所有属于被调试程序窗口及与窗口相关的重要参数,例如按钮、对应的ID及句柄(Handle)等。现在要对“Check”按钮下断点,即当单击该按钮时程序中断。在“Check”条目上单击右键,在弹出的快捷菜单中,执行“Message breakpoint on ClassProc’”(在ClassProc上设置消息断点)命令,会弹出如图所示的设置窗口,下拉列表中显示了文本控件、按钮、鼠标等类型的消息。如果选择第1项“Any Message””,将拦截所有消息。我们在这里关注的消息属于“Button’”(按钮)这一项,当单击按钮并松开时,会发送“WM LBUTTONUP”这个消息。单击下拉菜单,选择“202 WM LBUTTONUP”选项,再单击“OK”按钮,消息断点就设置好了。单击选中“Break on any window”单选按钮,表示程序的任何窗口收到该消息后都会中断。“Log WinProc arguments”是用于记录消息过程函数的参数。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240708151936150.webp" alt="image-20240708151936150"></p><p>回到TraceMe界面,单击“Check”按钮。松开鼠标时,程序将中断在Windows系统代码中,代码如下(不同版本的操作系统,代码会不同)。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240708152033708.webp" alt="image-20240708152033708"></p><p>消息已经捕捉到了,但还处于系统底层代码中,不属于TraceMe主程序的代码,这时企图使用“Alt+f9”或“Crl+F9”快捷键返回TraceMe程序代码领空的操作是徒劳的。</p></li></ol><p>主程序的代码在以00401000h开头的text区块里。从系统代码回到应用程序代码段的时候,正是.text区块代码执行的时候,因此,对,text区块下内存断点就能返回应用程序的代码领空。按“Al+M”快捷键打开内存窗口,对text区块下内存访问断点,然后执行右键快捷菜单中的命令“Setbreak-on-access’”(在访问上设置断点)或按快捷键“F2”</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240708152647327.webp" alt="image-20240708152647327"></p><p>按“F9”键运行程序,程序立即中断在004010D0h处,这里正是程序的消息循环处,代码如下。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240708152855957.webp"></p><p>这段代码是一个消息循环,不停地处理TraceMe主界面的各类消息。此时可能不会直接处理按钮事件。如果是单步跟踪,会进入系统代码。在系统代码里,再次</p><p>按“At+M”快捷键打开内存窗口,对.text区块下内存访问断点。按“f9”键运行,会再次来到代码中。重复这个过程,在一两次中断后就能到达处理按钮的事件代码处 了。“Chek”按钮的事件代码如下。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240709103012918.webp" alt="image-20240709103012918"></p><p>最后,可以将消息断点删除。按“Alt+B”快捷键切换到断点窗口,选中消息断点,直接将其删除。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240709104455427.webp" alt="image-20240709104455427"></p><ol start="6"><li>条件断点</li></ol><p>在调试过程中,我们经常希望断点在满足一定条件时才会中断,这类断点称为条件断点。OllyDbg的条件断点可以按寄存器、存储器、消息等设断。条件断点是一个带有条件表达式的普通INT3断点。当调试器遇到这类断点时,断点将计算表达式的值,如果结果非零或者表达式有效,则断点生效(暂停被调试程序)。</p><p>(1)按寄存器条件中断</p><p>OD打开Conditional_bp.exe,在00401476h处按下设置条件斯点的快捷键“Shift+F2”,在条件文本框内输入条件表达式“eax==0400000”。这样,程序在执行到00401476h处时,如果eax的值为0400000h,OllyDbg将会中断。如果安装了命令行插件,也可在命令行里直接输人如下命令。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240709105246966.webp" alt="image-20240709105246966"></p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240709105049423.webp" alt="image-20240709105049423"></p><p>(2)按存储器条件中断</p><p>在这里用CreateFileA函数进行演示。在实际应用中程序可能会成百上千次调用CreateFileA函数,因此让OllyDbg在CreateFileA函数打开所需文件时中断就显得十分有必要了。CreateFile函数的代码原型如下:</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">HANDLE <span class="title function_">CreateFileA</span><span class="params">(</span></span><br><span class="line"><span class="params"> [in] LPCSTR lpFileName,</span></span><br><span class="line"><span class="params"> [in] DWORD dwDesiredAccess,</span></span><br><span class="line"><span class="params"> [in] DWORD dwShareMode,</span></span><br><span class="line"><span class="params"> [in, optional] LPSECURITY_ATTRIBUTES lpSecurityAttributes,</span></span><br><span class="line"><span class="params"> [in] DWORD dwCreationDisposition,</span></span><br><span class="line"><span class="params"> [in] DWORD dwFlagsAndAttributes,</span></span><br><span class="line"><span class="params"> [in, optional] HANDLE hTemplateFile</span></span><br><span class="line"><span class="params">)</span>;</span><br></pre></td></tr></table></figure><p>运行实例Conditional_bp,对CreateFileA函数设断。单击“OpenTest’”按钮,如图是当OllyDg中断时从栈中看到的情形,左侧标出了各参数相对于当前ESP的地址。打开这个功能的方法是:在栈窗口中单击右键,执行快捷菜单中的“Address’”→“Relative to ESP”(“地址”+“相对于ESP”)命令。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240709111019091.webp" alt="image-20240709111019091"></p><p>CreateFileA函数采用标准调用约定,参数按从右到左的顺序人栈。因为在函数刚执行时EBP栈结构还未建立,所以只能用ESP访问这些参数。CreateFileA函数的第1个参数“FileName”是文件名指针。在OllyDbg里,如果要得到第1个参数的内存地址,可以使用“[ESP+4]”;如果还要得到此地址指向的字符串,就必须使用“[ESP+4]”。</p><p>该实例中调用了4次CreateFileA函数,假设要在该函数打开c:\1212.txt时下断,则”shitf+f2“,输入<code>[STRING [esp+4]]=="c:\\1212.txt"</code>(“STRING”前缀在OllyDbg中的解释是“以零结尾的ASCIl字符串”,如果是Unicode字符串,则改为UNICODE)</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240709111858405.webp" alt="image-20240709111858405"></p><ol start="7"><li><p>条件记录断点</p><p>条件记录断点除了具有条件断点的作用,还能记录断点处函数表达式或参数的值。也可以设置通过断点的次数,每次符合暂停条件时,计数器的值都将减1。例如,要记录Conditional_bp实例调用CreateFileA函数的情况,可在CreateFileA函数的第1行按快捷键Shft+F4”,打开条件记录窗口.在“Condition”(条件)域中输入要设置的条件表达式。在“Explanation”(说明)域中设置一个名称。“Expression”(表达式)域中是要记录的内容的条件,只能设置1个表达式,例如要记录EAX的值,可以输“EAX”。“Decode value of expression as’”(解码表达式的值)下拉列表中可以对记录的数据进行分析。例如,在条件记录窗口中,如果“Expression”域中填写的是[esp+4],则要在该下拉列表中选择“Pointer to ASCIIString”(指向AsCIl字符串的指针)选项,才能得到正确的结果,其功能相当于“STRING”前缀.</p><p>“Pause program”(暂停程序)域用于设置OllyDhg遇到断点时是否中断。“Log value of expression(记录表达式的值)域用于设置遇到断点时是否记录表达式的值。“Log function arguments’”(记录数参数)域用于设置遇到断点时是否记录函数的参数。对这3个域,可以根据需要设置“Never’”(不)、“On condition”(按条件)或“Always”(永远)。<br>条件记录断点允许向插件传递1个或多个命令。当应用程序因条件断点暂停,并且断点中包传递给插件的命令时,都会调用回调函数ODBG_Plugincmd(int reason,t_reg*registers,char*cmd)。如,当程序暂停时,传送命令“d esp”给CmdBar插件,只要在窗口的文本框中输”.d esp”,当条件断点断下时,就会执行“d esp”命令。这时,我们就可以在数据窗口中看到ESP地址处的数据了。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240709115926084.webp" alt="image-20240709115926084"></p><p>设置好条件记录断点,单击实例Conditional_.bp的“OpenTest”按钮,运行后,OllyDbg会在“Logdata”窗口(快捷键“Alt+L”)记录数据。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240709115945123.webp" alt="image-20240709115945123"></p></li></ol><h3 id="三、x64dbg"><a href="#三、x64dbg" class="headerlink" title="三、x64dbg"></a>三、x64dbg</h3><p>x64bg是一款开源的调试器,既支持32位和64位程序的调试,也支持插件的功能扩展,类似于C的表达式解析器,提供了图形模式代码流程、可调试的脚本支持等强大的功能。其界面及操作方法与OllyDbg相似,很容易上手。x32dbg.exe适用于32位程序的调试;x64dbg.exe适用于64位程序的调试。</p><p>本节以一个64位的程序为例演示x64dbg的基本用法,用x64dbg加载TraceMe64.exe,默认会中断在系统断点处。选择“选项”+“设置”选项,去除“系统断点”,可以直接中断在程序人口点。按“f9”键让TraceMe64运行,输入用户名和序列号(用户名为“newtext”,序列号为“123456”)。按“Ctrl+G”快捷键,打开表达式窗口,输入函数名“GetDlgItemTextA”,来到该函数人口处,按“F2”键设置断点。也可以直接在命令行环境中输入“bp GetDlgItemTextA”命令设置断点。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240709144351771.webp" alt="image-20240709144351771"></p><p>设好断点后,单击TraceMe的“Check”按钮,程序将中断在GetDlgItemTextA函数的人口处。按“F8”键走出这个函数,回到TraceMe64的代码中,代码如下。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240709144550651.webp" alt="image-20240709144550651"></p><p>在00007FF6F62715CFh这一行,在内存数据窗口输入“dump rdx’”,即可查看rdx指向的字符串,这个字符串就是真正的序列号</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240709144934658.webp" alt="image-20240709144934658"></p><p>下面介绍一下x64dbg在程序运行中使用消息断点来定位特定函数的方法。,单击TraceMe64的“Check”按钮,转到句柄选项卡,单击右键刷新窗口以获取句柄列表。在列表中找到“Check”按钮,右键单击它并选择消息断点,设置当单击左键时发送WM LBUTTONUP消息的程序将会停止,</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240709145718162.webp" alt="image-20240709145718162"></p><p>设置断点之后,单击“Check”按钮,程序会中断在系统user32.dll的相关代码处,在x64dbg主界面的标题栏中会显示当前停在哪个模块处</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240709152531979.webp" alt="image-20240709152531979"></p><p>因为我们的目的是回到TraceMe64主模块的代码中,所以可以多次使用“Ctrl+F9”快捷键切换到TraceMe64的代码领空。但实际情况是一直在系统内部代码中循环,不容易跳出来。我们可以用一个小技巧快速回到被调试的代码模块。切换到内存布局窗口,找到TraceMe64.exe模块,在其代码段text上单击右键,设置一次性内存断点,所示。按“F9”键执行程序,瞬间就会中断在被调试的模块处,接下来就可以按正常方式进行调试了。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240709153921363.webp" alt="image-20240709153921363"></p><h3 id="四、MDedug"><a href="#四、MDedug" class="headerlink" title="四、MDedug"></a>四、MDedug</h3><p>MDebug的界面风格和快捷键与VC相似,分为视图窗口和浮动停靠窗口。视图窗口用于显示信息量较大、复杂程度较高的内容,例如反汇编、模块列表、内存搜素、脚本编写等窗口:浮动停靠窗口用于显示信息量较少,但在调试过程中随时需要查看的内容,例如寄存器、内存显示、输出等窗口。</p><ol><li><p>反汇编窗口</p><p>在反汇编窗口中显示了被调试程序的代码,将光标移到不同的元素上,光标下方会智能显示相应的内容。如果将光标移到函数上,在弹出的气泡窗口中会显示该函数的反汇编内容。在反汇编窗口选中任意的寄存器、地址、函数,按“Eter”键,也可以跳转到相应的目标地址处。选中反汇编内容,单击右键快捷菜单中的“格式化复制”选项,可以对选中的反汇编机器码进行格式化复制。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240709160325025.webp" alt="image-20240709160325025"></p><p>在反汇编窗口的左侧双击,可以设置取消断点。选中函数名称或者反汇编窗口左侧的地址,可以进行函数名称的创建与编辑。</p></li><li><p>内存显示窗口</p></li></ol><p>MDebug支持多Tab显示8个内存窗口,为内存复制、内存修改提供了丰富的功能。通过“Tab”键可以进行BYTE/WORD/DWORD/QWORD显示的切换。支持直接在内存窗口中修政内存数据,也可以通过双击内存数据一次性修改。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240709160907108.webp" alt="image-20240709160907108"></p><ol start="3"><li>输出窗口</li></ol><p>输出窗口用于显示调试信息或者脚本的输出。</p><ol start="4"><li>表达式</li></ol><p>在调试过程中,经常需要查看内存地址或反汇编地址,这些地址在很多情况下需要通过一些表达式参与计算。MDebug支持类似C/C+形式的表达式运算,复杂表达式大都由单个基本元素与“(“ “[” “]” “+” “_” “*” “/” “%” ”^” “|” “&” “&&”等符号组合而成</p><ol start="5"><li><p>调试</p><p>MDebug支持多种调试模式,例如启动一个程序进行调试、调试DLL模块、附加(Attach)一个正在运行的程序、调试服务、调试一段独立的Shelleode,同时支持子进程调试。</p><p>选择菜单项“文件”→“调试进程”,在打开的对话框中选中“服务”单选按钮,就可以进行服务调试了。</p><p><code>调试dll</code>:在软件分析调试的过程中,有时会发现真正需要分析的功能位于某个DLL的输出函数中。MDedbg支持直接打开DLL进行调试,并允许直接调试DLL的输出函数,如果在被调试程序运行过程中,希望调试器能在特定的DLL模块被加载时中断在模块的人口处,可单击“选项”+“调试”菜单项,选择“在模块载人时停止在模块入口点”选项。</p><p><code>调试子进程</code>:在调试过程中,经常遇到被调试程序在中途启动了一个子进程,需要从人口处开始调试子进程的情况。MDebug可以有效解决子进程调试的难题。单击“选项”+“调试”菜单项,选择“调试子进程”选项,在子进程被侧建或启动的时候,MDeg会自动启动,并开蛤对子进程进行调试。</p></li></ol>]]></content>
</entry>
<entry>
<title>熊猫烧香病毒分析</title>
<link href="/2024/07/26/%E7%86%8A%E7%8C%AB%E7%83%A7%E9%A6%99%E7%97%85%E6%AF%92%E5%88%86%E6%9E%90/"/>
<url>/2024/07/26/%E7%86%8A%E7%8C%AB%E7%83%A7%E9%A6%99%E7%97%85%E6%AF%92%E5%88%86%E6%9E%90/</url>
<content type="html"><![CDATA[<h2 id="一,查壳"><a href="#一,查壳" class="headerlink" title="一,查壳"></a>一,查壳</h2><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240307200721943.webp" alt="image-20240307200721943"></p><p>无壳</p><p>采用Delphi Borland Delphi 6.0-7.0编写</p><p>Delphi编译器倾向于使用寄存器传递前几个参数,寄存器传递可以比堆栈传递更快。</p><p>而C++的参数传递方式是在进行函数调用之前,会使用PUSH指令将参数按顺序推入堆栈,然后通过CALL指令调用函数。函数执行完毕后,通过调整堆栈指针来清理堆栈。</p><h2 id="二,病毒样本分析"><a href="#二,病毒样本分析" class="headerlink" title="二,病毒样本分析"></a>二,病毒样本分析</h2><h3 id="自校验"><a href="#自校验" class="headerlink" title="自校验"></a>自校验</h3><p>打开od,载入病毒样本,查看程序入口点</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240307205445686.webp" alt="image-20240307205445686"></p><p>第一个函数<code>00404E8</code>,步入查看</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240308163810820.webp" alt="image-20240308163810820"></p><p>通过调用<code>GetModuleHandleA()</code>获得程序基地址(及程序实例句柄)</p><p>接下来第二个函数<code>004049E8</code>,这个函数被连续调用了两次,每次调用之前都传递了两个参数</p><p>在内存窗口中看到传递的为字符串</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240308164202849.webp" alt="image-20240308164202849"></p><p>进入函数查看,<img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240308172402899.webp" alt="image-20240308172402899"></p><p>内部又调用了三个函数,发现第二个调用的函数的功能为字符串的复制</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240308173456349.webp" alt="image-20240308173456349"></p><p>在内存中查看</p><p><strong><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240308173858186.webp" alt="image-20240308173858186"></strong></p><p>这是调用一次这个函数,复制了一部分字符串,总共调用了两次,全部调用完后是这样的(ps:重新启动了一下,存储地址发生变化了)</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240308174606279.webp" alt="image-20240308174606279"></p><p>继续往下看,发现两端相似的代码,但是传递的参数不同,调用的函数相同</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240308174824973.webp" alt="image-20240308174824973"></p><p>先分析<code>405360</code></p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240308175816339.webp" alt="image-20240308175816339"></p><p>两个参数分别指向两个字符串</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240308175846311.webp" alt="image-20240308175846311"></p><p>连个字符串经过如下算法加密后得到一个字符串为</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240308175718734.webp" alt="image-20240308175718734"></p><p>存储地跟在前面存的地址的后面(别问为啥地址又改了,问就是ctrl+f2了)</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240308180425135.webp" alt="image-20240308180425135"></p><p>可以在<code>0040268c</code>处打断点,然后再内存窗口中查看字符串加密过程,最后变为这个样子</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240308180806773.webp" alt="image-20240308180806773"></p><p>接下来看<code>404018</code></p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240308180928974.webp" alt="image-20240308180928974"></p><p>此时传递的两个参数</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240308181012038.webp" alt="image-20240308181012038"></p><p>一个是最开始存进去的,一个是刚刚加密后生成的,猜测是比较两个字符串,</p><p>跑一遍,果然</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240308191658811.webp" alt="image-20240308191658811"></p><p>这个循环遍历两个位置存的字符串,然后进行比较。</p><p>故函数<code>405360</code>是一个加密函数,而函数<code>404018</code>是一个字符串比较函数。从而判断源文件是否被修改。</p><p>下一段相似的代码也一样,检测的字符串发生变化</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240308192300541.webp" alt="image-20240308192300541"></p><p>这两次自校验过程后,继续向下,找到3个连续的调用函数</p><h3 id="主要功能函数"><a href="#主要功能函数" class="headerlink" title="主要功能函数"></a>主要功能函数</h3><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240308193722838.webp" alt="image-20240308193722838"></p><p>程序再调用这三个函数时会进入消息循环,所以可以得知,这三个函数为这个病毒的主要功能</p><h4 id="病毒备份"><a href="#病毒备份" class="headerlink" title="病毒备份"></a>病毒备份</h4><p>进入第一个函数<code>00408024</code></p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240308194644081.webp" alt="image-20240308194644081"></p><p>一堆函数,这里进入查看就不展示了,都做了注释</p><p>先调用<code>GetMoudleFileNameA( )</code>函数获得程序完整路径,然后在完整路径的基础上去掉程序名得到一个字符串后与<code>Desktep.ini</code>连接。并通过<code>FindFirstFile( )</code>函数寻找此文件。如果找到了则删除此文件,否则跳过继续执行。</p><p><code>je setup.00408110</code>如果没有找到文件就跳转</p><p>继续往下</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240308200942272.webp" alt="image-20240308200942272"></p><p>调用<code>ReadFile()</code>把自己读到内存中,然后调用<code>GetFileSize()</code>获得文件的大小接着其会判断文件的最后一个字节是不是等于1,意思是等与1就证明其被感染文件随之去执行上方一部分代码块,否则则继续执行。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240308202042166.webp" alt="image-20240308202042166"></p><p>往下分析,程序会会获取系统路径并与<code>drivers/</code>和<code>spcolsv.exe</code>连接成路径并于程序自身路径比较看是否相等,相等则会跳转。</p><p>因为程序路径与此路径不匹配,所以其不会跳转。</p><p>接下来程序会利用OpenProcrss()函数来提成程序的权限,然后会调用CopyFile()函数把自身复制一份到系统路径的drivers文件夹下,并命名为spcolsv.exe. 创建完之后程序会调用WinExec( )运行此程序,并结束进程。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240309103020380.webp" alt="image-20240309103020380"></p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240309103054763.webp" alt="image-20240309103054763"></p><p>panda.exe主要是为了在系统目录的driver文件夹下创建病毒的副本spcolsv.exe文件并运行它。这样其在上方判断路径是否相等时就会成立,从而是程序继续往下执行。</p><p>再次分析病毒样本,运行来到路径判断处时把ZF取反即可让程序认为自己就时系统目录drivers文件夹下的spcolsv.exe程序了</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240309103326369.webp" alt="image-20240309103326369"></p><p>这里是判断</p><p><strong><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240309103420720.webp" alt="image-20240309103420720"></strong></p><p>修改zf的值,就可以继续往下分析</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240309103508595.webp" alt="image-20240309103508595"></p><h4 id="完全感染"><a href="#完全感染" class="headerlink" title="完全感染"></a>完全感染</h4><p>来到第二个函数</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240309103639935.webp" alt="image-20240309103639935"></p><p>进来之后也是有三个函数调用,一个一个去分析</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240309103830654.webp" alt="image-20240309103830654"></p><p>第一个进来,调用了<code>CreateThread( )</code>创建了一个线程。线程函数为<code>0040A180</code>,我们可以在此函数入口处下断点</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240309104207972.webp" alt="image-20240309104207972"></p><p>这里涉及到多线程调试,在创建的线程入口处下一个断点,在运行完此函数返回后直接跳过后两个函数,这样第二个功能函数返回后,也不执行第三个功能函数,直接让程序进入消息循环。这样当程序返回消息循环后F9运行程序,这是那个新创建的线程已经启动,并且程序断在了我们设置的线程函数入口断点处。</p><p>这里又调用了俩函数,反汇编查看<code>00407834</code></p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240309105304009.webp" alt="image-20240309105304009"></p><p>其调用了GetDriveType( ),跟着跑一编,发现会循环遍历A到Z盘,把存在的盘符保存到<code>01F8025C</code>及以后</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240309105504878.webp" alt="image-20240309105504878"></p><p>接着会把存在的盘符与“:\”连接在一起。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240309110305606.webp" alt="image-20240309110305606"></p><p>接着会调用<code>004091DC</code>函数,本想着直接跳过的,可是程序停止运行了,进去看看</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240309130553640.webp" alt="image-20240309130553640"></p><p>把刚才找到存在的盘符与“*_*”连接后,调用<code>FindFirstFileA()</code>寻找任意文件,如果找不到文件就跳转结束。</p><p>这里有文件,不会跳转</p><h4 id="全盘感染"><a href="#全盘感染" class="headerlink" title="全盘感染"></a>全盘感染</h4><p>接下来是一堆相似的代码</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240309150203367.webp" alt="image-20240309150203367"></p><p>大概功能是对寻找到的文件,获得其扩展名,与对应的字符串进行比较,<img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240309150832440.webp" alt="image-20240309150832440"></p><p>这里特殊一点,如果后缀等于<code>GHO</code>的文件调用<code>DeleteFile()</code>删除,(<code>GHO</code>文件时系统备份文件,其是防止系统恢复)</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240309173854494.webp" alt="image-20240309173854494"></p><p>如果是<code>html,asp,php,jsp,aspx</code>的后缀,跳转到<code>00407AF4</code></p><p>如果是<code>exe,scr,pif,com</code>的后缀,跳转到<code>004040CC</code></p><p>继续往下</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240309175321896.webp" alt="image-20240309175321896"></p><p>先在已存在得盘得根目录下寻找setup.exe文件与autorun.inf文件。然后如果没有寻找到此文件就跳转。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240309175508143.webp" alt="image-20240309175508143"></p><p>修改zf的值,跟着跳转,把正在运行得病毒文件复制为<code>setup.exe</code></p><p>接下来继续运行,回到了刚刚的跳转,这次不修改,进去</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240309180211139.webp" alt="image-20240309180211139"></p><p>创建<code>c:\autorun.inf</code>文件,往文件中写入一下内容后,将<code>setup.exe</code>与<code>autorun.inf</code>的属性设为隐藏。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240309180317378.webp" alt="image-20240309180317378"></p><p>这个函数就差不多了</p><h4 id="局域网传播"><a href="#局域网传播" class="headerlink" title="局域网传播"></a>局域网传播</h4><p>接下来看第二个函数的最后一个函数</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240309180513296.webp" alt="image-20240309180513296"></p><p>进入此函数,发现其会创建线程,而且是循环创建10次。为了便于对此线程回调函数得分析,在其执行一次创建后就让其返回到消息循环中。</p><p>并在线程回调函数处下断点,运行程序将会断在此断点处。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240309180950887.webp" alt="image-20240309180950887"></p><p>程序停到此线程回调函数入口点后,继续分析</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240309181227361.webp" alt="image-20240309181227361"></p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240309181405328.webp" alt="image-20240309181405328"></p><p>发现此函数会通过端口139和端口445进行区域网传播</p><p>这样此函数就分析完了,到此为止我们把前两个功能函数已经分析完了,</p><h3 id="设置注册表与关闭杀软"><a href="#设置注册表与关闭杀软" class="headerlink" title="设置注册表与关闭杀软"></a>设置注册表与关闭杀软</h3><p>接下来看第三个函数,重新加载程序,修改eip到第三个函数那里,进入函数</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240310162731030.webp" alt="image-20240310162731030"></p><p>发现设置了4个计时器,通过4个计时器的回调函数来完成具体功能</p><p>在其计时器回调函数处下断点后我们运行完一个计时器设置函数后直接来到消息循环处。但是我们要注意其有的计时器设置得时间周期较长,我们可以在调用时把时间周期参数该小点。</p><p>在第一个调用函数入口点打一个断点,运行到断点处,我们发现其共有3种操作 </p><p>一是遍历杀毒软件并关闭</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240310202538028.webp" alt="image-20240310202538028"></p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240310202556982.webp" alt="image-20240310202556982"></p><p>二是设置特权</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240310203215302.webp" alt="image-20240310203215302"></p><p>三是关闭任务管理器等一些程序</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240310203333906.webp" alt="image-20240310203333906"></p><p>回到主线程</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240310203942540.webp" alt="image-20240310203942540"></p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240310204236164.webp" alt="image-20240310204236164"></p><p>调用<code>RegCreatekeyExA()---->RegSetValaueExA---->RegCloseKey()</code>来设置两个启动项,第一个是用来在开机时自动启动病毒。另一个是设置使用户无法查看隐藏文件</p><p>接下来看第二个计时器</p><p>调用第二个计时器设置函数时把其时间周期参数该小点,改为1000(1s)。然后在分析运行程序。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240310205107741.webp" alt="image-20240310205107741"></p><p>继续运行,一样的设置回调函数,再创建的线程中下断点,运行程序</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240310205307137.webp" alt="image-20240310205307137"></p><p>调用<code>00403CDC</code>函数将一段密文与“xboy”进行一定得运算,算法和文件自效验时用的一样。最后得到一个网址。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240310210204721.webp" alt="image-20240310210204721"></p><p>接下来回从刚才产生得那个网址上试图从网站读取到网页源代码并且运行代码</p><p>第三个计时器回调函数‘</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240310210422224.webp" alt="image-20240310210422224"></p><p>这里面又创建了两个回调函数</p><p>反汇编查看第二个线程得回调函数发现其利用cmd命令删除了共享文件</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240310210821427.webp" alt="image-20240310210821427"></p><p>第四个回调函数</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240310211349913.webp" alt="image-20240310211349913"></p><p>一样,进下一个线程</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240310211711864.webp" alt="image-20240310211711864"></p><p>主要是删除一些服务和杀毒软件得启动项</p><p>好嘞,到这里熊猫烧香就分析的差不多了,第一次分析病毒,有什么不对的地方还请师傅们多多指教。</p>]]></content>
</entry>
<entry>
<title>OD之断点一步到胃</title>
<link href="/2024/07/25/OD%E4%B9%8B%E6%96%AD%E7%82%B9%E4%B8%80%E6%AD%A5%E5%88%B0%E8%83%83/"/>
<url>/2024/07/25/OD%E4%B9%8B%E6%96%AD%E7%82%B9%E4%B8%80%E6%AD%A5%E5%88%B0%E8%83%83/</url>
<content type="html"><![CDATA[<p>常用的断点有INT3断点、硬件断点、内存断点、消息断点等。在调试时,合理使用断点能大大提高效率。</p><ol><li><p>INT3断点</p><p>这是一个常用的断点。在OllyDbg中可以使用bp命令或者“F2”快捷键来设置/取消断点。当执行一个INT3断点时,该地址处的内容被调试器用INT3指令替换了,此时OllyDbg将INT3隐藏,显示出来的仍是中断前的指令。</p><p>这个INT3指令,因其机器码是0xCC,也常被称为“CC指令”。当被调试进程执行INT3指令导致一个异常时,调试器就会捕捉这个异常,从而停在断点处,然后将断点处的指令恢复成原来的指令。当然,如果自己编写调试器,也可用其他指令代替INT3来触发异常。</p><p>使用NT3断点的优点是可以设置无数个断点,缺点是改变了原程序机器码,容易被软件检测到。例如,为了防范API被下断,一些软件会检测API的首地址是否为0xCC(以此判断是否被下断)。用C语言来实现这个检测,方法是取得检测函数的地址,然后读取它的第1个字节,判断它是否等于“CC”。下面这段代码就是对MessageBoxA函数进行的断点检测。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240708112830413.webp" alt="image-20240708112830413"></p><p>程序编译后,对MessageBoxA函数下断,程序将发现自己被设断跟踪了。躲过检测的方法是将断点设在函数内部或末尾,例如将断点设在函数入口的下一行。</p></li><li><p>硬件断点</p><p>硬件断点和DRx调试寄存器有关。在Intel CPU体系架构手册中可以找到对DRx调试寄存器的介绍</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240708113040210.webp" alt="image-20240708113040210"></p><p>DRx调试寄存器共有8个(DR0<del>DR7),每个寄存器的特性如下:<br>● DR0</del>DR3:调试地址寄存器,用于保存需要监视的地址,例如设置硬件断点。<br>● DR4~DR5:保留,未公开具体作用。<br>● DR6:调试寄存器组状态寄存器。<br>● DR7:调试寄存器组控制寄存器。</p><p>硬件断点的原理是使用DRO、DR1、DR2、DR3设定地址,并使用DR7设定状态,因此最多设置4个断点。硬件执行断点与CC断点的作用一样,但因为硬件执行断点不会将指令首字节修改为“CC”,所以更难检测。设断方法是在指定的代码行单击右键,执行快捷菜单中的“Breakpoint’”→“Hardware,,on execution’”(“断点”→“硬件执行”)命令(也可以在命令行中设置“HE地址”)。</p></li></ol><p>为了便于理解,这里演示一下设置硬件断点的过程。加载实例TraceMe.exe,右键单击寄存器面板窗口,执行快捷菜单中的“View debug registers”(查看调试寄存器)命令,接着在004013AAh处设置硬件断点。按“F9”键执行程序,程序就会中断在004013AAh处。查看调试寄存器,发现DR0的值为4013AAh,设置断点后,OllyDbg实际上是将DR0~DR3中的一个设置为“004013AA”,然后在DR7中设置相应的控制位。这样,当被调试进程运行到004013AAh处时,CPU就会向OllyDbg发送异常信息,OllyDbg对该信息进行初步处理后,中断程序,让用户继续操作。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240708114247540.webp" alt="image-20240708114247540"></p><p>删除硬件断点稍有些麻烦。单击菜单项“Debug’”→“Hardware breakpoints’”(“调试”→“硬件断点”),打开硬件断点面板,单击“Delete x”按钮删除相应的硬件断点。<br>OllyDbg提供了一个快捷键“F4”,可以执行到光标所在的行。这也是利用调试寄存器的原理,在中断后自动删除,相当于执行了一次性硬件断点。硬件断点的优点是速度快,在INT3断点容易被发现的地方使用硬件断点会有很好的效果,缺点是最多只能使用4个断点。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240708114554149.webp" alt="image-20240708114554149"></p><ol start="3"><li><p>内存断点</p><p>OllyDbg可以设置内存访问断点或内存写入断点,原理是对所设的地址赋予不可访问不可写属性,这样当访问/写入的时候就会产生异常。OllyDbg截获异常后,比较异常地址是不是断点地址,如果是就中断,让用户继续操作。</p><p>因为每次出现异常时都要通过比较来确定是否应该中断,所以内存断点会降低OllyDbg的执行速度一也许OllyDbg是考虑到执行速度才规定只能下1个内存断点吧。</p><p>程序运行时有3种状态,分别是读取、写入和执行。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240708115026150.webp" alt="image-20240708115026150"></p><p>用OllyDbg重新加载实例TraceMe.exe,看到004013D0h处有一个写内存的指令,代码如下。</p></li></ol><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240708142638318.webp" alt="image-20240708142638318"></p><p>下面用这个地址来演示如何下内存断点。在数据窗口中对00405528h处下内存写断点,将光标移到00405528h处,选中需要下断点的地址区域,单击右键,执行快捷菜单中的“Breakpoint’→“Memory,.on write”(“断点”→“内存写入”)命令</p><p>下内存写断点后,按“F9”键让程序运行,程序会马上中断在“4013D0mov[405528,edx]”这行。如果要清除内存断点,可以单击右键,执行快捷菜单中的“Breakpoint’”→“Remove memorbreakpoint’”(“断点”→“删除内存断点”)命令。内存访问断点的操作与此类似。</p><p>在这个场景中,硬件断点也可以实现与内存断点相同的效果。单个硬件写入访问断点可以设置为1字节、2字节或4字节,而且不论选择的数据范围有多大,只有前4个字节会起作用。打开数据窗口,选中需要下断点的地址区域,单击右键,执行快捷菜单中的“Breakpoint”→“Hardware,onwrite”→“Dword”(“断点”·“硬件写入”→“Dword’”)命令。</p><p>重新加载TraceMe,会发现程序中断在触发硬件写人断点的下一条指令处,所以请记住:硬件访问/写入断点是在触发硬件断点的下一条指令处下断,而内存断点是在触发断点的指令处下断。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240708143917773.webp" alt="image-20240708143917773"></p><p>对代码也可下内存访问断点。在OllyDbg里重新加载实例TraceMe..exe,任意定位一行代码,例如004013D6h处,单击右键,执行快捷菜单中的“Breakpoint”一→“Memory,on access”(“断点”“内存访问”)命令</p><p>当然,执行内存004013D6h处的代码时需要“访问”它,因此,按“F9”键让实例在OllyDbg里运行,就会中断在004013D6h处的内存访问断点上。这个实验表明,在内存执行的地方也可以通过内存访问来中断。内存断点不修改原始代码,不会像INT3断点那样因为修改代码被程序校验而导致下断失败。因此,在遇到代码校验且硬件断点失灵的情况下,可以使用内存断点。</p><ol start="4"><li>内存访问一次性断点</li></ol><p>Windows对内存使用段页式的管理方式。在OllyDbg里按“Alt+M”快捷键显示内存,可以看到许多个段,每个段都有不可访问、读、写、执行属性。在相应的段上单击右键,会在快捷菜单中发现一个命令“Set break-on-access’”(在访问上设置断点,其快捷键是“F2”,用于对整个内存块设置该类断点。这个断点是一次性断点,当所在段被读取或执行时就会中断。中断发生以后,断点将被删除。如果想捕捉调用或返回某个模块,该类断点就显得特别有用了。右键快捷菜单中的“Set memory breakpoint on access”(设置内存访问断点)命令和“Set break-on-access”命令的功能大致相同,所不同的是前者不是一次性断点。这类断点仅在NT架构下可用。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240708145000914.webp" alt="image-20240708145000914"></p><ol start="5"><li><p>消息断点</p><p>Windows本身是由消息驱动的,如果调试时没有合适的断点,可以尝试使用消息断点。当某个特定窗口函数接收到某个特定消息时,消息断点将使程序中断。消息断点与INT3断点的区别在于:INT3断点可以在程序启动之前设置,消息断点只有在窗口被创建之后才能被设置并拦截消息。<br>当用户单击一个按钮、移动光标或者向文本框中输人文字时,一条消息就会发送给当前窗体。所有发送的消息都有4个参数,分别是1个窗口句柄(hwnd)、1个消息编号(msg)和2个32位长(long)的参数。Windows通过句柄来标识它所代表的对象。例如,在单击某个按钮时,Windows通过句柄来判断单击了哪一个按钮,然后发送相应的消息来通知程序。</p><p>下面用实例TraceMe演示如何设置消息断点。在OllyDbg里运行实例,输入用户名和序列号,单击莱单项“View”→“Windows’”(“查看”→“窗口”)或工具栏中的W按钮,列出窗口相关参数。如果界面上没有内容,应执行右键快捷菜单中的“Actualize’”(刷新)命令。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240708151530825.webp" alt="image-20240708151530825"></p><p>这里列出了所有属于被调试程序窗口及与窗口相关的重要参数,例如按钮、对应的ID及句柄(Handle)等。现在要对“Check”按钮下断点,即当单击该按钮时程序中断。在“Check”条目上单击右键,在弹出的快捷菜单中,执行“Message breakpoint on ClassProc”(在ClassProc上设置消息断点)命令,会弹出如图所示的设置窗口,下拉列表中显示了文本控件、按钮、鼠标等类型的消息。如果选择第1项“Any Message””,将拦截所有消息。我们在这里关注的消息属于“Button’”(按钮)这一项,当单击按钮并松开时,会发送“WM LBUTTONUP”这个消息。单击下拉菜单,选择“202 WM LBUTTONUP”选项,再单击“OK”按钮,消息断点就设置好了。单击选中“Break on any window”单选按钮,表示程序的任何窗口收到该消息后都会中断。“Log WinProc arguments”是用于记录消息过程函数的参数。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240708151936150.webp" alt="image-20240708151936150"></p><p>回到TraceMe界面,单击“Check”按钮。松开鼠标时,程序将中断在Windows系统代码中,代码如下(不同版本的操作系统,代码会不同)。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240708152033708.webp" alt="image-20240708152033708"></p><p>消息已经捕捉到了,但还处于系统底层代码中,不属于TraceMe主程序的代码,这时企图使用“Alt+f9”或“Crl+F9”快捷键返回TraceMe程序代码领空的操作是徒劳的。</p></li></ol><p>主程序的代码在以00401000h开头的text区块里。从系统代码回到应用程序代码段的时候,正是.text区块代码执行的时候,因此,对,text区块下内存断点就能返回应用程序的代码领空。按“Al+M”快捷键打开内存窗口,对text区块下内存访问断点,然后执行右键快捷菜单中的命令“Setbreak-on-access’”(在访问上设置断点)或按快捷键“F2”</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240708152647327.webp" alt="image-20240708152647327"></p><p>按“F9”键运行程序,程序立即中断在004010D0h处,这里正是程序的消息循环处,代码如下。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240708152855957.webp"></p><p>这段代码是一个消息循环,不停地处理TraceMe主界面的各类消息。此时可能不会直接处理按钮事件。如果是单步跟踪,会进入系统代码。在系统代码里,再次</p><p>按“At+M”快捷键打开内存窗口,对.text区块下内存访问断点。按“f9”键运行,会再次来到代码中。重复这个过程,在一两次中断后就能到达处理按钮的事件代码处 了。“Chek”按钮的事件代码如下。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240709103012918.webp" alt="image-20240709103012918"></p><p>最后,可以将消息断点删除。按“Alt+B”快捷键切换到断点窗口,选中消息断点,直接将其删除。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240709104455427.webp" alt="image-20240709104455427"></p><ol start="6"><li>条件断点</li></ol><p>在调试过程中,我们经常希望断点在满足一定条件时才会中断,这类断点称为条件断点。OllyDbg的条件断点可以按寄存器、存储器、消息等设断。条件断点是一个带有条件表达式的普通INT3断点。当调试器遇到这类断点时,断点将计算表达式的值,如果结果非零或者表达式有效,则断点生效(暂停被调试程序)。</p><p>(1)按寄存器条件中断</p><p>OD打开Conditional_bp.exe,在00401476h处按下设置条件斯点的快捷键“Shift+F2”,在条件文本框内输入条件表达式“eax==0400000”。这样,程序在执行到00401476h处时,如果eax的值为0400000h,OllyDbg将会中断。如果安装了命令行插件,也可在命令行里直接输人如下命令。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240709105246966.webp" alt="image-20240709105246966"></p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240709105049423.webp" alt="image-20240709105049423"></p><p>(2)按存储器条件中断</p><p>在这里用CreateFileA函数进行演示。在实际应用中程序可能会成百上千次调用CreateFileA函数,因此让OllyDbg在CreateFileA函数打开所需文件时中断就显得十分有必要了。CreateFile函数的代码原型如下:</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">HANDLE <span class="title function_">CreateFileA</span><span class="params">(</span></span><br><span class="line"><span class="params"> [in] LPCSTR lpFileName,</span></span><br><span class="line"><span class="params"> [in] DWORD dwDesiredAccess,</span></span><br><span class="line"><span class="params"> [in] DWORD dwShareMode,</span></span><br><span class="line"><span class="params"> [in, optional] LPSECURITY_ATTRIBUTES lpSecurityAttributes,</span></span><br><span class="line"><span class="params"> [in] DWORD dwCreationDisposition,</span></span><br><span class="line"><span class="params"> [in] DWORD dwFlagsAndAttributes,</span></span><br><span class="line"><span class="params"> [in, optional] HANDLE hTemplateFile</span></span><br><span class="line"><span class="params">)</span>;</span><br></pre></td></tr></table></figure><p>运行实例Conditional_bp,对CreateFileA函数设断。单击“OpenTest’”按钮,如图是当OllyDg中断时从栈中看到的情形,左侧标出了各参数相对于当前ESP的地址。打开这个功能的方法是:在栈窗口中单击右键,执行快捷菜单中的“Address’”→“Relative to ESP”(“地址”+“相对于ESP”)命令。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240709111019091.webp" alt="image-20240709111019091"></p><p>CreateFileA函数采用标准调用约定,参数按从右到左的顺序人栈。因为在函数刚执行时EBP栈结构还未建立,所以只能用ESP访问这些参数。CreateFileA函数的第1个参数“FileName”是文件名指针。在OllyDbg里,如果要得到第1个参数的内存地址,可以使用“[ESP+4]”;如果还要得到此地址指向的字符串,就必须使用“[ESP+4]”。</p><p>该实例中调用了4次CreateFileA函数,假设要在该函数打开c:\1212.txt时下断,则”shitf+f2“,输入<code>[STRING [esp+4]]=="c:\\1212.txt"</code>(“STRING”前缀在OllyDbg中的解释是“以零结尾的ASCIl字符串”,如果是Unicode字符串,则改为UNICODE)</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240709111858405.webp" alt="image-20240709111858405"></p><ol start="7"><li><p>条件记录断点</p><p>条件记录断点除了具有条件断点的作用,还能记录断点处函数表达式或参数的值。也可以设置通过断点的次数,每次符合暂停条件时,计数器的值都将减1。例如,要记录Conditional_bp实例调用CreateFileA函数的情况,可在CreateFileA函数的第1行按快捷键Shft+F4”,打开条件记录窗口.在“Condition”(条件)域中输入要设置的条件表达式。在“Explanation”(说明)域中设置一个名称。“Expression”(表达式)域中是要记录的内容的条件,只能设置1个表达式,例如要记录EAX的值,可以输“EAX”。“Decode value of expression as’”(解码表达式的值)下拉列表中可以对记录的数据进行分析。例如,在条件记录窗口中,如果“Expression”域中填写的是[esp+4],则要在该下拉列表中选择“Pointer to ASCIIString”(指向AsCIl字符串的指针)选项,才能得到正确的结果,其功能相当于“STRING”前缀.</p><p>“Pause program”(暂停程序)域用于设置OllyDhg遇到断点时是否中断。“Log value of expression(记录表达式的值)域用于设置遇到断点时是否记录表达式的值。“Log function arguments’”(记录数参数)域用于设置遇到断点时是否记录函数的参数。对这3个域,可以根据需要设置“Never’”(不)、“On condition”(按条件)或“Always”(永远)。<br>条件记录断点允许向插件传递1个或多个命令。当应用程序因条件断点暂停,并且断点中包传递给插件的命令时,都会调用回调函数ODBG_Plugincmd(int reason,t_reg*registers,char*cmd)。如,当程序暂停时,传送命令“d esp”给CmdBar插件,只要在窗口的文本框中输”.d esp”,当条件断点断下时,就会执行“d esp”命令。这时,我们就可以在数据窗口中看到ESP地址处的数据了。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240709115926084.webp" alt="image-20240709115926084"></p><p>设置好条件记录断点,单击实例Conditional_.bp的“OpenTest”按钮,运行后,OllyDbg会在“Logdata”窗口(快捷键“Alt+L”)记录数据。</p></li></ol><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240709115945123-1720497892426-61.webp" alt="image-20240709115945123"></p>]]></content>
</entry>
<entry>
<title>32位下函数分析要点一步到"胃"</title>
<link href="/2024/07/25/32%E4%BD%8D%E4%B8%8B%E5%87%BD%E6%95%B0%E5%88%86%E6%9E%90%E8%A6%81%E7%82%B9%E4%B8%80%E6%AD%A5%E5%88%B0%E8%83%83/"/>
<url>/2024/07/25/32%E4%BD%8D%E4%B8%8B%E5%87%BD%E6%95%B0%E5%88%86%E6%9E%90%E8%A6%81%E7%82%B9%E4%B8%80%E6%AD%A5%E5%88%B0%E8%83%83/</url>
<content type="html"><![CDATA[<p>程序都是由具有不同功能的函数组成的,因此在逆向分析中将重点放在函数的识别及参数的传递上是明智的,这样做可以将注意力集中在某一段代码上。函数是一个程序模块,用来实现一个特定的功能。一个函数包括函数名、入口参数、返回值、函数功能等部分。</p><p><code>函数的识别:</code>程序通过调用程序来调用函数,在函数执行后又返回调用程序继续执行。函数如何知道要返回的地址呢?实际上,调用函数的代码中保存了一个返回地址,该地址会与参数一起传递给被调用的函数。有多种方法可以实现这个功能,在绝大多数情况下,编译器都使用call和ret指令来调用函数及返回调用位置。</p><p>call指令与跳转指令功能类似。不同的是,call指令保存返回信息,即将其之后的指令地址压入栈的顶部,当遇到ret指令时返回这个地址。也就是说,call指令给出的地址就是被调用函数的起始地址。ret指令则用于结束函数的执行(当然,不是所有的ret指令都标志着函数的结束)。通过这一机制可以很容易地把函数调用和其他跳转指令区别开来。<br>因此,可以通过定位call机器指令或利用ret指令结束的标志来识别函数。call指令的操作数就是所调用函数的首地址。看一个例子,代码如下。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">int</span> <span class="title function_">Add</span><span class="params">(<span class="type">int</span> x, <span class="type">int</span> y)</span>;</span><br><span class="line">main( )</span><br><span class="line">{</span><br><span class="line"> <span class="type">int</span> a = <span class="number">5</span>, b = <span class="number">6</span>;</span><br><span class="line"> Add(a, b);</span><br><span class="line"> <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">Add(<span class="type">int</span> x, <span class="type">int</span> y)</span><br><span class="line">{</span><br><span class="line"> <span class="keyword">return</span>(x + y);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>编译结果如下</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240715141146105.webp" alt="image-20240715141146105"></p><p>这种函数直接调用方式使程序变得很简单一所幸大部分情况都是这样的。但也有例外,程序调用函数是间接调用的,即通过寄存器传递函数地址或动态计算函数地址调用。例如<code>CALL [4*eax+10h]</code></p><p><code>函数的参数:</code>函数传递参数有3种方式,分别是栈方式、寄存器方式及通过全局变量进行隐含参数传递的方式。如果参数是通过栈传递的,就需要定义参数在栈中的顺序,并约定函数被调用后由谁来平衡栈。如果参数是通过寄存器传递的,就要确定参数存放在哪个寄存器中。每种机制都有其优缺点,且与使用的编译语言有关。</p><p>(1)利用栈传递参数</p><p>栈是一种“后进先出”的存储区,栈顶指针esp指向栈中第1个可用的数据项。在调用函数时,调用者依次把参数压入栈,然后调用函数。函数被调用以后,在栈中取得数据并进行计算。函数计算结束以后,由调用者或者函数本身修改栈,使栈恢复原样(即平衡栈数据)。<br>在参数的传递中有两个很重要的问题:当参数个数多于1个时,按照什么顺序把参数压人栈?函数结束后,由谁来平衡栈?这些都必须有约定。这种在程序设计语言中为了实现函数调用而建立的协议称为调用约定(Calling Convention)。这种协议规定了函数中的参数传送方式、参数是否可变和由谁来处理栈等问题。不同的语言定义了不同的调用约定,常用的调用约定如下。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240715141839888.webp" alt="image-20240715141839888"></p><p>C规范(即__cdecl)函数的参数按照从右到左的顺序人栈,由调用者负责清除栈。__cdecl是C和C++程序的默认调用约定。C/C+和MFC程序默认使用的调用约定是、__cdecl,也可以在函数声明时加上__cdecl关键字来手动指定。<br>pascal规范按从左到右的顺序压参数人栈,要求被调用函数负责清除栈。<br>stdcall调用约定是Win32API采用的约定方式,有“标准调用”(Standard CALL)之意,采用C调用约定的入栈顺序和pascal调用约定的调整栈指针方式,即函数入口参数按从右到左的顺序入栈,并由被调用的函数在返回前清理传送参数的内存栈,函数参数的个数固定。由于函数体本身知道传入的参数个数,被调用的函数可以在返回前用一条retn指令直接清理传递参数的栈。在Win32API中,也有一些函数是__cdecl调用的,例如wsprintf。</p><p>为了了解不同类型约定的处理方式,我们来看一个例子。假设有调用函数test1(Parl,Par2,Par3)按__cdecl、pascal和stdeall的调用约定,其汇编代码如下。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">cdecl:</span><br><span class="line">push par3 ;参数从右到左传递</span><br><span class="line">push par2</span><br><span class="line">push par1</span><br><span class="line">call test1</span><br><span class="line">add esp,0c ;平衡栈</span><br></pre></td></tr></table></figure><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">pascal:</span><br><span class="line">push par1 ;参数从左到右传递</span><br><span class="line">push par2</span><br><span class="line">push par3</span><br><span class="line">call test1 ;函数内平衡栈</span><br></pre></td></tr></table></figure><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">stdcall:</span><br><span class="line">push par3 ;参数从右到左传递</span><br><span class="line">push par2</span><br><span class="line">push par1</span><br><span class="line">call test1 ;函数内平衡栈</span><br></pre></td></tr></table></figure><p>可以清楚地看到,__cdecl类型和stdcall类型先把右边的参数压人栈,pascal则相反。在栈平衡上,__cdecl类型由调用者用“add esp,0c”指令把12字节的参数空间清除,pascal和stdcall类型则由子程序负责清除。<br>函数对参数的存取及局部变量都是通过栈来定义的,非优化编译器用一个专门的寄存器(通常是ebp)对参数进行寻址。C、C+、pascal等高级语言的函数(子程序)执行过程基本一致,情祝如下。 </p><p>调用者将函数(子程序)执行完毕时应返回的地址、参数压入栈。</p><p>子程序使用“ebp指针+偏移量”对栈中的参数进行寻址并取出,完成操作。<br>子程序使用ret或retf指令返回。此时,CPU将eip置为栈中保存的地址,并继续执行它。</p><p>栈在整个过程中发挥着非常重要的作用。栈是一个先进后出的区域,只有一个出口,即当前栈顶。栈操作的对象只能是双操作数(占4字节)。例如,按stdcall约定调用函数test2(Parl,Par2)(有2个参数),其汇编代码大致如下。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">push par2 ;参数2</span><br><span class="line">push par1 ;参数1</span><br><span class="line">call test2 ;调用子程序test2</span><br><span class="line">{</span><br><span class="line">push ebp ;保护现场,原来的ebp指针</span><br><span class="line">mov ebp, esp ;设置新的ebp,使其指向栈顶</span><br><span class="line">mov eax, dword ptr [ebp+0c] ;调用参数2</span><br><span class="line">mov ebx, dword ptr [ebp+08] ;调用参数1</span><br><span class="line">sub esp, 8 ;若函数要使用局部变量,则要再栈中留出一部分空间</span><br><span class="line">......</span><br><span class="line">add esp, 8 ;释放局部变量占用的栈</span><br><span class="line">pop ebp ;恢复现场的ebp</span><br><span class="line">ret 8 ;返回(相当于ret;add esp,8)</span><br><span class="line"> ;ret后面的值等于参数个数乘4h</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>因为esp是栈指针,所以一般使用ebp来存取栈。其栈建立过程如下。</p><p>①此例函数中有2个参数,假设执行函数前栈指针的esp为K。<br>②根据stdcall调用约定,先将参数Par2压进栈,此时esp为K-04h。<br>③将参数Par1压人栈,此时esp为K-08h.</p><p>④参数入栈后,程序开始执行call指令。cal指令把返回地址压人栈,这时esp为K-0Ch。<br>⑤现在已经在子程序中了,可以开始使用ebp来存取参数了。但是,为了在返回时恢复ebp的值,需要使用“push ebp”指令来保存它,这时esp为K-10h。<br>⑥执行“mov ebp,.esp”指令,ebp被用来在栈中寻找调用者压人的参数,这时[ebp+8]就是参数1,[ebp+c]就是参数2。<br>⑦“sub esp,8”指令表示在栈中定义局部变量。局部变量1和局部变量2对应的地址分别是[ebp-4和[ebp-8]。函数结束时,调用“add esp,8”指令释放局部变量占用的栈。局部变量的作用域是定义该变量的函数 ,也就是说,当函数调用结束后局部变量便会消失。<br>⑧调用“ret 8”指令来平衡栈。在ret指令后面加一个操作数,表示在ret指令后给栈指针esp加上操作数,完成同样的功能。<br>处理完毕,就可以用ebp存取参数和局部变量了,这个过程如图所示。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240715151420270.webp" alt="image-20240715151420270"></p><p>此外,指令enter和leave可以帮助进行栈的维护。enter语句的作用就是“push ebp”“mov ebp,esp”“sub esp,xxx”,而leave语句则完成“add esp,xxx”“pop ebp”的功能。所以,上面的程序可以改成如下形式。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">enter xxxx,0 ;0表示创建xxxx空间来放置局部变量</span><br><span class="line">......</span><br><span class="line">leave ;恢复现场</span><br><span class="line">ret 8;返回</span><br></pre></td></tr></table></figure><p>在许多时候,编译器会按优化方式来编译程序,栈寻址稍有不同。这时,编译器为了节省ebp寄字器或尽可能减少代码以提高速度,会直接通过esp对参数进行寻址。esp的值在函数执行期间会发生变化,该变化出现在每次有数据进出栈时。要想确定对哪个变量进行了寻址,就要知道程序当前位置的esp的值,为此必须从函数的开始部分进行跟踪。<br>同样,对上例中的test2(Parl,Par2)函数,在VC6.0里将优化选项设置为“Maximize Speed’”。重新编译该函数,其汇编代码可能如下。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">push par2 ;参数2</span><br><span class="line">push par1 ;参数1</span><br><span class="line">call test2 ;调用子程序test2</span><br><span class="line">{</span><br><span class="line">mov eax, dword ptr [esp+04] ;调用参数1</span><br><span class="line">mov ebx, dword ptr [esp+08] ;调用参数1</span><br><span class="line">......</span><br><span class="line">ret 8 ;返回</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这时,程序就用esp来传递参数了。其栈建立情况如图所示,过程如下。<br>①假设执行函数前栈指针esp的值为K。<br>②根据stdcall调用约定,先将参数Par2压入栈,此时esp为K-04h。<br>③将Par1压入栈,此时esp为K-08h。<br>④参数入栈后,程序开始执行call指令。call指令把返回地址压人栈,这时esp为K-OCh。<br>⑤现在已经在子程序中了,可以使用esp来存取参数了。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240715152157868.webp" alt="image-20240715152157868"></p><p>(2)利用寄存器传递参数</p><p>寄存器传递参数的方式没有标准,所有与平台相关的方式都是由编译器开发人员制定的。尽管没有标准,但绝大多数编译器提供商都在不对兼容性进行声明的情况下遵循相应的规范,即Fastcall规范。Fastcall,顾名思义,特点就是快(因为它是靠寄存器来传递参数的)。</p><p>不同编译器实现的Fastcall稍有不同。Microsoft Visual C++编译器在采用Fastcall规范传递参数时,左边的2个不大于4字节(DWORD)的参数分别放在ecx和edx寄存器中,寄存器用完后就要使用栈,其余参数仍然按从右到左的顺序压入栈,被调用的函数在返回前清理传送参数的栈。浮点值,远指针和int64类型总是通过栈来传递的。而Borland Delphi/C+编译器在采用Fastcall规范传递参数时,左边的3个不大于4字节(DWORD)的参数分别放在eax、edx和ecx寄存器中,寄存器用完后,其余参数按照从左至右的PASCAL方式压人栈。</p><p>另有一款编译器Watcom C总是通过寄存器来传递参数,它严格为每一个参数分配一个寄存器,默认情况下第1个参数用eax,第2个参数用edx,第3个参数用ebx,第4个参数用ecx。如果寄存器用完,就会用栈来传递参数。Vatcom C可以由程序员指定任意一个寄存器来传递参数,因此,其参数实际上可能通过任何寄存器进行传递。来看一个用Microsoft Visual C++6.0编译的Fastcall调用实例,代码如下。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">int</span> __fastcall <span class="title function_">Add</span><span class="params">(<span class="type">char</span>, <span class="type">long</span>, <span class="type">int</span>, <span class="type">int</span>)</span>;</span><br><span class="line"></span><br><span class="line">main(<span class="type">void</span>)</span><br><span class="line">{</span><br><span class="line"> Add(<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>, <span class="number">4</span>);</span><br><span class="line"> <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="type">int</span> __fastcall <span class="title function_">Add</span><span class="params">(<span class="type">char</span> a, <span class="type">long</span> b, <span class="type">int</span> c, <span class="type">int</span> d)</span></span><br><span class="line"></span><br><span class="line">{</span><br><span class="line"> <span class="keyword">return</span> (a + b + c + d);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>使用Visual C++进行编译,将“Optimizations’”选项设置为“Default’”。编译后查看其反汇编代码,具体如下。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240715160336803.webp" alt="image-20240715160336803"></p><p>Add()函数<br><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240715160347231.webp" alt="image-20240715160347231"></p><p>另一个调用规范thiscall也用到了寄存器传递参数。thiscall是C++中的非静态类成员函数的默认调用约定,对象的每个函数隐含接收this参数。采用thiscall约定时,函数的参数按照从右到左的顺序人栈,被调用的函数在返回前清理传送参数的栈,仅通过ecx寄存器传送一个额外的参数——this指针。</p><p>定义一个类,并在类中定义一个成员函数,代码如下</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string"><stdio.h></span></span></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">CSum</span></span></span><br><span class="line"><span class="class">{</span></span><br><span class="line">public:</span><br><span class="line"> <span class="type">int</span> <span class="title function_">Add</span><span class="params">(<span class="type">int</span> a, <span class="type">int</span> b)</span> <span class="comment">//实际Add原型具有如下形式:Add(this,int a,int b)</span></span><br><span class="line"> {</span><br><span class="line"> <span class="keyword">return</span> (a + b);</span><br><span class="line"> }</span><br><span class="line">};</span><br><span class="line"></span><br><span class="line"><span class="type">void</span> <span class="title function_">main</span><span class="params">()</span></span><br><span class="line">{</span><br><span class="line"> CSum sum;</span><br><span class="line"> sum.Add(<span class="number">1</span>, <span class="number">2</span>);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>使用Visual C++进行编译,将“Optimizations”选项设置为“”Default’”。编译后查看其反汇编代码。<br><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240715162537397.webp" alt="image-20240715162537397"></p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240715162640456.webp" alt="image-20240715162640456"></p><p>(3)名称修饰约定</p><p>为了允许使用操作符和函数重载,C++编译器往往会按照某种规则改写每一个入口点的符号名,从而允许同一个名字(具有不同的参数类型或者不同的作用域)有多个用法且不会破坏现有的基于C的链接器。这项技术通常称为名称改编(Name Mangling)或者名称修饰(Name Decoration)。许多C++编译器厂商都制定了自己的名称修饰方案。<br>在VC++中,函数修饰名由编译类型(C或C++)、函数名、类名、调用约定、返回类型、参数等因素共同决定。关于名称修饰的内容很多,下面仅简单谈一下常见的C编译、C++编译函数名的修饰。</p><p>C编译时函数名修饰约定规则如下。</p><p>stdcall调用约定在输出函数名前面加一个下画线前缀,在后面加一个“@”符号及其参数的字节数,格式为“functionname(@number”。<br>__cdecl调用约定仅在输出函数名前面加一个下画线前缀,格式为”_functionname”。<br>Fastcall调用约定在输出函数名前面加一个“@”符号,在后面加一个“@”符号及其参数的字节数,格式为“@functionname@number”。</p><p>它们均不改变输出函数名中的字符大小写。这和pascall调用约定不同。pascal约定输出的函数名不能有任何修饰且全部为大写。<br>C++编译时函数名修饰约定规则如下。</p><p>stdcall调用约定以“”标识函数名的开始,后跟函数名;在函数名后面,以“@@YG”标识参数表的开始,后跟参数表;参数表的第1项为该函数的返回值类型,其后依次为参数的数据类型,指针标识在其所指数据类型前:在参数表后面,以“@Z”标识整个名字的结束(如果该函数没有参数,则以“Z”标识结束)。其格式 为“? functionname@@YC****@Z”或“?functionname@@YG*XZ。<br>__cdecl调用约定规则与上面的stdcall调用约定规则相同,只是参数表的开始标识由“@@YG”变成了“@@YA”。<br>Fastcall调用约定规则与上面的stdcall调用约定规则相同,只是参数表的开始标识由“@@YG”变成了“@@YT”。</p><p><code>函数的返回值:</code>函数被调用执行后,将向调用者返回1个或多个执行结果,称为函数返回值。返回值最常见的形式是return操作符,还有通过参数按传引用方式返回值、通过全局变量返回值等。</p><p>(1)用return操作符返回值</p><p>在一般情况下,函数的返回值放在eax寄存器中返回,如果处理结果的大小超过eax寄存器的容量,其高32位就会放到edx寄存器中,例如下面这段C程序。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">MyAdd(<span class="type">int</span> x, <span class="type">int</span> y)</span><br><span class="line">{</span><br><span class="line"> <span class="type">int</span> temp;</span><br><span class="line"> temp = x + y;</span><br><span class="line"> <span class="keyword">return</span> temp;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这是一个普通的函数,它将两个整数相加。这个函数有两个参数,并使用一个局部变量临时保存结果。其汇编实现代码所下。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240715173428513.webp" alt="image-20240715173428513"></p><p>(2)通过参数按传引用方式返回值</p><p>给函数传递参数的方式有两种,分别是传值和传引用。进行传值调用时,会建立参数的一份副本,并把它传给调用函数,在调用函数中修改参数值的副本不会影响原始的变量值。传引用调用允许调用函数修改原始变量的值。调用某个函数,皆把变量的地址传递给函数时,可以在函数中用间接引用运算符修改调用函数内存单元中该变量的值。例如,在调用函数max时,需要用两个地址(或者两个指向整数的指针)作为参数,函数会将结果较大的数放到参数a所在的内存单元地址中返回,代码如下。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string"><stdio.h></span></span></span><br><span class="line"></span><br><span class="line"><span class="type">void</span> <span class="title function_">max</span><span class="params">(<span class="type">int</span> *a, <span class="type">int</span> *b)</span>;</span><br><span class="line">main( )</span><br><span class="line">{</span><br><span class="line"> <span class="type">int</span> a = <span class="number">5</span>, b = <span class="number">6</span>;</span><br><span class="line"> max(&a, &b);</span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"a、b中较大的数是%d"</span>, a); <span class="comment">//将最大的数显示出来</span></span><br><span class="line"> <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="type">void</span> <span class="title function_">max</span><span class="params">( <span class="type">int</span> *a, <span class="type">int</span> *b)</span></span><br><span class="line">{</span><br><span class="line"> <span class="keyword">if</span>(*a < *b)</span><br><span class="line"> *a = *b; <span class="comment">//经比较后,将较大的数放到a变量之中</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>其汇编代码如下</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240725180625834.webp" alt="image-20240725180625834"></p>]]></content>
</entry>
<entry>
<title>常见数据类型分析要点一步到"胃"</title>
<link href="/2024/07/25/%E5%B8%B8%E8%A7%81%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B%E5%88%86%E6%9E%90%E8%A6%81%E7%82%B9%E4%B8%80%E6%AD%A5%E5%88%B0%E8%83%83/"/>
<url>/2024/07/25/%E5%B8%B8%E8%A7%81%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B%E5%88%86%E6%9E%90%E8%A6%81%E7%82%B9%E4%B8%80%E6%AD%A5%E5%88%B0%E8%83%83/</url>
<content type="html"><![CDATA[<p></p><p>数据结构是计算机存储、组织数据的方式。在进行逆向分析时,确定数据结构以后,算法就很容易得到了。有些时候,事情也会反过来,即根据特定算法来判断数据结构。本节将讨论常见的数据结构及它们在汇编语言中的实现方式。</p><p><code>局部变量:</code>局部变量(Local Variables)是函数内部定义的一个变量,其作用域和生命周期局限于所在函数内。使用局部变量使程序模块化封装成为可能。从汇编的角度来看,局部变量分配空间时通常会使用栈和寄存器。</p><p>(1)利用栈存放局部变量</p><p>局部变量在栈中进行分配,函数执行后会释放这些栈,程序用“sub esp,8”语句为局部变量分配空间,用[ehp-xxxx]寻址调用这些变量,而参数调用相对于ebp偏移量是正的,即[ebp+xxxx],因此在逆向时比较容易区分。编译器在优化模式时,通过esp寄存器直接对局部变量和参数进行寻址。当函数退出时,用“add esp,8”指令平衡栈,以释放局部变量占用的内存。有些编译器(例如Delphi)通过给esp加一个负值来进行内存的分配。另外,编译器可能会用“push reg’”指令取代“sub esp,4”指令,以节省几字节的空间。<br>局部变量分配与清除栈的形式如表所示。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240716144036292.webp" alt="image-20240716144036292"></p><p>下面这个实例是用“push reg””指令来取代“sub esp,4”指令的。 </p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">int</span> <span class="title function_">add</span><span class="params">(<span class="type">int</span> x, <span class="type">int</span> y)</span>;</span><br><span class="line"><span class="type">int</span> <span class="title function_">main</span><span class="params">(<span class="type">void</span>)</span></span><br><span class="line">{</span><br><span class="line"> <span class="type">int</span> a = <span class="number">5</span>, b = <span class="number">6</span>;</span><br><span class="line"> add(a, b);</span><br><span class="line"> <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="type">int</span> <span class="title function_">add</span><span class="params">(<span class="type">int</span> x, <span class="type">int</span> y)</span></span><br><span class="line">{</span><br><span class="line"> <span class="type">int</span> z;</span><br><span class="line"> z = x + y;</span><br><span class="line"> <span class="keyword">return</span>(z);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>用Microsoft Visual C+6.0进行编译,不进行忧化,其汇编代码如下。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240716144329185.webp" alt="image-20240716144329185"></p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240716144347269.webp" alt="image-20240716144347269"></p><p>add函数里不存在“sub esp,n”之类的指令,程序通过“push ecx’”指令来开辟一块栈,然后用[ebp-04]来访问这个局部变量。局部变量的起始值是随机的,是其他函数执行后留在栈中的垃圾数据,因此需要对其进行初始化。初始化局部变量有两种方法:一种是通过mov指令为变量赋值,例如“mov[ebp-04],5”;另一种是使用push指令直接将值压人栈,例如“push 5”。</p><p>(2)利用寄存器存放局部变量</p><p>除了栈占用2个寄存器,编译器会利用剩下的6个通用寄存器尽可能有效地存放局部变量,这样可以少产生代码,提高程序的效率。如果寄存器不够用,编译就会将变量放到栈中。在进行逆向分析时要注意,局部变量的生存周期比较短,必须及时确定当前寄存器的变量是哪个变量。</p><p><code>全局变量:</code>全局变量作用于整个程序,它一直存在,放在全局变量的内存区中。局部变量则存在于函数的栈区中,函数调用结束后便会消失。在大多数程序中,常数一般放在全局变量中,例如一些注册版标记、测试版标记等。在大多数情况下,在汇编代码中识别全局变量比在其他结构中要容易得多。全局变量通常位于数据区块(.data)的一个固定地址处,当程序需要访问全局变量时,一般会用一个固定的硬编码地址直接对内存进行寻址,示例如下。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">mov eax, dword ptr [40874c0h]</span><br></pre></td></tr></table></figure><p>全局变量可以被同一文件中的所有函数修改,如果某个函数改变了全局变量的值,就能影响其他函数(相当于函数间的传递通道),因此,可以利用全局变量来传递参数和函数返回值等。全局变量在程序的整个执行过程中占用内存单元,而不像局部变量那样在需要时才开辟内存单元。</p><p>看一个利用全局变量传递参数的实例,代码如下。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">int</span> z;</span><br><span class="line"></span><br><span class="line"><span class="type">int</span> <span class="title function_">add</span><span class="params">(<span class="type">int</span> x, <span class="type">int</span> y)</span>;</span><br><span class="line"><span class="type">int</span> <span class="title function_">main</span><span class="params">(<span class="type">void</span>)</span></span><br><span class="line">{</span><br><span class="line"> <span class="type">int</span> a = <span class="number">5</span>, b = <span class="number">6</span>;</span><br><span class="line"> z = <span class="number">7</span>;</span><br><span class="line"> add(a, b);</span><br><span class="line"> <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="type">int</span> <span class="title function_">add</span><span class="params">(<span class="type">int</span> x, <span class="type">int</span> y)</span></span><br><span class="line">{</span><br><span class="line"> <span class="keyword">return</span>(x + y + z);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>用Microsoft Visual C+6.0进行编译,但不进行优化,其汇编代码如下。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240716151649239.webp" alt="image-20240716151649239"></p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240716151700772.webp" alt="image-20240716151700772"></p><p>用PEID打开编译后的程序,查看区块,区块信息如图所示。全局变量004084C0h在.data区块中,该区块的属性为可读写。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240716153017410.webp" alt="image-20240716153017410"></p><p>使用这种对内存直接寻址的硬编码方式,比较容易识别出这是一个全局变量。一般编译器会将全局变量放到可读写的区块里(如果放到只读区块里,就是一个常量)。<br>与全局变量类似的是静态变量,它们都可以按直接方式寻址等。不同的是,静态变量的作用范围是有限的,仅在定义这些变量的函数内有效。</p><p><code>数组:</code>数组是相同数据类型的元素的集合,它们在内存中按顺序连续存放在一起。在汇编状态下访问数组一般是通过基址加变址寻址实现的。<br>请看下面这个数组访问实例。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string"><stdio.h></span></span></span><br><span class="line"></span><br><span class="line"><span class="type">int</span> <span class="title function_">main</span><span class="params">(<span class="type">void</span>)</span></span><br><span class="line">{</span><br><span class="line"> <span class="type">static</span> <span class="type">int</span> a[<span class="number">3</span>] = {<span class="number">0x11</span>, <span class="number">0x22</span>, <span class="number">0x33</span>};</span><br><span class="line"> <span class="type">int</span> i, s = <span class="number">0</span>, b[<span class="number">3</span>];</span><br><span class="line"> <span class="keyword">for</span>(i = <span class="number">0</span>; i < <span class="number">3</span>; i++)</span><br><span class="line"> {</span><br><span class="line"> s = s + a[i];</span><br><span class="line"> b[i] = s;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">for</span>(i = <span class="number">0</span>; i < <span class="number">3</span>; i++)</span><br><span class="line"> {</span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"%d\n"</span>, b[i]);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>用Microsoft Visual C+6.0进行编译,将优化选项设置为“Maximize Speed”,其汇编代码如下。</p><p><img src="https://raw.githubusercontent.com/FeowmAomr/picx-images-hosting/master/image-20240716153737093.webp" alt="image-20240716153737093"></p><p>在内存中,数组可存在于栈、数据段及动态内存中。本例中的a[ ]数组就保存在数据段.data中,其寻址用“基址+编移量”实现。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">mov edi, dword_407030[eax]</span><br></pre></td></tr></table></figure><p>这种间接寻址一般出现在给一些数组或结构赋值的情况下,其寻址形式一般是[基址+偏移量]。基址可以是常量,也可以是寄存器,为定值。根据n值的不同,可以对结构中的相应单元赋值。<br>b[ ]数组放在栈中,这些栈在编译时分配。数组在声明时可以直接计算偏移地址,针对数组成员寻址是采用实际的偏移量完成的。</p>]]></content>
</entry>
</search>