本帖最后由 小白670 于 2018-5-6 14:48 编辑
1. Bugku 游戏过关 (找到关键函数进入正确跳转,异或得flag)
运行程序,看到界面跟平常的不太一样,是让我们玩个游戏,大概意思就是要让灯都关上,就是让中间的开关闭合,不想玩直接开始逆向
用IDA搜索字符串
发现flag就是程序内的固定字符进行处理然后输出,现在可以直接dump下数据然后写脚本跑出flag,也可以修改程序逻辑,让它直接跳转到正确分支输出flag。但是不管怎么说,这样做都没有达到考察逆向能力的目的,也是这题的缺点,在正规比赛中,是不会出现这种题目的,一般都是最后的输出跟你的输入有关,这样就不能直接修改程序弹出flag,而是要分析程序的校验算法了,这才是真正考察能力的地方。
其实想要修改难度也很简单,你输入的数字要通过这个游戏,最后flag就是你输入的数字,这就倒逼你去分析校验函数,写脚本逆向算法,这也是逆向题的常规做法,找到关键函数,分析对我们输入的检验算法,逆向算法然后跑出正确的输入,而这个正确输入就是我们要的flag!!!这和crackme很类似,就是为了成功注册写个注册机,虽然我看很多人在做160个crackme都是爆破,而不是去耐心的分析算法。之所以说这么多,也是因为我入门的时候做题就是这样,以为修改程序逻辑就算做出来了。
接下来,我们来分析一下它的检验算法,通过搜素字符串很容易找到关键函数(而且这个程序字符串还特别多)程序用了两个循环,一个循环不断接受输入,然后进行处理,最后判断是否正确,正确则输出flag,否则接着循环
接着让我们进去关键函数看一下,里面涉及三个数组,
[C] 纯文本查看 复制代码 if ( a1 )
{
if ( a1 == 7 )
{
v2 = byte_532E28[7] == 0;
byte_532E28[7] = v2;
v2 = byte_532E27[7] == 0;
byte_532E27[7] = v2;
v2 = byte_532E28[0] == 0;
result = 1;
byte_532E28[0] = byte_532E28[0] == 0;
}
else
{
v2 = byte_532E28[a1] == 0;
byte_532E28[a1] = v2;
v2 = byte_532E27[a1] == 0;
byte_532E27[a1] = v2;
v2 = byte_532E29[a1] == 0;
result = v2;
byte_532E29[a1] = v2;
}
}
else
{
byte_532E28[0] = byte_532E28[0] == 0;
v2 = byte_532E29[0] == 0;
byte_532E29[0] = byte_532E29[0] == 0;
v2 = byte_532E28[7] == 0;
result = 1;
byte_532E28[7] = v2;
}
return result;
三个数组是连着的,最后只需要byte_532E28数组全为1即可,我上面画了简陋的示意图,大概意思就是这三个数组指向的数据有重复。分析可知这个函数就是说打开一盏灯,可能就会关上另外一盏灯,根据它们之间的规律,就可以得出正确的开灯顺序,如果题目设置成正确的开灯顺序就是flag的话,这题会更有意思点。
检验算法分析到这里,这题我懒得算正确的输入了,既然可以爆破那我就省点力气了,用OD搜索字符串,找到输出flag的函数,找到跳转,转到call处
往上找到第一个判断的地方,下断点,然后运行时修改ZF标志位,就可以顺利到达call flag的地方
最后flag就自己出来了
2. BugKu love(base64加密,明文比较)Ida打开,shift+12查看字符串,找到提示字符串,找到关键函数,F5分析
首先让我们输入字符串,接着对字符串处理,返回值拷贝给一个新的字符串Dest,然后比较字符串,这里的str2以明文e3nifIH9b_C@n@dH存放在程序中。 先看下sub_41127()这个函数,可以看到最后是通过判断eax的值来跳转的,strncmp的返回值是存放在eax中的,如果sub_41127()改变了eax的值,则说明在sub_41127()函数中还有另外的玄机
这里分析一下,在函数里面有调用一个函数,add esp,8说明是cdecl调用,并且参数有2个,所以上面有两个push是压入参数,剩下的push和pop对应一下,发现eax的值是原来push的值,也就是说这个函数没有改变eax的值,所以这个函数没啥作用
接着分析处理字符串的函数,对密码学有所了解的人可以看出这是base64加密算法,这就很简单了,对e3nifIH9b_C@n@dH先循环递增减去i,然后进行base64解密就得到最后结果了
[C] 纯文本查看 复制代码 if ( my_str && len )
{
v7 = len / 3;
if ( (signed int)(len / 3) % 3 )
++v7;
v7 *= 4;
*a3 = v7;
malloc(v7 + 1);
Dst = (void *)sub_411127();
if ( Dst )
{
j_memset(Dst, 0, v7 + 1);
v9 = my_str;
v7 = len;
i = 0;
v5 = 0;
while ( v7 > 0 )
{
byte_41A144[2] = 0;
byte_41A144[1] = 0;
byte_41A144[0] = 0;
for ( i = 0; i < 3 && v7 >= 1; ++i )
{
byte_41A144[i] = *v9;
--v7;
++v9;
}
if ( !i )
break;
v4 = i;
if ( i == 1 )
{
*((_BYTE *)Dst + v5++) = aAbcdefghijklmn[(signed int)(unsigned __int8)byte_41A144[0] >> 2];
*((_BYTE *)Dst + v5++) = aAbcdefghijklmn[((byte_41A144[1] & 0xF0) >> 4) | 16 * (byte_41A144[0] & 3)];
*((_BYTE *)Dst + v5++) = aAbcdefghijklmn[64];
*((_BYTE *)Dst + v5++) = aAbcdefghijklmn[64];
}
else if ( v4 == 2 )
{
*((_BYTE *)Dst + v5++) = aAbcdefghijklmn[(signed int)(unsigned __int8)byte_41A144[0] >> 2];
*((_BYTE *)Dst + v5++) = aAbcdefghijklmn[((byte_41A144[1] & 0xF0) >> 4) | 16 * (byte_41A144[0] & 3)];
*((_BYTE *)Dst + v5++) = aAbcdefghijklmn[((byte_41A144[2] & 0xC0) >> 6) | 4 * (byte_41A144[1] & 0xF)];
*((_BYTE *)Dst + v5++) = aAbcdefghijklmn[64];
}
else if ( v4 == 3 )
{
*((_BYTE *)Dst + v5++) = aAbcdefghijklmn[(signed int)(unsigned __int8)byte_41A144[0] >> 2];
*((_BYTE *)Dst + v5++) = aAbcdefghijklmn[((byte_41A144[1] & 0xF0) >> 4) | 16 * (byte_41A144[0] & 3)];
*((_BYTE *)Dst + v5++) = aAbcdefghijklmn[((byte_41A144[2] & 0xC0) >> 6) | 4 * (byte_41A144[1] & 0xF)];
*((_BYTE *)Dst + v5++) = aAbcdefghijklmn[byte_41A144[2] & 0x3F];
}
}
*((_BYTE *)Dst + v5) = 0;
}
}
如果不熟悉base64加密函数的话,现逆算法的话还是有点麻烦和费时的,这需要一定的识别算法能力,因为大多数加密算法都是用现成的或者对现有的进行改进利用 要逆base64算法。
首先先了解一下base64加密的原理转换的时候,将三个byte的数据,先后放入一个24bit的缓冲区中,先来的byte占高位。数据不足3byte的话,于缓冲器中剩下的bit用0补足。然后,每次取出6(因为26=64)个bit,按照其值选择ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/中的字符作为编码后的输出。不断进行,直到全部输入数据转换完成。 当原数据长度不是3的整数倍时, 如果最后剩下一个输入数据,在编码结果后加2个“=”;如果最后剩下两个输入数据,编码结果后加1个“=”;如果没有剩下任何数据,就什么都不要加,这样才可以保证数据还原的正确性。 这里还有一篇博文有实例介绍:https://blog.csdn.net/u013412497/article/details/51552335
最后我就写了一个简单的脚本:
[Python] 纯文本查看 复制代码 import base64
str2="e3nifIH9b_C@n@dH"
flag=""
for i in range(len(str2)):
flag+=chr(ord(str2[i])-i)
print base64.b64decode(flag)
Flag是{i_l0ve_you}
3. 南邮ctf WxyVM1(要用脚本处理大量数据)
用ida分析,发现关键函数都在main函数中,输入字符串后先调用一个函数,再判断输入的字符串长度是否为24,接着和一个数组比较值,如果相等的话,v4的值就为1,程序puts正确
乍一看不怎么难,首先看下sub_4005B6这个函数,如果没有对输入字符串进行处理的话,那么这题就结束了,哈哈哈哈哈怎么可能这么简单呢,这好歹也是我选的肯定要比入门级难
进去后发现是一个很长的循环,为了好辨识,我把刚才的输入命名为my_str,可以看到这就是根据byte_6010C0数组的值来选择switch的不同分支,然后选择性的和byte_6010C0数组的值进行运算。其实就算法来说并不复杂,但是在处理数据的复杂度上可以说是比一般题目要难。 在这个时候,就需要用到我们idc脚本了,ida集成支持idc和python脚本,这题用idc脚本的话很快就能得到flag
简单介绍下idc脚本,idc和c语言很像,在idc中,变量命名叫auto,比如说定义i,在idc中写auto i;
ida有很多内带的函数,我们这题要用到的一个函数叫PatchByte
ida自带的说明如下:
// Change value of a program byte
// If debugger was active then the debugged process memory will be patched too
// ea - linear address
// value - new value of the byte
// Returns: 1 if successful, 0 if notsuccess PatchByte (long ea,long value);
// change a byte
这个函数的作用就是改变程序里一个byte的值
Idc脚本里必须声明main函数
static main()
{
//代码
}
Idc的介绍就到这里,下面是我们的脚本:
[C] 纯文本查看 复制代码 static main()
{
auto i;
for ( i = 14997; i >= 0; i = i - 3 )
{
auto v0 = Byte(0x6010C0+i);
auto v3 = Byte(0x6010C0+(i + 2));
auto result = v0;
if(v0==1){
result =Byte(0x6010C0+i + 1);
PatchByte(0x601060 + result*4,Byte(0x601060 + result*4)-v3);
}
if(v0==2){
result =Byte(0x6010C0+i + 1);
PatchByte(0x601060 + result*4,Byte(0x601060 + result*4)+v3);
}
if(v0==3){
result =Byte(0x6010C0+i + 1);
PatchByte(0x601060 + result*4,Byte(0x601060 + result*4)^v3);
}
if(v0==4){
result =Byte(0x6010C0+i + 1);
PatchByte(0x601060 + result*4,Byte(0x601060 + result*4)/v3);
}
if(v0==5){
result =Byte(0x6010C0+i + 1);
PatchByte(0x601060 + result*4,Byte(0x601060 + result*4)^Byte(0x601060+v3*4));
}
else
continue;
}
for(i=0;i<24;i++)
Message("%c",Byte(0x601060+i*4));//最后输出修改后既逆向得到的flag
}
整个idc脚本结构和分析的函数一样,所以我没写注释了,地址在ida可以看到,在ida中载入脚本,就能一键出flag
消息的输出是在ida下方的窗口,可以看到flag已经出来了 这题主要是介绍idc脚本的使用,并未涉及到更高深的用法,但是也可以认识到idc对提高逆向效率的实用性
4.实验吧 分道扬镳 (分析可知是在走迷宫,移动顺序既flag)
这个题目挺有意思的,除了验证函数不同,还加了一个混淆的错误验证算法。
同样可以搜索字符串找到关键函数,首先判断长度是否等于22,然后在一个循环里,一位一位判断输入,其中输入只能为107 ,106 ,104,108,转化为ascii码为k,j,h,l,接下来的判断需要先分析v3,v5和v6的值
程序中并没有特意给v3,v5,v6赋值,所以需要分析变量存储的实际情况,看紫色框是在栈帧中的偏移量,在栈中可以看到,v5在v4后面,v6在v5后面,所以给v4赋值字符串时,v5和v6处也覆盖了字符串file:///C:\Users\ASUS\AppData\Local\Temp\ksohtml\wpsF834.tmp.jpg file:///C:\Users\ASUS\AppData\Local\Temp\ksohtml\wpsF835.tmp.jpg
知道这些变量的值之后,分析下面四个不同的分支,发现了以下规律
104 - h - v3--
106 - j - v3+=8
108 - l - v3++
107 - k - v3-=8
其中v3都不能等于42,既*,而等于35既#就成功了 再看v4字符串,只有一个#,这看上去不就像是走迷宫么,h,l表示左右,k,j表示上下
********* * ** * ** ** * ** ** * #* ** **** ** *********
结合前面的分析,v5的位置就是起始位置,把字符串复制到文档中,每8个一行,得到如下迷宫,起始位置和终点位置找到以后,再根据刚才分析出的方向键,即可得出正确的走法file:///C:\Users\ASUS\AppData\Local\Temp\ksohtml\wpsF836.tmp.jpg
jjjjjlllllkkkkkhhhjjjl
当然了,其实它还有另一条线,真正运行时不走那条线,而是先判断迷宫这条线而且也只是简单的异或,没有迷宫好玩,关键是那个函数算出来的flag还是假的哈哈哈。 file:///C:\Users\ASUS\AppData\Local\Temp\ksohtml\wpsF847.tmp.jpg
未完待续······ |