退役了学什么

· · 科技·工程

推荐先来学 IDA 怎么用。

现在的信息竞赛选手写 IDA * 应该很熟练了,然而似乎不经常使用 IDA。其实 IDA 是很强的,在很多题目里都会用到。事实上,到了工程实践中,IDA 的使用将会极为广泛——功能强大,无可替代。

本文将简单介绍一下 IDA 的基础用法。

什么是 IDA

Interactive Disassembler Professional,交互式反汇编器专业版。更多时候称作 IDA Pro 或 IDA。这是一款静态静态反编译软件,界面设计非常符合人体工程学(通俗地说就是易用),同时还保持了强大的自由度,甚至能往里面导入脚本,通过编程的方式精准地控制反编译工程的每一步。

下面直接通过例题来讲述怎么使用 IDA。

另外:如果想要体验 IDA,自行搜索下载即可,即使是 IDA Freeware 版(即:Pro 版的下位替代)也很好用了。

SJTU 2025,Reverse,ExprWarmup

算术小课堂开课了\ \ 难度定位:入门/简单\ \ 提供 C 和 C++ 两种语言实现的附件,任选其一逆向即可。 如有不一致的地方,以 C++ 实现的为准。

看 C 的代码会更简单一些。所以选 C 的附件。

打开 IDA,选 New 开始反编译下载下来的附件即可。也可以选 Go,然后打开主界面之后,把附件拖进去。

打开后到左边 Function,运气不错,这个附件没有隐藏入口函数点(即 main 函数),能直接看到。

汇编太复杂看不懂没关系,按 Tab 键,IDA 就会尝试反编译。反编译结果就是类似 C 的高级语言输出了。下面就可以开始读代码了。

split 函数

第一个 for 循环,可以看出就只是在对字符串 a1 进行一个遍历。当遇到和 a2 相同的字符(或者遍历到末尾都没有这个字符)则退出。

然后……分配一段 j - v8 + 1 的空间,放到 ptr[v7] 里,接着把 a1v8j 这里面的字符串复制进去,再……把 v7 增加 1,并且把 v8 赋值为 j + 1。再无条件跳转到标签 LABEL_17 处。

这么看来,v7 就只是个计数器,v8 是个位置记录器。这个 goto 又是在做什么呢?

一个小坑点:LABEL_17 是在上面那个 for 循环的末尾处的。这是 IDA 反编译的时候,因为各种原因(比如,出题人编译这个程序时,编译器进行了优化)没办法特别好地还原代码结构。稍微思考就可以知道:goto LABEL_17 之后,会跳转到循环的末尾,循环的末尾又会跳转回循环的开头。所以,这个 goto 其实是跳转到循环开头的,只是分了两步。

也就是说,看似下面这个 iffor 循环外部,实际上是在内部的。这个 if 会多次执行。

综上来看,这个 split 函数就如同它的名字那样,在做的事情是:给定字符串 a1、目标字符 a2、结果保存处 a3。把字符串 a1 根据 a2 分隔开成为多个字符串,放到 a3 内部。

main 函数,第二部分

首先是个函数 sub_1A77,跟进去看。

sub_1A77 函数

函数很好懂,用随机数初始化一个三维向量,a2a3 就是随机数的范围。

一个常见的现象:传入的应该是个指针,但是 main 函数内(第 59 行)传入的是 v22 的引用,v22 是个普通 double 变量?

看内存布局:

能发现 v22v23v24 在栈上是相邻的,而且都是 double,这说明其实这三个变量其实是一个局部 double [3] 数组。而且看下文,v23v24 也有用到,说明它们肯定是被初始化了,在哪里初始化?——只能是和 v22 一起被初始化了。

init_expr 函数

回到 main 函数,往下看,接着用到了 init_expr 函数。

其实 a1 是个指针。根据 main 函数里调用它的方式,可以知道 a1 的类型是 double *

这里展示一个 IDA 技巧:对于这种 IDA 没识别出来的类型,右键它,选择 Set lvar type(或者直接按快捷键 Y);

然后像 C 语言一样修改它的类型声明;

IDA 就自动修正代码了。

这个函数的逻辑也很清楚了:把 a1[100] 设为 -1、然后把 a2a3a4 依次放进去。

evaluate 函数和 process_token 函数

回到 main 函数,往下看,还有个 evaluate 函数。

内部嵌套调用 process_token 函数。

稍微观察这个函数的三个参数,再结合 main 函数调用的这个方式,可以发现:

往内部看 process_token 函数,功能还是很清晰的:

这里的 a1 是那个数组。这个函数就是根据字符串 a2 的值(实际上只是看第一个字母)来对表达式和栈进行操作。综合所有信息看来,a1 这个数组应该是一个类似结构体的东西,这个结构体用来保存表达式计算的上下文:

struct Context {
    double stack[100];
    int top;
    double x, y, z;
};

这就是一个计算表达式的东西了。

sub_1B77 函数、sub_1C08 函数和 check1 函数

回到 main 函数观察:

还有一个小函数 sub_1B77,这个函数的代码非常清晰了,就只是对向量进行单位化操作,代码贴在下面。

重点是 check1 函数。它检查用户的输入,如果不通过就无法获得 flag。代码如下:

先把用到的 sub_1C08 函数理解了。它的功能很简单:计算两个向量的叉乘。

观察 check1 函数。两个参数都是 64 位整数,结合下文来看,显然这又是 IDA 没有发现这是指针类型,手动改为 double*。第 8 到第 10 行,那些 64 位整数的数组,从下文来看,也应该是 double 数组(那些很奇怪的大整数,其实和实数 1.0 的二进制表示是一样的),也改正。这下代码就清晰很多了:

注意第五行那个 v5 以及接下来的 v6v7,地址相邻,而且在调用 sub_1C06 时把 v5 作为地址传入,说明这三个变量其实是同一个变量,和下面的 v8 之类是一样的:都是 double 数组。然后,v6v7 其实就是向量的第二、第三个的元素。

因此,检查的逻辑如下:

整体梳理

现在已经理解了所有函数,来梳理一下执行逻辑,以下是关键部分:

所以……重点在于构造一个能通过检查的表达式。

答案很简单:F_x=1,F_y=y^2,F_z=z^2。构造这个输入,nc 连上再输进去就拿到 flag 了。

总结

这个是基础的 reverse 题,相比二进制工程需要的知识,它更注重代码的理解。所以用来入门练习 IDA 的使用是很有用处的。

暂别 OI 了就来学更多东西吧!别再对着那个大纲死磕了!

2025 年 7 月 13 日。