idek Side effect题解
题目
附件下载题解
一道虚拟机题,程序读入prog
和mem
文件然后加载进虚拟机。点进sub_4015BF
,这里面映射了一页可以执行的内存,然后把一些指令片段copy过去:
再调用sub_401444
修正404800
处的20个数组,每个数组里面都放着一些数字,它们被解析成指向指令片段的指针或者库函数:
然后申请内存创建虚拟机对象,它的数据结构分析如下:
1 | struct instruct { |
reg是寄存器,一共有65536个,但其实用上的也就不到10个;pc是程序计数器;flag是标志位;ptrs存放虚拟机调用一些函数时生成的指针,虚拟机内部可以操作这些指针并把它们传入别的函数;prog存放指令;mem存放用户数据。把这些导入IDA,再观察主函数:
vm_exec
做了一些初始工作,然后开始执行虚拟机:
这个vm_exec
简单读出一条指令,然后执行vm_exec_instruction
:
与一般的虚拟机题不同,这个虚拟机并没有switch-case
,它从之前初始化时的20个数组中找到对应的,把它们copy到栈顶,然后直接调用栈顶的函数指针。对这20个数组进行分析,发现每一个数组的开头都是这个函数:
1 | add rsp, 10h |
它修改了rsp的值。而调用这个stub时,栈上放着返回地址和指针数组(第一个是此stub的地址),而执行第一句指令以后,返回地址就变成了第二个地址,而它执行另一个stub,例如:
1 | inc ecx |
这个stub返回时会执行第三个地址,如此循环下去,相当于执行了一整条ROP链,最后一个stub是:
1 | leave |
此时rsp恢复正常,程序准备执行下一条指令。
把相关数据扒下来,用Python把切成片段的stub重组,然后整理成一个程序重新编译。值得注意的是程序使用pop
和cmov
来实现分支跳转,这部分需要单独处理:
1 | from capstone import * |
dummy.c
:
1 |
|
用IDA打开新生成的dummy文件,就知道20条指令分别做了什么了:
但实际上这个文件并不能被成功执行,经过与原程序的对比调试发现是op_16
不一致,但无妨继续分析代码。指令的格式是op x, y
,二十条指令分别是:
1 | 00 add rx, ry |
解析并整理,代码如下(ja
指令实际上应该是je
指令):
1 | printf("Initializing Interpreter...\n"); |
手动翻译X_X:
1 |
|
它也不能被执行,但是经过一段时间的观察,可以看出这是另一个brainfuck虚拟机,mem
中的八种数字分别对应八条指令,但其中+
指令实际执行了两次加法,>
指令向右移动了两次指针,如果把mem
解析成标准brainfuck,则对应表如下(注释是符号在文件中出现的次数):
1 | imap = { |
值得注意的是,mem
中存放的brainfuck指令流在解析时应该倒过来看,因为程序是用ungetc
把指令逐个push
到文件f1
里去的。解析完如下(数据太多了,50个+
用+50
表示):
1 | <5+50[-<+<64+>65]+50>+46<2[-<+<7+>8]<[->+<]<7[>,<[-<+>]<-]<6[->+>6+<7]>[-<+>]>7[-<3+>3]<3[->+>2+<3]>2[>[-<3+>3]<-[->+<]<[->+<]>2]<[->3+<3]>9[-<[-<+<5+>6]<[->+<]<5[<+7[<3[->+>+<2]>[-<+>]>2-]>2[-<2+<+>3]<2[->2+<2]>-[-<+>]<2[->2+<2]<2[-]>3]<8[->+>7+<8]>[-<+>]>8[-<2+<+>3]<3[->3+<3]>2[>[-<4+>4]<-[->+<]<[->+<]>2]>[-]<2[->2+<2]>9.<]<2+10.<9-56[>2[-<+>]<+<[-]]<-249[>2[-<+>]<+<[-]]<-57[>2[-<+>]<+<[-]]<-189[>2[-<+>]<+<[-]]<-100[>2[-<+>]<+<[-]]<-114[>2[-<+>]<+<[-]]<-19[>2[-<+>]<+<[-]]<-194[>2[-<+>]<+<[-]]<-150[>2[-<+>]<+<[-]]<-229[>2[-<+>]<+<[-]]<-15[>2[-<+>]<+<[-]]<-95[>2[-<+>]<+<[-]]<-157[>2[-<+>]<+<[-]]<-173[>2[-<+>]<+<[-]]<-224[>2[-<+>]<+<[-]]<-220[>2[-<+>]<+<[-]]<-36[>2[-<+>]<+<[-]]<-35[>2[-<+>]<+<[-]]<-119[>2[-<+>]<+<[-]]<-69[>2[-<+>]<+<[-]]<-248[>2[-<+>]<+<[-]]<-43[>2[-<+>]<+<[-]]<-148[>2[-<+>]<+<[-]]<-216[>2[-<+>]<+<[-]]<-239[>2[-<+>]<+<[-]]<-247[>2[-<+>]<+<[-]]<-68[>2[-<+>]<+<[-]]<-177[>2[-<+>]<+<[-]]<-198[>2[-<+>]<+<[-]]<-81[>2[-<+>]<+<[-]]<-224[>2[-<+>]<+<[-]]<-231[>2[-<+>]<+<[-]]<-4[>2[-<+>]<+<[-]]<-241[>2[-<+>]<+<[-]]<-74[>2[-<+>]<+<[-]]<-224[>2[-<+>]<+<[-]]<-237[>2[-<+>]<+<[-]]<-179[>2[-<+>]<+<[-]]<-196[>2[-<+>]<+<[-]]<-51[>2[-<+>]<+<[-]]<-192[>2[-<+>]<+<[-]]<-136[>2[-<+>]<+<[-]]<-96[>2[-<+>]<+<[-]]<-28[>2[-<+>]<+<[-]]<-120[>2[-<+>]<+<[-]]<-29[>2[-<+>]<+<[-]]<-208[>2[-<+>]<+<[-]]<-186[>2[-<+>]<+<[-]]<-59[>2[-<+>]<+<[-]]<-179[>2[-<+>]<+<[-]]+>[[-]<[-]+87.[-]+82.[-]+79.[-]+78.[-]+71.[-]+33.[-]+10.[-]>]<[[-]+67.[-]+79.[-]+82.[-]+82.[-]+69.[-]+67.[-]+84.[-]+33.[-]+10.[-]] |
从百度可以了解到一些brainfuck的一些经典操作:
1 | [-] ; clear `*p` |
代码的前几行是对输入的flag进行变换,中间是判断变换后的flag的每一位是否为指定值,后面是输出正确与否的信息。中间的数组如下:
1 | [56,249,57,189,100,114,19,194,150,229,15,95,157,173,224,220,36,35,119,69,248,43,148,216,239,247,68,177,198,81,224,231,4,241,74,224,237,179,196,51,192,136,96,28,120,29,208,186,59,179] |
brainfuck的代码看似很多,实际加密过程也就只位于前半部分。对前半部分手动逆向,并用解释器验证结果(可以找一个brainfuck的在线解释器)。
逆向的示意结果:
1 | p[4] = 46; p[5] = p[6] = 50; p[11] = last(); p[16~65] = input(); p[70] = 50; p = 5; |
brainfuck的一个特点是代码与位置高度相关,因此很难翻译回容易理解的代码,上面的代码仅能起到参考作用。这一段大致意思为:先读入50个字符,接着进行50轮操作,每轮操作如下:
1 | for i, c in enumerate(ipt): |
然后可以写出如下脚本解密flag(前半部分只是验证动态调试的结果是否与分析结果一致):
1 |
|
最后结果:
idek{bf=two_zippers=four_stacks=getc_and_ungetc..}