sergio-echigo/malware_analysis_cp1

GitHub: sergio-echigo/malware_analysis_cp1

该项目是一份恶意软件分析练习(Checkpoint 01),通过汇编级逆向工程演示如何逐步破解程序的凭据验证逻辑。

Stars: 0 | Forks: 0

# 程序的初步分析 起初,在程序执行后,可以发现它依赖于输入有效的用户名和密码值,即"username"和"password"。首先必须输入用户名和密码,然而,只要用户名不正确,程序就会终止执行。因此,获取有效凭据的第一步就是获取一个有效的用户名。我们做的第一件事是寻找与无效用户名错误信息相符的文本引用(字符串),即"Pay attention to the username." ![Incorrect username](https://raw.githubusercontent.com/sergio-echigo/malware_analysis_cp1/main/imgs/incorrect_username.png) 除了找到这个引用之外,还需要注意的是,还存在其他文本引用,这些引用与目前发现的逻辑是一致的:即数据的输入、用户名的验证、程序的终止或继续(取决于用户名的有效性)、密码的验证,以及根据查找到的数据显示成功或错误信息。 ![Input and output code.](https://static.pigsec.cn/wp-content/uploads/repos/cas/5b/5ba5d780dcce3c630e11519224c5d0de9828c5cc61fff576fd33d57245088109.png) ![Input code](https://static.pigsec.cn/wp-content/uploads/repos/cas/c1/c1ca227a650f55bc8eab356792f40ddecbcc11b6d3e31fc5cbf093e686cd25aa.png) ## 用户名验证 在设置了一系列 *breakpoints* 并对汇编代码进行了漫长的调试之后,我们成功映射出了以下指令,这些指令与程序的数据输出和输入有关: ![Username validation](https://static.pigsec.cn/wp-content/uploads/repos/cas/08/08834b16b5f0766c331c7851b2b1c8a2072d6cd28e6c53def2d2d9faa30254ea.png) 在图中可以看到一系列为了方便理解代码而添加的注释,此外还添加了一些 *labels* 以便理解某些函数——例如 **OutputFunction** 和 **InputFunction**。此外,还注意到了一个相当特殊的条件,它会导致在屏幕上显示用户名无效的消息,如下所示: ``` je 2tdcg.cp01.thestartpoint.351301 ``` 条件 *je* 会在 **ZF** *flag* 为 1 时执行跳转到特定地址。通常,条件 *jumps* 会在执行完特定值的比较指令后立即执行,这正是以下指令中所发生的情况: ``` cmp dword ptr ss:[ebp-30],B ``` 上面的指令对两个参数进行比较——更具体地说,是进行减法运算——如果比较结果为零,则将 **ZF** *flag* 的值设为 1。因此,这里可以确定比较的值必须相等,这样用户名才能被正确验证。但是,第一个操作数代表什么呢? ``` ;dword significa double word, equivalente a 32 bits (quatro bytes). ;ptr significa que é um endereço na memória. ;ss:[ebp-30] significa um endereço da stack. ``` 寻找它的引用,唯一发现的是,在所有这些值的输出和输入机制开始之前,它被赋予了一个零值。通过更深入地观察,正如下面的视频所示,可以发现所指内存地址中的值始终等于用户名的长度。 https://youtu.be/L2MYrhpcdyU 综上所述,可以得出结论,用户名的长度必须为 **B**——即十进制的十一。确凿的证据如下: https://youtu.be/IFVBfQHiyf8 现在,我们需要寻找一个有效的密码。 ## 密码验证 我们再次找到了多个条件跳转,它们位于许多其他指令执行之后,幸运的是,这些指令并没有将程序的执行重定向到任何其他地方——如果发生了重定向,并且在这些其他地方还存在更多的重定向,代码追踪起来就会变得相当复杂,至少乍一看是这样。无论如何,这些条件跳转是导致应用程序提示所提供的密码无效的原因。因此,我们必须对它们中的每一个进行分析,以便正确验证给定的密码。 ### 条件 1 第一条可能改变 **ZF** *flag* 值并导致条件跳转发生(或不发生)的指令如下: ``` test eax, eax ``` 这条指令在两个值之间执行 *bitwise* **AND** 操作。**AND** 操作可以看作是对特定值的每一位进行比较;对于每一位,如果两者的值都为 1,则操作结果为 1;反之,如果其中一个值为零,则结果为零。在 **test** 指令执行期间,当操作结果不为零时,**ZF** *flag* 会被赋予零值。因此,可以得出以下“真值表”: | eax & eax | ZF | | :-------: | :-: | | 0 | 1 | | ~0 | 0 | 因此,我们必须确保 **eax** 的值不为零,这在默认情况下已经是成立的,毕竟,在应用程序执行的这一刻,**eax** 的值等于密码值的长度。因此,这个条件完全不会影响密码的验证——除非以某种方式输入了空值。 ![First condition.](https://static.pigsec.cn/wp-content/uploads/repos/cas/86/862ffe8b0c1bc35247aba96d65304826c91092dc758e85fa1e96d72cfe6c23d2.png) ### 条件 2 找到的下一个条件与上面的相同,只是 **eax** 的值计算起来稍微复杂一些。通过对代码中发生的事情进行深入分析,可以发现,根据输入的数据,**eax** 会被赋予零值,随后在执行 `test eax, eax` 指令期间,**ZF** *flag* 的值为零,这会导致密码被判定为无效。以下 **assembly** 代码片段演示了对 **eax** 寄存器隐式赋值的过程: ![Second condition.](https://raw.githubusercontent.com/sergio-echigo/malware_analysis_cp1/main/imgs/second_condition.png) 正是在执行 `call ` 指令期间,**eax** 的值会根据用户名和密码的特定输入值而发生改变。经过一番研究,注意到该指令调用了一个 **C/C++** 的原生函数,即 `memchr`。该函数负责返回一个指向内存地址的指针,在该地址处可以在 *string* 或内存地址中找到特定字符。例如,在 **C** 语言中可以找到以下定义: ``` void *memchr(const void *str, int c, size_t n) ``` 我们可以简要地注意到存在以下参数:用于搜索字符 `c` 的内存地址 `str`,并限制为自 `str` 内存地址起的前 `n` 个字符。再次分析 **assembly** 代码,可以发现有三个非常具体的值被依次添加到 *stack* 顶部: - 一个整数; - 一个正整数,然而它始终代表 **ASCII** 中的一个有效字符; - 一个 *string*。 将这些值联系起来,对代码进行调试并理解它,可以发现这些值分别指的是密码的长度、要搜索的字符以及 *string*(密码)本身。再次调试代码,可以发现这种重复操作最多发生三次,并测试非常具体的值,即:用户名的第三个、第五个和最后一个字符,以及密码本身。因此,可以确定这些字符必须存在于密码中,否则 **eax** 的值将被清零,密码将被判定为无效。以下添加到 *stack* 的数据映射有助于分析此行为: ``` ==== REPRESENTAÇÃO 1 ==== Username: 01234567890 Password: 234567890 0x9 0x32 ('2') 0093F778 ("234567890") Portanto, EAX=0093F778 ("234567890") E, ESP=0093F73C 0x9 0x34 ('4') 0093F778 ("234567890") Portanto, EAX=0093F77A ("4567890") E, ESP=0093F73C 0x9 0x30 ('0') 0093F778 ("234567890") Portanto, EAX=0093F780 E, ESP=0093F73C ==== REPRESENTAÇÃO 2 ==== Username: 01234567890 Password: VITOR 0x5 0x32 ('2') ("VITOR") Portanto, EAX=0 Aqui ``` 只要做到这一点,密码仍然被视为有效,代码继续执行。一些条件 *jumps* 被忽略,紧接着便遇到了密码验证的下一个条件。 ### 条件 3 尽管前面的两个条件会导致跳转从而提示密码无效,但这第三个条件正是负责将密码验证为正确的。以下代码行证明了这一点: ``` cmova eax,esi cmp byte ptr ds:[eax+3],2D je 2tdcg.cp01.thestartpoint.1E13A7 ``` 假设 **esi** 寄存器的值是密码,可以看出 **eax** 的新值现在就是密码本身。最后,将密码的第四个字符(这表明密码至少需要四个字符)与十六进制 **0x2D** 进行比较,后者在 **ASCII** 表中代表 **-** 字符。因此,第四个字符必须是连字符。以下图片演示了这些数据的验证过程: ![Fourth valid character.](https://static.pigsec.cn/wp-content/uploads/repos/cas/d7/d771c044eac39a33aa514948cadf58c78fd82ba9723ee6e9f1da6110400efe5b.png) 有了这个,我们就可以获取有效的用户名和密码。然而,分析剩余的 **assembly** 代码可以发现,生成有效密码的另一个条件是:第九个字符必须和第三个字符可能的情况一样是连字符。以下图片也演示了这一点: ![Ninth valid character.](https://static.pigsec.cn/wp-content/uploads/repos/cas/27/27912688c4e907c6c3a7d02eefe134a8161a584292c9c22b30db493bd4add3aa.png) ## 结论 综上所述,我们可以描述选择有效用户名和密码的以下条件: - 用户名**必须**正好是十一个字符。 - 密码**必须**包含用户名的第三个、第五个和最后一个字符。 - 密码的第四个或第九个字符必须是连字符。 一些有效的数据包括: - 01234567890,240- - VITOR_VITOR,TRpAAAAA- - rm988248901,ma933801- ![Valid credentials.](https://raw.githubusercontent.com/sergio-echigo/malware_analysis_cp1/main/imgs/valid_credentials.png) RM98824
标签:DAST, UML, 云资产清单, 快速连接, 恶意软件分析, 汇编语言, 逆向工程