汇编课程期末报告
[TOC]
1、作业说明
说明:使用 IDA 或 xdb 逆向分析给定的 exe 程序。在命令行中运行程序,输入学号和 key,实现成功打印:success
提交:必须以 Markdown 格式撰写报告,并导出 PDF 格式报告,以:”姓名-学号.pdf”格式命名,提交到学习通中。
2、实验结果
输入的学号是:2024141530054
输入的 key 是:17EAFACE19021EEAFACE190205C1F4D9CA1E3EB3CBE02CD23EB3CBE02CD03FB3CBE02CD23DFEA5C55C023AB3CBE02CD2
实验截图:

3、实验步骤解析
(1)需要详细说明解题步骤,文字+截图。
(2)并说明程序如何实现(一定程度上)每台电脑运行程序时需要的 key 不一样?
(1)解题步骤
最近被一个逆向师傅推荐了两本书其中有一本 <<从零开始学ida逆向> > 的书讲述了 ida 的基本使用和基本逆向过程, 接下来是我运用课上知识和课后学习成果的时候了
分析文件
这道题解压之后由一个.exe 文件, 一个.pdb 文件和多个.dll 动态链接库文件组成

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

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

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

好的 现在确认了文件是 32 位 PE 文件 (事实上 ida 自己也能做一些识别的) 现在我们把它拖入 ida 中进行分析
在 ida 中的代码审计分析
拖入 ida 中, 发现 ida 自动识别了文件类型, 所以我们选择确定即可, 因为作业发布的文件中有.pdb 文件, 所以我们选择加载 pdb 调试信息文件, 方便我们阅读

进来之后第一件事找函数入口, 由于是 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; unsigned __int64 v1; unsigned int v2; unsigned int v3; unsigned __int64 *Myfirst; unsigned int v5; int v6; unsigned __int64 *v7; unsigned __int64 *v8; unsigned __int64 *v9; unsigned __int64 *v10; unsigned __int64 *v11; unsigned int v12; unsigned int v14; int v15; int v16; int v17; int v18; std::vector<unsigned __int64> address; char temp[14]; char stuId[16]; int v22;
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; unsigned __int8 v3;
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 函数中计算了长度, 所以我们找到这部分的汇编代码

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


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 个字符) 我们在第一处打上断点 调试并查看寄存器的值


前 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
运行程序也输出正确

诶(⊙o⊙) 我想直接秒了这道题 分析太麻烦了 有无简单方法?
patch 修改汇编指令 把 jz 改成 jnz

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

为什么程序能实现(一定程度上)每台电脑运行程序时需要的 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 地址一步步推算出密钥