参加CTF(Capture The Flag)是提高网络安全技术水平的一种有效途径,其解题模式中主要提供了逆向、漏洞挖掘与利用、Web渗透等题目,作为业余爱好者,这不失为一种锻炼自己思路的好方法。2019年03月10日开始的“2019看雪CTF(第一季)”已经落下帷幕,本文就从第一关开始,详细解析本季CTF的比赛题目。本文面向读者为初学者,所以部分分析过程可能会详细一些, 高手可以略过 。
一、初窥门径
首先,我们登录ctf.pediy.com,找到“2019看雪CTF晋级赛Q1”,然后就从第1关开始我们的夺旗之路吧。
第一关“流浪者”,题目要求输入正确的注册码。我们将程序下载下载后双击,会出现一个输入注册码的界面。此时我们随意输入一串字符并点击“验证”,程序会弹出一个消息框,提示我们输入错误。如图:
如果你曾有些逆向的入门知识的话,这算是逆向工程中最常见的场景了。按照常规的软件运行逻辑(获取输入的字符串 --> 对字符串进行处理 --> 与真正的注册码做比较 --> 如果正确则进行下一步、错误则弹出消息框),我们只需进行反向操作,即先根据消息框上面的文字,找到调用该消息框的函数,再通过层层回溯,找到处理字符串的关键函数,进而窥探出注册码的庐山真面目。那么在这里,我们需要记下消息框上的关键线索:“错了!加油!”,然后用反汇编软件对目标程序进行详细剖析。
二、庖丁解牛
在这里,我们使用著名的反汇编软件IDA ,首先打开IDA,载入目标程序。经过一番载入后,界面停在了程序的入口点:
那么,我们下一步的任务,就是从目标程序里面找到我们记下的关键线索——那条叫做“错了!”的字符串。我们在 IDA 菜单栏中点选“View”-->“Open subview”-->“Strings”:
然后IDA界面上就显示出了目标程序内的字符串。看,里面有我们寻找的线索:
此时,我们双击该字符串,就来到了该字符串对应的位置(先不要管这个红色线框):
看到上图红色线框里面的字符“sub_4017B0”了吗?这就是引用“关键线索”字符串的函数,我们双击这个函数,来到了它的地址:
从图中的汇编代码中可知,这是一个弹出消息框的函数,并不参与注册码的相关计算。再看红色线框的内容,是调用这个函数的“上家”函数sub_4017F0,我们且双击一下它,看看“上家”在搞什么鬼?
在双击过后,界面显示了sub_4017F0函数的地址:
哦……看看红色线框里面的内容,是在操作某些字符串!是不是感觉有了点眉目了?但这一堆压栈、读写寄存器等汇编指令看得人头大,我们在这个界面上按下键盘上的“F5”,IDA会将这个函数的汇编代码翻译为C语言风格的伪代码:
这下真相大白了,原来“上家”都干了这些事情!我们分析一下代码:
int __cdecl sub_4017F0(int a1)//传入一个int变量a1
{
int result; // eax@6
char Str1[28]; // [sp+D8h] [bp-24h]@4
int v3; // [sp+F4h] [bp-8h]@1
int v4; // [sp+F8h] [bp-4h]@1
v4 = 0;
v3 = 0;
//以a1的值为基础地址,每次累加4字节,读取对应的内存数据。由此可推断出a1是一个字符串的首地址,该字符串数
//据类型为DWORD,而1个DWORD所占空间为4字节,所以要以4的倍数来移动
while ( *(_DWORD *)(a1 + 4 * v4) < 62 && *(_DWORD *)(a1 + 4 * v4) >= 0 )
{
//将依次读取的a1[ ]的值作为aAbcdefghiabcde[ ]数组的下标,取得相应的值存入数组Str1[ ]中。
Str1[v4] = aAbcdefghiabcde[*(_DWORD *)(a1 + 4 * v4)];
++v4;
}
Str1[v4] = 0;
//将Str1[ ]与"KanXueCTF2019JustForhappy"进行对比,如果相同则调用函数sub_401770(),如果不同则调用函数
//sub_4017B0(注意,这个就是之前弹报错消息框的函数)
if ( !strcmp(Str1, "KanXueCTF2019JustForhappy") )
result = sub_401770();
else
result = sub_4017B0();
return result;
}
看到这里,我们就知道了,原来函数sub_4017F0是对某个叫a1[ ]的字符串当成了索引,用它来查阅aAbcdefghiabcde[ ]的内容,然后把查阅出来的内容与"KanXueCTF2019JustForhappy"进行对比。
那么,aAbcdefghiabcde[ ]数组里面的内容是啥呢?我们再次回顾到函数sub_4017F0的反汇编代码处:
哦,原来是“KanXueCTF2019JustForhappy”下面的那一串字符啊!注册码比对的原理搞明白了,胜利就在前方!
但是先别着急,还有最后一个问题:这个用来作对比的a1[ ]数组是从哪儿来的呢?是我们一开始输入到程序里的那串字符吗?还是有别的情况?带着这个疑惑,我们回到sub_4017F0函数的界面,发现该函数还有个“上家”sub_401890在调用它:
我们还用老办法,双击红色线框内的sub_401890,跳到这个函数。然后鼠标滚轮往上滚几下,来到函数的首地址00401890:
看,从反汇编代码中可以看出,这个函数要从程序的输入框中读取字符了!看来这就是计算注册码的第一个关口了。为了方便分析,我们继续用“F5”来读(伪)源代码:
坚持住,这是最后一步了!我们对代码进行分析:
//把输入的字符放到字符串数组Str[ ]中
Str = CString::GetBuffer((CWnd *)((char *)v8 + 100), v3);
//遍历整个数组,对数组的值进行处理
if ( strlen(Str) )
{
for ( i = 0; Str[i]; ++i )
{
if ( Str[i] > 57 || Str[i] < 48 )
{
if ( Str[i] > 122 || Str[i] < 97 )
{
if ( Str[i] > 90 || Str[i] < 65 )
sub_4017B0();
else
v5[i] = Str[i] - 29;//如果Str[i]的元素的值在65~90之间,就把它减去29,将结果放到v5[i]中去;
}
else
{
v5[i] = Str[i] - 87;//如果Str[i]的元素的值在97~122之间,就把它减去29,将结果放到v5[i]中去;
}
}
else
{
v5[i] = Str[i] - 48;//如果Str[i]的元素的值在57~48之间,就把它减去29,将结果放到v5[i]中去;
}
}
result = sub_4017F0((int)v5);//将数组v5[]传递给函数sub_4017F0 ;
}
else
{
result = CWnd::MessageBoxA(v8, "请输入pass!", 0, 0);
}
return result;
从上面可以看出,函数sub_401890主要是对输入的字符的值进行逐个处理。如果字符的值在48~57之间,就将其减去48;如果在65~90之间,就将其减去29;如果在97~122之间,就将其减去87。然后将处理过的数组存放到v5[ ]里面。注意,这里面的值是ASCII码!通过查阅ASCII码表可以发现:48-57对应的是数字字符“0”~“9”,65-90对应的是大写字母“A”~“Z ”,97-122对应的是小写字母“a”~“z”。
综上所述,该程序的运行流程为:[双击启动程序 ]--> [输入注册码] --> [函数sub_401890获取注册码并进行处理]--> [将处理后的字符串传递给函数sub_4017F0函数,该函数以处理后的字符串数组为索引,从aAbcdefghiabcde[ ]里面查表] --> [如果查出的结果等于 “KanXueCTF2019JustForhappy”则通过,若不相等则报错]
三、见招拆招
知道了原理,那破译出注册码的算法也就简单了,我们只需要将其反向操作一下即可。
以“KanXueCTF2019JustForhappy”的第一个字符“K”为例,“K”字符在aAbcdefghiabcde[ ]字符串的第19个位置,(aAbcdefghiabcde[ ] =“abcdefghiABCDEFGHIJKLMNjklmn0123456789opqrstuvwxyzOPQRSTUVWXYZ”,且数组第一个下标是以0为起点的),而且48~57减去48对应的值是[0~9];65~90减去29对应的值是[36~61];97~122减去87对应的值是[10~35];那么刚才“K”字符对应的19,落在了[10~35]的区间,可以得知“K”对应的注册码应该是19+87=106,通过查阅ASCII码可知,对应的是小写字母“j”。
那么,我们用编程来实现一下。上Python代码:
str_A = 'KanXueCTF2019JustForhappy'
str_B = 'abcdefghiABCDEFGHIJKLMNjklmn0123456789opqrstuvwxyzOPQRSTUVWXYZ'
str_C = []
for i in str_A:
str_C.append(str_B.index(i))
password = ''
for i in str_C:
if i >= 0 and i <10:
password += chr(i+48)
if i >= 9 and i < 36:
password += chr(i+87)
if i >= 35 and i < 62:
password += chr(i+29)
print(password)
运行结果如下:
看,注册码被计算出来了。这时我们把蓝色字符输入到程序中,CTF第一关就通过啦:
四、后记
其实第一关没有什么难度,但由于本文主要面向新手,所以重点讲解了IDA的基本操作 、字符串及函数的交叉引用、DWORD数据结构的存储原理 、简单的算法分析等一些基本功。随着第一关的顺利通过,我们稍事休息,在第二关见!
附本题目下载地址: 2019看雪CTF > 晋级赛Q1 > 第一题 流浪者
*本文作者:张召忠,转载请注明来自FreeBuf.COM