学破解第141天,《攻防世界reverse练习区IgniteMe》学习
前言:
从小学到大专(计算机网络技术专业),玩过去的,所以学习成绩惨不忍睹,什么证书也没考,直到找不到工作才后悔,不知道怎么办才好。
2017年12月16日,通过19元注册码注册论坛账号,开始做伸手党,潜水一年多,上来就是找软件。(拿论坛高大上的软件出去装X)
2018年8月10日,报名了华中科技大学网络教育本科(计算机科学与技术专业)2018级秋季。(开始提升学历)
2019年6月17日,不愿再做小菜鸟一枚,开始零基础学习破解。(感谢小糊涂虫大哥在我刚开始学习脱壳时,录制视频解答我的问题)
2020年7月7日,感谢H大对我的鼓励,拥有了第一篇获得优秀的文章。(接下来希望学习逆向,逆天改命)
2021年8月11日,华科学位英语2次不过,仅取得了毕业证书,学业提升失败,开始琢磨考注册类和职称类证书,谋求涨薪
坛友们,年轻就是资本,和我一起逆天改命吧,我的学习过程全部记录及学习资源:https://www.52pojie.cn/thread-1278021-1-1.html
立帖为证!--------记录学习的点点滴滴
0x1 下载文件
1.下载CM,是一个exe文件。
2.exeInfo工具查壳:
3.使用studyPE查看程序信息:
4.可能有的坛友不知道为什么我都要重复这个步骤,这里说明下:
第一步看文件类型,exe是win平台,elf是Linux平台,能在win平台跑我就可以动态调试了。
第二步看目标程序是否有壳,以决定是脱壳还是带壳打补丁。
第三步主要是看程序基址是否固定以便固定基址方便调试和打补丁,另自带有密码学探测工具,这里暂时用不到。
5.通过上述信息,我知道了这个程序大概率是vc++6.0编写的32位程序,无壳,固定基址,未使用标准加密算法。
0x2 OD调试分析
1.既然是exe文件还是先爆破,将程序载入OD,搜索字符串,进度条拉到最上方:
2.回车,在几处错误提示的字符串处F2下断点:
00401069 |. 83F8 04 cmp eax,0x4
0040106C |. 77 25 ja short fac4d129.00401093
0040106E |> 68 70364000 push fac4d129.00403670
00401073 |. 68 60B14300 push fac4d129.0043B160 ; Sorry, keep trying!
004010F5 |. 3BD1 |cmp edx,ecx
004010F7 |. 74 25 |je short fac4d129.0040111E
004010F9 |. 68 70364000 |push fac4d129.00403670
004010FE |. 68 40B14300 |push fac4d129.0043B140 ; Sorry, keep trying!
004010F5 |. 3BD1 |cmp edx,ecx
004010F7 |. 74 25 |je short fac4d129.0040111E
004010F9 |. 68 70364000 |push fac4d129.00403670
004010FE |. 68 40B14300 |push fac4d129.0043B140 ; Sorry, keep trying!
00401124 |. 83FA 7D cmp edx,0x7D
00401127 |. 74 22 je short fac4d129.0040114B
00401129 |. 68 70364000 push fac4d129.00403670
0040112E |. 68 40B14300 push fac4d129.0043B140 ; Sorry, keep trying!
0040115C |. 85C0 test eax,eax ; kernel32.BaseThreadInitThunk
0040115E |. 75 22 jnz short fac4d129.00401182
00401160 |. 68 70364000 push fac4d129.00403670
00401165 |. 68 40B14300 push fac4d129.0043B140 ; Sorry, keep trying!
3.将程序跑起来,输入123456,断在0040106C 这里,单步往下走看看:
00401069 |. 83F8 04 cmp eax,0x4
这里必须跳过去,否则失败,eax的值6,可以推测就是我输入的123456的长度
004010F5 |. 3BD1 |cmp edx,ecx
edx是0x31,可以猜到是输入的第一个字符1对应的ASCII值,ecx是0x45 E,判断两个值是否相等,不相等就失败,这里我们在寄存器窗口点Z,改变标志,让它跳过这个失败,开始下一次循环。
https://imgtu.com/i/hdmqtU
第一轮循环
ECX 00000045 E
EDX 00000031 1
第二轮循环
ECX 00000049 I
EDX 00000032 2
第三轮循环
ECX 00000053 S
EDX 00000033 3
第四轮循环
ECX 0000007B {
EDX 00000034 4
00401124 |. 83FA 7D cmp edx,0x7D
这里目前还没看出来什么意思,改跳转继续往正确的流程走
0040114F |. E8 6C000000 call fac4d129.004011C0
00401154 |. 83C4 04 add esp,0x4
00401157 |. 25 FF000000 and eax,0xFF
0040115C |. 85C0 test eax,eax
0040115E |. 75 22 jnz short fac4d129.00401182
再往下只有Sorry, keep trying! 和Congratulations! 两条路。
4.显然0040114F这里调用的call就是关键函数了,在回到第一个断点往上翻:
00401058 |. 83F8 1E cmp eax,0x1E
0040105B |. 73 11 jnb short fac4d129.0040106E
0040105D |. 8D55 80 lea edx,[local.32]
00401060 |. 52 push edx
00401061 |. E8 6AE20100 call fac4d129.0041F2D0
00401066 |. 83C4 04 add esp,0x4
00401069 |. 83F8 04 cmp eax,0x4
0040106C |. 77 25 ja short fac4d129.00401093
0040106E |> 68 70364000 push fac4d129.00403670
00401073 |. 68 60B14300 push fac4d129.0043B160 ; Sorry, keep trying!
这里判断了0x1E和0x4,jnb不小于则跳转,ja大于则跳转,根据前面推测,eax是我输入字符串的长度,所以我输入的字符串长度应该是4-30位,而开头的4位必须是“EIS{”
5.到这里把0040115E这个jnz让它跳过就成功爆破了。
6.当然了,爆破对CTF题来说意义不大,接下来还是分析一下算法吧。
0x3 IDA代码分析
1.将程序用32位IDA打开,找到main函数,F5反编译一下:
int __cdecl main(int argc, const char **argv, const char **envp)
{
int result; // eax
size_t i; // [esp+4Ch] [ebp-8Ch]
char v5[8]; // [esp+50h] [ebp-88h] BYREF
char Str[128]; // [esp+58h] [ebp-80h] BYREF
sub_402B30(&unk_446360, "Give me your flag:");
sub_4013F0(sub_403670);
sub_401440(Str, 127);
if ( strlen(Str) < 0x1E && strlen(Str) > 4 )
{
strcpy(v5, "EIS{");
for ( i = 0; i < strlen(v5); ++i )
{
if ( Str[i] != v5[i] )
goto LABEL_7;
}
if ( Str[28] != 125 )
{
LABEL_7:
sub_402B30(&unk_446360, "Sorry, keep trying! ");
sub_4013F0(sub_403670);
return 0;
}
if ( (unsigned __int8)sub_4011C0(Str) )
sub_402B30(&unk_446360, "Congratulations! ");
else
sub_402B30(&unk_446360, "Sorry, keep trying! ");
sub_4013F0(sub_403670);
result = 0;
}
else
{
sub_402B30(&unk_446360, "Sorry, keep trying!");
sub_4013F0(sub_403670);
result = 0;
}
return result;
}
2.前面和我在OD中分析的一样,先判断我输入的字符串长度是否满足要求,再看是否为EIS{开头,再调用sub_4011C0函数验证
if ( (unsigned __int8)sub_4011C0(Str) )
sub_402B30(&unk_446360, "Congratulations! ");
通过OD查找调用过程,发现从004019C3这个地址调用的004017F0函数,向上翻找到段首00401890。
3.看一看sub_4011C0函数反编译后的代码:
bool __cdecl sub_4011C0(char *Str)
{
size_t v2; // eax
int v3; // [esp+50h] [ebp-B0h]
char Str2[32]; // [esp+54h] [ebp-ACh] BYREF
int v5; // [esp+74h] [ebp-8Ch]
int v6; // [esp+78h] [ebp-88h]
size_t i; // [esp+7Ch] [ebp-84h]
char v8[128]; // [esp+80h] [ebp-80h] BYREF
if ( strlen(Str) <= 4 )
return 0;
i = 4;
v6 = 0;
while ( i < strlen(Str) - 1 )
v8[v6++] = Str[i++];
v8[v6] = 0;
v5 = 0;
v3 = 0;
memset(Str2, 0, sizeof(Str2));
for ( i = 0; ; ++i )
{
v2 = strlen(v8);
if ( i >= v2 )
break;
if ( v8[i] >= 97 && v8[i] <= 122 )
{
v8[i] -= 32;
v3 = 1;
}
if ( !v3 && v8[i] >= 65 && v8[i] <= 90 )
v8[i] += 32;
Str2[i] = byte_4420B0[i] ^ sub_4013C0(v8[i]);
v3 = 0;
}
return strcmp("GONDPHyGjPEKruv{{pj]X@rF", Str2) == 0;
}
while ( i < strlen(Str) - 1 )
v8[v6++] = Str[i++];
v8[v6] = 0;
将str字符串给到char数组v8,末尾补0;
memset(Str2, 0, sizeof(Str2));
初始化str2字符串
接下来就是一段关键的字符串处理了,最后比较str2和GONDPHyGjPEKruv{{pj]X@rF是否相等,这段for循环可以像前面一样不用理解它做了什么,直接等式变换,大于变小于等于,加变减,异或不用变,直接用它的算法些脚本得到flag,这里我们学习一下阅读代码,如果能读懂一会写脚本就不会知其然不知其所以然。
4.继续看看for循环这段代码:
v2 = strlen(v8);
if ( i >= v2 )
break;
这里就是判断是否超过我输入的字符串长度了,超过了直接跳出循环,失败
if ( v8[i] >= 97 && v8[i] <= 122 )
{
v8[i] -= 32;
v3 = 1;
}
这里急判断如果我输入的是小写字母就变成大写字母,这里v3的作用目前还看不出来。
if ( !v3 && v8[i] >= 65 && v8[i] <= 90 )
v8[i] += 32;
如果是大写字母就变成小写字母,使用完后v3又重置为0,v3是大小写判断,例如前面判断小写了,则这里不再判断是否为大写。
Str2[i] = byte_4420B0[i] ^ sub_4013C0(v8[i]);
这段代码就这句有用,这里又调用了一个函数
int __cdecl sub_4013C0(int a1)
{
return (a1 ^ 0x55) + 72;
}
所以str2[i] = byte_4420B0[i]^((v8[i] ^ 0x55) + 72),循环执行。
byte_4420B0[i]是一个char数组,存储的内容为:
为了便于一会容易理解,将str2[i] = byte_4420B0[i]^((v8[i] ^ 0x55) + 72)拆分:
v8[i] = v8[i] ^ 0x55
v8[i] = v8[i] + 72
str2[i] = v8[i] ^ byte_4420B0[i]
倒过来就是:
v8[i] = str2[i] ^ byte_4420B0[i]
v8[i] = v8[i] -72
v8[i] = v8[i] ^ 0x55
5.整理后的代码:
package ctf0;
public class Main {
public static void main(String[] args) {
// TODO Auto-generated method stub
char temp[] = { 0x0D, 0x13, 0x17, 0x11, 0x2, 0x1, 0x20, 0x1D, 0x0C, 0x2, 0x19, 0x2F, 0x17, 0x2B, 0x24, 0x1F,
0x1E, 0x16, 0x9, 0xF, 0x15, 0x27, 0x13, 0x26, 0x0A, 0x2F, 0x1E, 0x1A, 0x2D, 0x0C, 0x22, 0x4 };
StringBuffer str = new StringBuffer("GONDPHyGjPEKruv{{pj]X@rF");
StringBuffer flag = new StringBuffer("GONDPHyGjPEKruv{{pj]X@rF");
boolean on = true;//大写
for (int i = 0; i < str.length(); i++) {
flag.setCharAt(i, (char) (str.charAt(i)^temp[i]));
flag.setCharAt(i, (char) (flag.charAt(i)-72));
flag.setCharAt(i, (char) (flag.charAt(i)^0x55));
if(flag.charAt(i)>=97&&flag.charAt(i)<=122)
{
on = false;//小写
flag.setCharAt(i,(char) (flag.charAt(i)-32));
}
if(on==true&&flag.charAt(i)>=65&&flag.charAt(i)<=90)
{
flag.setCharAt(i,(char) (flag.charAt(i)+32));
}
on = true;
}
System.out.println(flag);
}
}
运行后:wadx_tdgk_aihc_ihkn_pjlm
6.提交flag,发现一直不对???什么情况!!!一脸懵X,回去复盘一下代码,看看忽略了什么,还记得前面OD分析的时候,必须EIS{开头吗?再去看看:
if ( strlen(Str) < 0x1E && strlen(Str) > 4 )
这是长度要求
strcpy(v5, "EIS{");
for ( i = 0; i < strlen(v5); ++i )
{
if ( Str[i] != v5[i] )
goto LABEL_7;
}
这是判断flag开头前四位是否是EIS{
if ( Str[28] != 125 )
这是判断flag第29位是}
i = 4;
v6 = 0;
while ( i < strlen(Str) - 1 )
v8[v6++] = Str[i++];
再看,i是从4开始的,赋值时去掉了前四位,strlen(Str) - 1,赋值时去掉了最后一位。
所以flag应该是EIS{wadx_tdgk_aihc_ihkn_pjlm}
7.再次提交flag,验证通过:
0x4总结
1.分析的时候应该逐步推进,不能只盯着关键函数,注意循环时变量的起始位置,字符串拷贝不一定全部复制过去了。
2.flag不对时小菜鸟差点哭了,明明分析的没问题,为什么会这样,最后冷静下来,耐心复盘就找到了原因。
3.编写脚本时用了不熟悉的java,查找函数耗费了一些时间(不能总是只会一门c语言吧,适当了解别的语言)
4.这是道1分题,主要是还是大意了,将脚本跑起来以为就成功了,却忘了前面为什么判断EIS{了。
5.尝试读懂代码,更有利于编写脚本。
0x5参考资料
1.Java™ Platform, Standard Edition 8API Specification
PS:善于总结,善于发现,找到分析问题的思路和解决问题的办法。虽然我现在还是零基础的小菜鸟一枚,也许学习逆向逆天改命我会失败,但也有着成功的可能,只要还有希望,就决不放弃!