sergio-echigo/malware_analysis_cp1
GitHub: sergio-echigo/malware_analysis_cp1
该项目是一份恶意软件分析练习(Checkpoint 01),通过汇编级逆向工程演示如何逐步破解程序的凭据验证逻辑。
Stars: 0 | Forks: 0
# 程序的初步分析
起初,在程序执行后,可以发现它依赖于输入有效的用户名和密码值,即"username"和"password"。首先必须输入用户名和密码,然而,只要用户名不正确,程序就会终止执行。因此,获取有效凭据的第一步就是获取一个有效的用户名。我们做的第一件事是寻找与无效用户名错误信息相符的文本引用(字符串),即"Pay attention to the username."

除了找到这个引用之外,还需要注意的是,还存在其他文本引用,这些引用与目前发现的逻辑是一致的:即数据的输入、用户名的验证、程序的终止或继续(取决于用户名的有效性)、密码的验证,以及根据查找到的数据显示成功或错误信息。


## 用户名验证
在设置了一系列 *breakpoints* 并对汇编代码进行了漫长的调试之后,我们成功映射出了以下指令,这些指令与程序的数据输出和输入有关:

在图中可以看到一系列为了方便理解代码而添加的注释,此外还添加了一些 *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** 的值等于密码值的长度。因此,这个条件完全不会影响密码的验证——除非以某种方式输入了空值。

### 条件 2
找到的下一个条件与上面的相同,只是 **eax** 的值计算起来稍微复杂一些。通过对代码中发生的事情进行深入分析,可以发现,根据输入的数据,**eax** 会被赋予零值,随后在执行 `test eax, eax` 指令期间,**ZF** *flag* 的值为零,这会导致密码被判定为无效。以下 **assembly** 代码片段演示了对 **eax** 寄存器隐式赋值的过程:

正是在执行 `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** 表中代表 **-** 字符。因此,第四个字符必须是连字符。以下图片演示了这些数据的验证过程:

有了这个,我们就可以获取有效的用户名和密码。然而,分析剩余的 **assembly** 代码可以发现,生成有效密码的另一个条件是:第九个字符必须和第三个字符可能的情况一样是连字符。以下图片也演示了这一点:

## 结论
综上所述,我们可以描述选择有效用户名和密码的以下条件:
- 用户名**必须**正好是十一个字符。
- 密码**必须**包含用户名的第三个、第五个和最后一个字符。
- 密码的第四个或第九个字符必须是连字符。
一些有效的数据包括:
- 01234567890,240-
- VITOR_VITOR,TRpAAAAA-
- rm988248901,ma933801-

RM98824
标签:DAST, UML, 云资产清单, 快速连接, 恶意软件分析, 汇编语言, 逆向工程