退役了学什么
AzusaShirasu · · 科技·工程
推荐先来学 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 的高级语言输出了。下面就可以开始读代码了。
- 28 行:标记一个 canary 来防止攻击。不管。
- 29 到 32 行:初始化一个随机数到
v13里? - 39 行到 50 行:连续读入三个字符串,到
lineptr、s、v16里面。getline函数读入的字符串在末尾会有个换行符,所以去掉。 - 51 行到 53 行,出现了第一个自定义的函数:
split,跟进去看。
split 函数
第一个 for 循环,可以看出就只是在对字符串 a1 进行一个遍历。当遇到和 a2 相同的字符(或者遍历到末尾都没有这个字符)则退出。
然后……分配一段 j - v8 + 1 的空间,放到 ptr[v7] 里,接着把 a1 从 v8 到 j 这里面的字符串复制进去,再……把 v7 增加 1,并且把 v8 赋值为 j + 1。再无条件跳转到标签 LABEL_17 处。
这么看来,v7 就只是个计数器,v8 是个位置记录器。这个 goto 又是在做什么呢?
一个小坑点:LABEL_17 是在上面那个 for 循环的末尾处的。这是 IDA 反编译的时候,因为各种原因(比如,出题人编译这个程序时,编译器进行了优化)没办法特别好地还原代码结构。稍微思考就可以知道:goto LABEL_17 之后,会跳转到循环的末尾,循环的末尾又会跳转回循环的开头。所以,这个 goto 其实是跳转到循环开头的,只是分了两步。
也就是说,看似下面这个 if 在 for 循环外部,实际上是在内部的。这个 if 会多次执行。
综上来看,这个 split 函数就如同它的名字那样,在做的事情是:给定字符串 a1、目标字符 a2、结果保存处 a3。把字符串 a1 根据 a2 分隔开成为多个字符串,放到 a3 内部。
main 函数,第二部分
首先是个函数 sub_1A77,跟进去看。
sub_1A77 函数
函数很好懂,用随机数初始化一个三维向量,a2 和 a3 就是随机数的范围。
一个常见的现象:传入的应该是个指针,但是 main 函数内(第 59 行)传入的是 v22 的引用,v22 是个普通 double 变量?
看内存布局:
能发现 v22、v23 和 v24 在栈上是相邻的,而且都是 double,这说明其实这三个变量其实是一个局部 double [3] 数组。而且看下文,v23 和 v24 也有用到,说明它们肯定是被初始化了,在哪里初始化?——只能是和 v22 一起被初始化了。
init_expr 函数
回到 main 函数,往下看,接着用到了 init_expr 函数。
其实 a1 是个指针。根据 main 函数里调用它的方式,可以知道 a1 的类型是 double *。
这里展示一个 IDA 技巧:对于这种 IDA 没识别出来的类型,右键它,选择 Set lvar type(或者直接按快捷键 Y);
然后像 C 语言一样修改它的类型声明;
IDA 就自动修正代码了。
这个函数的逻辑也很清楚了:把 a1[100] 设为 -1、然后把 a2、a3 和 a4 依次放进去。
evaluate 函数和 process_token 函数
回到 main 函数,往下看,还有个 evaluate 函数。
内部嵌套调用 process_token 函数。
稍微观察这个函数的三个参数,再结合 main 函数调用的这个方式,可以发现:
a1是表达式保存的地方;a2是方才用split分离出来的若干 token,也就是一个字符串的数组。a3是 token 数量。
往内部看 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 数组(那些很奇怪的大整数,其实和实数
注意第五行那个 v5 以及接下来的 v6、v7,地址相邻,而且在调用 sub_1C06 时把 v5 作为地址传入,说明这三个变量其实是同一个变量,和下面的 v8 之类是一样的:都是 double 数组。然后,v6 和 v7 其实就是向量的第二、第三个的元素。
因此,检查的逻辑如下:
- 传入两个三维向量
\vec a, \vec b ; - 令
\vec p=(1,0,0),\vec q=(0,1,0),\vec r=(0,1,0) ; - 计算
\vec s=(\vec p \times \vec b) \times (\vec q \times \vec r) ; - 如果
s_y / s_z 等于a_y^2 / a_z^2 ,则检查通过;反之不通过。
整体梳理
现在已经理解了所有函数,来梳理一下执行逻辑,以下是关键部分:
- 一共进行十次检查;
- 每次,随机一个向量
\vec m ; - 用户之前已经输入了三个表达式,并且被
split函数处理好了,用这三个表达式(记作F_x,F_y,F_z )计算\vec n=(F_x(m_x), F_y(m_y), F_z(m_z)) ,然后归一化计算出\vec e_n ; - 如果
\vec m 和\vec e_n 没通过检查,则程序结束; - 十次检查都通过,给出 flag。
所以……重点在于构造一个能通过检查的表达式。
答案很简单:
总结
这个是基础的 reverse 题,相比二进制工程需要的知识,它更注重代码的理解。所以用来入门练习 IDA 的使用是很有用处的。
暂别 OI 了就来学更多东西吧!别再对着那个大纲死磕了!
2025 年 7 月 13 日。