【Reversing.kr】SimpleVM
本帖最后由 whklhh 于 2017-10-13 00:53 编辑Reversing.kr是韩国的一个逆向题目网站
有分国度的排行榜,还是挺有意思的
本题即来自于第十七关SimpleVM
http://reversing.kr/challenge.php可以下载到文件
本来以为这个标题跟南邮CTF平台上的题目一样,只是个简单的虚拟机壳题……
没想到南邮的是入门类型,这个是复杂入门类型啊QAQ
ELF文件,拖入IDA发现EP地址在映像范围之外,于是无法正常分析
gdb加载又没有任何函数可下断
参考了一下WP,发现可以直接用IDA附加进程,然后dump内存下来再分析
dump脚本:
#include <idc.idc>
static main(void)
{
auto i,fp;
fp = fopen("f:\\dump.dex","wb");
for(i=0x8048000;i<0x804C000;i++)
{
fputc(Byte(i),fp);
}
}
直接保存用IDA加载即可(如果直接命令行输入的话去掉#include和main,直接输入命令即可)
dump下来再拖入IDA分析,这次能得到反汇编代码了
不过导入表坏了╮(╯_╰)╭
只好连猜带蒙
首先根据明码字符串"input:"定位到这个地方,猜测该函数为输出printf
http://img.blog.csdn.net/20171012234531835?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvd2hrbGhoaGg=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast
错误提示Wrong是被加密过的,直接搜索没有结果
根据print的出现地方和参数,可以猜测出byte_804b06e等几个字符串就是加密的提示内容了
附加的时候发现有两个进程
代码中又出现了这种结构
v6 = sub()
if(v6 == -1)
xxx
else if (v6)
xxx
else
xxx
那么就该想起fork函数了
执行它的时候会再复制一个一模一样的子进程
然后父进程中的返回值为子进程的pid,子进程中的返回值为0
返回值为-1时为fork失败
根据fork的返回值来选择父进程和子进程的不同代码
分别分析:
http://img.blog.csdn.net/20171012235830654?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvd2hrbGhoaGg=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast
read函数是通过IDA中观察函数堆栈的返回地址猜出来的
输入内容放在v9中,v9和v10各4个字节,v11作为溢出标志
如果溢出的话就输入Wrong
否则解密关键字符串0x804B0A0(长度200)
然后先后两个printf将v9(输入字符串)和0x804B0A0送出,注意这里第一个参数是v3,而之前输出Wrong的第一个参数是1
其实这个printf应该是管道,父子进程通过管道交互
原理是管道调用相当于两个文件句柄,一个读一个写
由于fork完全复制,因此父子进程所拥有的管道的句柄是相同的
也因此,父进程写入,子进程读取或者子进程写入,父进程读取都是可行的
这里的v3就是管道写入句柄了
然后父进程通过read(第一个参数为v2,管道读取句柄)来先后获取输入值和0x804B0A0字符串
http://img.blog.csdn.net/20171013001038343?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvd2hrbGhoaGg=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast
父进程再次对0x804B0A0进行解密,然后将输入的8个比特放在开头,再异或10(从之后的分析可以看出来这个其实是加密)
成功的条件是chechk()函数返回1,并且key非0
那么接下来去check里看看吧:
http://img.blog.csdn.net/20171013001312946?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvd2hrbGhoaGg=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast
结构看起来不复杂,但是每个case里面一个子函数很让人头疼……
程序不断读取字符串的数据,异或10解密后作为switch
子函数中的处理就很复杂了,每个都不一样,并且最后还会改变字符串的值
不过因为输入的值的ASCII一定在33以上,因此不可能作为swtich部分
尝试在IDA中对输入字符串下读取断点,发现是经过异或后写入字符串中
这种改变程序流程的难度就比较大了……因为逆运算很困难
穷举空间在93^8左右,本身计算也比较耗时,难度有点大
不过我们可以猜测一下流程
异或输入字符,写入字符串,之后再读取该值,正确的话会进入下一步case
错误的话则会直接落入swtich的default中跳出
那么这里就有一个东西可以利用了
运算次数
破解方法学名叫做“边信道攻击”
通过pintools可以注入dll入程序中进行计算指令次数
我们依次对输入字符串下读取断点,可以发现程序的读取过程是从前往后依次的,那么就允许来逐字符爆破了
逐字符爆破的穷举空间就只有93*8的大小了
如果某个字符合格了,那么程序就可以晚落入default中一段时间,通过计算指令次数就能知道这一点
python2爆破代码:
#!/usr/bin/env python
#-*- coding:utf-8 -*-
import popen2,string
INFILE = "test"
CMD = "pin/pin/pin -t pin/pin/source/tools/ManualExamples/obj-ia32/inscount1.so -- IDA/SimpleVM <" + INFILE
choices = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!#$%&'()*+,-./:;<=>?@[\]^_`{|}~"#自定义爆破字典顺序,将数字和小写字母提前可以使得速度快一些~
def execlCommand(command):
global f
fin,fout = popen2.popen2(command)
result1 = fin.readline()#获取程序自带打印信息,wrong或者correct
print result1
if(result1 != 'Input : Wrong\n'):#输出Correct时终止循环
f = 0
result2 = fin.readline()#等待子进程结束,结果输出完成
fin.close()
def writefile(data):
fi = open(INFILE,'w')
fi.write(data)
fi.close()
flag = ''
f = 1
while(f):
l = 0#初始化计数器
for i in choices:
key = flag + i#测试字符串
print ">",key
writefile(key)
execlCommand(CMD)
fi = open('./inscount.out', 'r')
n = int(fi.read(), 10)
fi.close()
print n
if(n-l > 30 and l):#如果两次运行指令差别过大,说明字符正确
flag += i
break
else:
l = n
print flag
得到结果
http://img.blog.csdn.net/20171013003650117?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvd2hrbGhoaGg=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast
这个SimpleVM一点都不Simple……(:з」∠)
不较高深的,我慢慢研究。谢谢。 感谢分享
感谢分享 那一年一个小小的fork()难倒多少程序员
页:
[1]