汇编课程期末报告

文章发布时间:

最后更新时间:

汇编课程期末报告

[TOC]

1、作业说明

说明:使用 IDA 或 xdb 逆向分析给定的 exe 程序。在命令行中运行程序,输入学号和 key,实现成功打印:success

提交:必须以 Markdown 格式撰写报告,并导出 PDF 格式报告,以:”姓名-学号.pdf”格式命名,提交到学习通中。

2、实验结果

输入的学号是:2024141530054

输入的 key 是:17EAFACE19021EEAFACE190205C1F4D9CA1E3EB3CBE02CD23EB3CBE02CD03FB3CBE02CD23DFEA5C55C023AB3CBE02CD2

实验截图:

image-20250513102959119

3、实验步骤解析

(1)需要详细说明解题步骤,文字+截图。

(2)并说明程序如何实现(一定程度上)每台电脑运行程序时需要的 key 不一样?

(1)解题步骤

最近被一个逆向师傅推荐了两本书其中有一本 <<从零开始学ida逆向> > 的书讲述了 ida 的基本使用和基本逆向过程, 接下来是我运用课上知识和课后学习成果的时候了

分析文件

这道题解压之后由一个.exe 文件, 一个.pdb 文件和多个.dll 动态链接库文件组成

image-20250512233529802

我从书上看到分析文件的几种方法, 首先可以直接运行程序, 然后用任务管理器查看是 32 位程序

image-20250512234046011

还有就是可以用十六进制编辑器查看 比如 Hxd (以下是书上内容, 我并没用这种方法分析)

image-20250512234641256

还有比较方便的方法就是使用软件了 我这里一直使用的 ExeinfoPE 可以查看文件类型 还可以查看文件是否被加壳 查看壳的类型 只需要拖入文件就行, 你甚至可以自己添加识别类型

image-20250512235041936

好的 现在确认了文件是 32 位 PE 文件 (事实上 ida 自己也能做一些识别的) 现在我们把它拖入 ida 中进行分析

在 ida 中的代码审计分析

拖入 ida 中, 发现 ida 自动识别了文件类型, 所以我们选择确定即可, 因为作业发布的文件中有.pdb 文件, 所以我们选择加载 pdb 调试信息文件, 方便我们阅读

image-20250512235411843

进来之后第一件事找函数入口, 由于是 PE 文件而不是 ELF, 我们在左侧的函数列表中搜素 main 函数即可, 点进 main 函数, 看到程序的主要汇编代码,

F5 反汇编 main 代码如下

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
int __cdecl main()
{
const unsigned __int8 *v0; // esi
unsigned __int64 v1; // rax
unsigned int v2; // ebx
unsigned int v3; // eax
unsigned __int64 *Myfirst; // edx
unsigned int v5; // edi
int v6; // ecx
unsigned __int64 *v7; // eax
unsigned __int64 *v8; // eax
unsigned __int64 *v9; // eax
unsigned __int64 *v10; // eax
unsigned __int64 *v11; // edx
unsigned int v12; // ecx
unsigned int v14; // [esp+10h] [ebp-60h]
int v15; // [esp+24h] [ebp-4Ch]
int v16; // [esp+28h] [ebp-48h]
int v17; // [esp+2Ch] [ebp-44h]
int v18; // [esp+30h] [ebp-40h]
std::vector<unsigned __int64> address; // [esp+34h] [ebp-3Ch] BYREF
char temp[14]; // [esp+40h] [ebp-30h] BYREF
char stuId[16]; // [esp+50h] [ebp-20h] BYREF
int v22; // [esp+6Ch] [ebp-4h]

getAllMacAddress(&address);
v22 = 0;
j__printf("input the student id: ");
j__scanf("%13s", stuId);
j__printf("input the key: ");
v0 = (const unsigned __int8 *)_malloc(12 * (address._Mypair._Myval2._Mylast - address._Mypair._Myval2._Myfirst) + 1);
j__scanf("%s", v0);
if ( !check(v0, 12 * (address._Mypair._Myval2._Mylast - address._Mypair._Myval2._Myfirst)) )
goto LABEL_14;
v1 = processInput(stuId);
v2 = v1;
v3 = HIDWORD(v1);
Myfirst = address._Mypair._Myval2._Myfirst;
v5 = 0;
v14 = v3;
if ( address._Mypair._Myval2._Mylast - address._Mypair._Myval2._Myfirst )
{
v18 = __PAIR64__(v3, v2) >> 31;
v17 = __PAIR64__(v3, v2) >> 30;
v16 = __PAIR64__(v3, v2) >> 29;
v15 = __PAIR64__(v3, v2) >> 28;
v6 = 2 * v2;
while ( 1 )
{
LODWORD(Myfirst[v5]) ^= v2;
HIDWORD(Myfirst[v5]) ^= v3;
v7 = address._Mypair._Myval2._Myfirst;
LODWORD(address._Mypair._Myval2._Myfirst[v5]) ^= v6;
HIDWORD(v7[v5]) ^= v18;
v8 = address._Mypair._Myval2._Myfirst;
LODWORD(address._Mypair._Myval2._Myfirst[v5]) ^= 4 * v2;
HIDWORD(v8[v5]) ^= v17;
v9 = address._Mypair._Myval2._Myfirst;
LODWORD(address._Mypair._Myval2._Myfirst[v5]) ^= 8 * v2;
HIDWORD(v9[v5]) ^= v16;
v10 = address._Mypair._Myval2._Myfirst;
LODWORD(address._Mypair._Myval2._Myfirst[v5]) ^= 16 * v2;
HIDWORD(v10[v5]) ^= v15;
j__sprintf(temp, "%012llX\n", address._Mypair._Myval2._Myfirst[v5]);
if ( *(_DWORD *)temp != *(_DWORD *)v0
|| *(_DWORD *)&temp[4] != *((_DWORD *)v0 + 1)
|| *(_DWORD *)&temp[8] != *((_DWORD *)v0 + 2) )
{
break;
}
++v5;
Myfirst = address._Mypair._Myval2._Myfirst;
v0 += 12;
v6 = 2 * v2;
v3 = v14;
if ( v5 >= address._Mypair._Myval2._Mylast - address._Mypair._Myval2._Myfirst )
goto LABEL_8;
}
LABEL_14:
j__printf("key error");
_exit(0);
}
LABEL_8:
j__printf("success");
v11 = address._Mypair._Myval2._Myfirst;
if ( address._Mypair._Myval2._Myfirst )
{
v12 = ((char *)address._Mypair._Myval2._Myend - (char *)address._Mypair._Myval2._Myfirst) & 0xFFFFFFF8;
if ( v12 >= 0x1000 )
{
v11 = (unsigned __int64 *)*((_DWORD *)address._Mypair._Myval2._Myfirst - 1);
v12 += 35;
if ( (unsigned int)((char *)address._Mypair._Myval2._Myfirst - (char *)v11 - 4) > 0x1F )
__invalid_parameter_noinfo_noreturn();
}
operator delete(v11, v12);
}
return 0;
}

分析后可以了解函数的主要逻辑

声明定义变量–> 获取主机 mac 地址存储到 address 中–> 让用户输入学号和秘钥 key–> check 函数检查用户输入秘钥长度是否符合(分支点)

(1)–> 长度不符合 直接跳转到 LABEL_14 输出错误 程序结束

(2)–> 长度符合 继续进行下面的验证加密代码

可以看出我们如果想逆向分析出秘钥的话, 这段加密代码是需要分析并且逆向构造出 exp 代码的, 最后让程序跳转到 LABEL_8 输出 success 就算胜利

check 函数代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
char __cdecl check(const unsigned __int8 *keys, int len)
{
int v2; // ecx
unsigned __int8 v3; // al

if ( strlen((const char *)keys) == len )
{
v2 = 0;
if ( len <= 0 )
return 1;
while ( 1 )
{
v3 = keys[v2];
if ( (v3 < 0x30u || v3 > 0x39u) && (unsigned __int8)(v3 - 65) > 5u )
break;
if ( ++v2 >= len )
return 1;
}
}
return 0;
}

可以看出 check 的作用就是检查两个参数长度是否一致 还有就是检查传入的秘钥是否在指定的范围之内 如果长度不一致或者在范围之外就会返回 0(不合法)

尝试绕过第一步检验 长度一致检测

我们现在想要让我们输入的 key 值通过 check 检查 也就是长度正确

if ( ! check(v0, 12 * (address._Mypair._Myval2._Mylast - address._Mypair._Myval2._Myfirst)) )

也就是让 key 的长度等于 12 * (address._Mypair._Myval2._Mylast - address._Mypair._Myval2._Myfirst)

getAllMacAddress(&address); 得到所有 MAC 地址,存储在 std::vector<unsigned __int64> address

关键词 含义
std::vector C++ 标准库中提供的 动态数组,可以自动扩展大小
1
2
3
4
5
6
struct std::vector<T> {
T* _Myfirst; // 起始指针
T* _Mylast; // 尾部指针(最后一个元素的下一个位置)
T* _Myend; // 容器的容量上限(分配的内存末尾)
};

length = _Mylast - _Myfirst;

check 函数中计算了长度, 所以我们找到这部分的汇编代码

image-20250513004030515

可以看出最后这三行就是计算长度的代码 最后存储在 eax 中 我们打断点动态调试 local windows debugger 查看 eax 的值为 0x40 也就是 64

image-20250513004127292

image-20250513094840556

64 右移三位为 8 8+8*2 = 24 24 左移两位为 96

所以我们输入密钥的长度应该是 96

获取密钥

开始我想读取 address 中的 mac 地址来按照加密流程一步步弄出秘钥, 但是这样太麻烦了, 完全没有必要, 只要是验证过程一定会有判定的时间点, 这个时候读取判定就能得到最终结果

所以我们打上断点动态调试 让后输入学号 2024141530054 key 值输入 96 个 A 来通过 check 检查

AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

接下来通过加密验证代码我们得知加密逻辑是把输入的 key 值分段进行加密验证, 所以我们待会读取 key 值的时候也要一段一段地读取

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
v1 = processInput(stuId);
v2 = v1;
v3 = HIDWORD(v1);
Myfirst = address._Mypair._Myval2._Myfirst;
v5 = 0;
v14 = v3;
if ( address._Mypair._Myval2._Mylast - address._Mypair._Myval2._Myfirst )
{
v18 = __PAIR64__(v3, v2) >> 31;
v17 = __PAIR64__(v3, v2) >> 30;
v16 = __PAIR64__(v3, v2) >> 29;
v15 = __PAIR64__(v3, v2) >> 28;
v6 = 2 * v2;
while ( 1 )
{
LODWORD(Myfirst[v5]) ^= v2;
HIDWORD(Myfirst[v5]) ^= v3;
v7 = address._Mypair._Myval2._Myfirst;
LODWORD(address._Mypair._Myval2._Myfirst[v5]) ^= v6;
HIDWORD(v7[v5]) ^= v18;
v8 = address._Mypair._Myval2._Myfirst;
LODWORD(address._Mypair._Myval2._Myfirst[v5]) ^= 4 * v2;
HIDWORD(v8[v5]) ^= v17;
v9 = address._Mypair._Myval2._Myfirst;
LODWORD(address._Mypair._Myval2._Myfirst[v5]) ^= 8 * v2;
HIDWORD(v9[v5]) ^= v16;
v10 = address._Mypair._Myval2._Myfirst;
LODWORD(address._Mypair._Myval2._Myfirst[v5]) ^= 16 * v2;
HIDWORD(v10[v5]) ^= v15;
j__sprintf(temp, "%012llX\n", address._Mypair._Myval2._Myfirst[v5]);
if ( *(_DWORD *)temp != *(_DWORD *)v0
|| *(_DWORD *)&temp[4] != *((_DWORD *)v0 + 1)
|| *(_DWORD *)&temp[8] != *((_DWORD *)v0 + 2) )
{
break;
}
++v5;
Myfirst = address._Mypair._Myval2._Myfirst;
v0 += 12;
v6 = 2 * v2;
v3 = v14;
if ( v5 >= address._Mypair._Myval2._Mylast - address._Mypair._Myval2._Myfirst )
goto LABEL_8;
}

我们找到验证每段输入的这部分的汇编代码 看到三个比较跳转(每次比较 4 个 一共比较三次 12 个字符) 我们在第一处打上断点 调试并查看寄存器的值

image-20250513100643453

image-20250513100452915

前 12 个字符就是 17EAFACE1902 接下来我们把前十二个输入替换成这个

(17EAFACE1902AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA)

然后重复上述找 key 过程 跳过第一个循环 再找 ecx 处的值 12 个

13-24 个字符是 1EEAFACE1902

25-36 05C1F4D9CA1E

37-48 3EB3CBE02CD2

49-60 3EB3CBE02CD0

61-72 3FB3CBE02CD2

73-84 3DFEA5C55C02

85-96 3AB3CBE02CD2

最后得到全部密钥 17EAFACE19021EEAFACE190205C1F4D9CA1E3EB3CBE02CD23EB3CBE02CD03FB3CBE02CD23DFEA5C55C023AB3CBE02CD2

运行程序也输出正确

image-20250513102959119

诶(⊙o⊙) 我想直接秒了这道题 分析太麻烦了 有无简单方法?

patch 修改汇编指令 把 jz 改成 jnz

image-20250513001425304

这样我们 apply 保存之后只要输入错误的秘钥就能让程序输出 success

image-20250513001659040

为什么程序能实现(一定程度上)每台电脑运行程序时需要的 key 不一样?

关键在于它通过获取 本机的 MAC 地址 来参与生成和验证密钥(见下方代码), 而每台机器的 mac 地址都不一样

即使学号相同,不同设备因 MAC 地址不同,处理后得到的 key 也自然会不同

1
2
3
4
5
6
7
8
9
10
11
12
13
14
LODWORD(Myfirst[v5]) ^= v2;
HIDWORD(Myfirst[v5]) ^= v3;
v7 = address._Mypair._Myval2._Myfirst;
LODWORD(address._Mypair._Myval2._Myfirst[v5]) ^= v6;
HIDWORD(v7[v5]) ^= v18;
v8 = address._Mypair._Myval2._Myfirst;
LODWORD(address._Mypair._Myval2._Myfirst[v5]) ^= 4 * v2;
HIDWORD(v8[v5]) ^= v17;
v9 = address._Mypair._Myval2._Myfirst;
LODWORD(address._Mypair._Myval2._Myfirst[v5]) ^= 8 * v2;
HIDWORD(v9[v5]) ^= v16;
v10 = address._Mypair._Myval2._Myfirst;
LODWORD(address._Mypair._Myval2._Myfirst[v5]) ^= 16 * v2;
HIDWORD(v10[v5]) ^= v15;

总结

这道题不算难, 毕竟给了 pdb 文件, 我学到了分析题目的几种思路, 其实这题修改 eip 修改跳转指令很快就能做出来, 但是显然这样学不到很多东西, 如果逆向一步步解密很难实现, 不妨动态调试的时候直接查看计算好的值, 这是一个解题的不错思路, 后续我还会尝试自己获取 mac 地址一步步推算出密钥