无壳无花无反调无VM,只有简单算法的CM
如题,也许应该叫ReverseMe,什么都没有只有几个简单算法,也是很常见的算法,就是用了比较别扭的方法实现了。只要知道是什么算法应该就秒了。不知道能活多久。由于没怎么限制输入字符串长度,应该有多解。成功图
失败图
爆破应该是没有用的。
本帖最后由 梦游枪手 于 2019-8-28 20:57 编辑
真的没人玩了。。。惨,在这层公布flag和破解思路。后面来的想练习的同学可以先不看这层。
公布flag,破解思路。
flag:W6bjWfJcrU
思路
题外话:其实我不喜欢CM作者自己写破解思路的,因为作者一开始就知道程序运行机制是怎样的,没有程序源码的cracker有些时候看到这种破解思路会蒙圈。
程序丢进IDA,找到main函数,粗略分析如下
int __cdecl sub_40217D(int argc, const char **argv, const char **envp)
{
unsigned int len; // eax
int v4; // eax
int output; //
__int16 v7; //
int v8; //
int Str; //
_BYTE v10; //
int v11; //
BOOL v12; //
int v13; //
sub_402340();
Str = 0;
v11 = 0;
memset(
(void *)((unsigned int)v10 & 0xFFFFFFFC),
0,
4 * (((unsigned int)((char *)&Str - ((unsigned int)v10 & 0xFFFFFFFC) + 255) & 0xFFFFFFFC) >> 2));
output = 0;
v8 = 0;
memset(
(void *)((unsigned int)&v7 & 0xFFFFFFFC),
0,
4 * (((unsigned int)((char *)&output - ((unsigned int)&v7 & 0xFFFFFFFC) + 255) & 0xFFFFFFFC) >> 2));
v13 = 0;
initlinklist(); // 初始化双向链表,成员为1,5,2,7,4,3,8,0,6
scan((int)&Str); // scanf %s,但是%s被加密
if ( isalnum((char *)&Str) ) // 判断输入是否为字母和数字组合
{
len = strlen((const char *)&Str);
v13 = base64_decode((int)&Str, len, &output);// base64解码,码表可以用byte_405020转换得到
// 码表为dG/oHhf418OsS+IEUtgQzu6lvbWr2LmXqwVBFyn9MeRPcjZTCiK0NkYD5p73xJAa
v12 = run((int)&output, v13); // 解码后的结果放到run函数里面去跑
if ( v12 )
{
v4 = issuccess(); // 判断链表成员是否为想要的结果,判断条件是链表的下一个成员大于上一个成员,且最后一个成员为0即返回成功
// 也就是说,链表的最终结果为1,2,3,4,5,6,7,8,0
printsuccesorfailed(v4);
}
else
{
printsuccesorfailed(0);
}
}
else
{
printsuccesorfailed(0);
}
return 0;
}
基本上函数做的操作我都注释了,不懂的之后再问吧。
梳理下流程,输入字符串先base64解码,然后将解码结果作为run函数的参数并执行run函数,只要run函数将链表成员从1,5,2,7,4,3,8,0,6转为1,2,3,4,5,6,7,8,0即成功。
细看下run函数
BOOL __cdecl run(int a1, int a2)
{
int op; // eax
int i; //
signed int v5; //
v5 = 0;
for ( i = 0; i < a2; ++i )
{
op = *(unsigned __int8 *)(i + a1) - i - 99; // 解密输入,并作为switch case的参数
if ( op == 4 )
{
if ( !move(linklisthead, 4) )
v5 = 1; // move失败则flag无效
}
else if ( op > 4 )
{
if ( op == 6 )
{
if ( !move(linklisthead, 6) )
v5 = 1;
}
else
{
if ( op != 8 )
{
LABEL_17:
v5 = 1;
goto LABEL_18;
}
if ( !move(linklisthead, 8) )
v5 = 1;
}
}
else
{
if ( op != 2 )
goto LABEL_17;
if ( !move(linklisthead, 2) )
v5 = 1;
}
LABEL_18:
if ( v5 )
return v5 == 0;
}
return v5 == 0;
}
该函数里可以看出,解密输入后,输入的字符串里只有{2,4,6,8},函数成功条件为move函数不能返回0。在已经知道base64的码表以及和输入字符串结构的情况,可以自行生成一串字符,OD跟踪一下观察链表顺序的变化,就知道move函数是干啥的了。这里就直接公布吧。
run函数实际上是个推箱子游戏,初始化的链表实际上是初始化了这样一个布局的推箱子。
1,5,2
7,4,3
8,0,6
0表示空地
输入的字符串解密后,2表示0向上移动,4表示向下移动,6表示向左移动,8表示向右移动。
只要将布局还原为
1,2,3
4,5,6
7,8,0
就算成功了。
为什么要用链表而不是数组?我觉得链表在内存中的分布是分散的,而在程序中可以依靠指针将它们串连在一起,而且IDA的伪代码在不知道结构体的定义时,表现并不是很好(但是只要知道了结构体的定义,效果就不一样了),这样可以迷惑破解者。在这个CM里面,要得到链表的结果,必须一次次地跟踪每个链表成员的前节点和后节点(也可以写脚本输出),增加了还原时的繁琐程度。
回到正题,还原上面的推箱子游戏的步骤为{6,2,8,2,8,4,4},再按照run函数解密的方法加密回去,得到字符串ifmholm,为了降低难度选择了加密成明文字符串。
再用上面的码表base64编码后,得到结果W6bjWfJcrU==,去掉等号就是flag了。因为main函数的isalnum已经判断了输入字符串不能有符号,而且base64字符串的等号就是填充对齐的作用,去掉也不会有影响。
至于主楼说到的多解,应该也很容易理解,因为CM没有对输入长度做很强的限制,所以可以在输入里面多加几个步骤,只要布局最后是上面那样子就行。
综上所述,我觉得这个CM的难度其实算低的,够不到中等难度的门槛。用到的算法都是常见的算法,只是做了一点改动,只要被猜出来是什么算法,CM就不攻自破了。虽然CM的链表写的是双向链表,但是基本上只使用后节点,前节点在这个小游戏几乎用不上。推箱子游戏用链表的方式重写也只是想避免被猜出来。不过玩这个CM的人太少了,并不能知道实际效果怎么样。
写完发现这字数快比得上之前发的帖子的字数了。 我别的工具都不会用,只会OD!
因此,看到这个程序,那就OD打开程序,停在这
00402510 .56 push esi ;kernel32.Sleep
00402511 .53 push ebx
00402512 .83EC 14 sub esp,0x14
00402515 .833D 18404000>cmp dword ptr ds:,0x2
0040251C .8B4424 24 mov eax,dword ptr ss:
00402520 .74 0A je short crackme.0040252C
00402522 .C705 18404000>mov dword ptr ds:,0x2
0040252C >83F8 02 cmp eax,0x2
0040252F .74 12 je short crackme.00402543
00402531 .83F8 01 cmp eax,0x1
00402534 .74 3A je short crackme.00402570
00402536 >83C4 14 add esp,0x14
00402539 .B8 01000000 mov eax,0x1
0040253E .5B pop ebx ;crackme.00401214
0040253F .5E pop esi ;crackme.00401214
00402540 .C2 0C00 retn 0xC
我们F8单步到向下走,看到了
00401386 .E8 F20D0000 call crackme.0040217D
0040138B .8B0D 08604000 mov ecx,dword ptr ds:
00401391 .A3 0C604000 mov dword ptr ds:,eax
00401396 .85C9 test ecx,ecx ;msvcrt.77C04E29
00401398 .0F84 CE000000 je crackme.0040146C
0040139E .8B15 04604000 mov edx,dword ptr ds:
004013A4 .85D2 test edx,edx
004013A6 .75 0A jnz short crackme.004013B2
004013A8 .E8 0F1F0000 call <jmp.&msvcrt._cexit> ; [msvcrt._cexit
004013AD .A1 0C604000 mov eax,dword ptr ds:
004013B2 >8D65 F0 lea esp,dword ptr ss:
004013B5 .59 pop ecx ;msvcrt.77C04E29
004013B6 .5B pop ebx
004013B7 .5E pop esi
004013B8 .5F pop edi
004013B9 .5D pop ebp
004013BA .8D61 FC lea esp,dword ptr ds:
004013BD .C3 retn
下面是一个exit,百度一下,就是退出程序的意思!
00401386这里如果F8,程序就跑起来了,我们F7跟进去,继续F8单步走
00402201|.E8 41FFFFFF call crackme.00402147
00402206|.8D8424 190100>lea eax,dword ptr ss:
0040220D|.890424 mov dword ptr ss:,eax
00402201在这里为我们回车进去能看到一个scanf函数,获取输入的数据
这里我们F8,程序跑飞,我们随便输入一个字符串:jiugongzi
程序此时还没跑飞,说明我们的思路是对的,我们继续F8向下走,来到了
00402298|.E8 7BFBFFFF call crackme.00401E18
0040229D|>B8 00000000 mov eax,0x0
004022A2|.8D65 F8 lea esp,
004022A5|.5B pop ebx
004022A6|.5F pop edi
004022A7|.5D pop ebp
004022A8\.C3 retn
00402298这里如果我们F8,就会发现程序窗口已经出现错误信息(wrong flag!)了
因此,这个call我们要F7进去,看看它到底干了什么!
接着我们继续F8向下走,来到了
00401E5C|.E8 23FEFFFF call crackme.00401C84
00401E61|.8945 F4 mov ,eax
00401E64|>8B45 F4 mov eax, ; ||
00401E67|.890424 mov dword ptr ss:,eax ; ||
00401E6A|.E8 F5130000 call <jmp.&msvcrt.puts> ; |\puts
00401E6F|.8B45 F4 mov eax, ; |
00401E72|.890424 mov dword ptr ss:,eax ; |
00401E75|.E8 0A140000 call <jmp.&msvcrt.free> ; \free
00401E7A|.90 nop
00401E7B|.C9 leave
00401E7C\.C3 retn
我们发现下面那个retn就返回了,而返回后程序就直接输出错误信息了,puts这个函数是写入
字符串,free是释放内存。那么这个puts写入的内容肯定就是上面那个call里面干的!
所以00401E5C这个call我们F7进去
00401C84/$55 push ebp
00401C85|.89E5 mov ebp,esp
00401C87|.53 push ebx
00401C88|.83EC 34 sub esp,0x34
00401C8B|.C745 DB 00000>mov dword ptr ss:,0x0 ; ||
00401C92|.C745 DF 00000>mov dword ptr ss:,0x0 ; ||
00401C99|.C645 E3 00 mov byte ptr ss:,0x0 ; ||
00401C9D|.8B45 0C mov eax, ; ||
00401CA0|.890424 mov dword ptr ss:,eax ; ||
00401CA3|.E8 CC150000 call <jmp.&msvcrt.malloc> ; |\malloc
00401CA8|.8945 E4 mov ,eax ; |
00401CAB|.8B45 0C mov eax, ; |
00401CAE|.894424 08 mov dword ptr ss:,eax ; |
00401CB2|.8B45 08 mov eax, ; |crackme.00405000
00401CB5|.894424 04 mov dword ptr ss:,eax ; |
00401CB9|.8B45 E4 mov eax, ; |
00401CBC|.890424 mov dword ptr ss:,eax ; |
00401CBF|.E8 A8150000 call <jmp.&msvcrt.memcpy> ; \memcpy
00401CC4|.C745 F4 00000>mov ,0x0
00401CCB|.EB 27 jmp short crackme.00401CF4
00401CCD|>A1 E0634000 /mov eax,dword ptr ds: ;88?
00401CD2|.8B55 F4 |mov edx,
00401CD5|.895424 04 |mov dword ptr ss:,edx
00401CD9|.890424 |mov dword ptr ss:,eax
00401CDC|.E8 7DF8FFFF |call crackme.0040155E
00401CE1|.8B00 |mov eax,dword ptr ds:
00401CE3|.8D50 61 |lea edx,dword ptr ds:
00401CE6|.8D4D DB |lea ecx,dword ptr ss:
00401CE9|.8B45 F4 |mov eax,
00401CEC|.01C8 |add eax,ecx
00401CEE|.8810 |mov byte ptr ds:,dl
00401CF0|.8345 F4 01 |add ,0x1
00401CF4|>837D F4 08 cmp ,0x8
00401CF8|.^ 7E D3 \jle short crackme.00401CCD
00401CFA|.837D 10 00 cmp ,0x0
00401CFE|.74 3B je short crackme.00401D3B
00401D00|.C745 F0 00000>mov ,0x0
00401D07|.EB 28 jmp short crackme.00401D31
00401D09|>8B55 F0 /mov edx,
00401D0C|.8B45 E4 |mov eax,
00401D0F|.01D0 |add eax,edx
00401D11|.0FB618 |movzx ebx,byte ptr ds:
00401D14|.8D55 DB |lea edx,dword ptr ss:
00401D17|.8B45 F0 |mov eax,
00401D1A|.01D0 |add eax,edx
00401D1C|.0FB608 |movzx ecx,byte ptr ds:
00401D1F|.8B55 F0 |mov edx,
00401D22|.8B45 E4 |mov eax,
00401D25|.01D0 |add eax,edx
00401D27|.31CB |xor ebx,ecx
00401D29|.89DA |mov edx,ebx
00401D2B|.8810 |mov byte ptr ds:,dl
00401D2D|.8345 F0 01 |add ,0x1
00401D31|>8B45 F0 mov eax,
00401D34|.3B45 0C |cmp eax,
00401D37|.^ 7C D0 \jl short crackme.00401D09
00401D39|.EB 4C jmp short crackme.00401D87
00401D3B|>C745 EC 00000>mov ,0x0
00401D42|.EB 3B jmp short crackme.00401D7F
00401D44|>C745 E8 00000>/mov ,0x0
00401D4B|.EB 28 |jmp short crackme.00401D75
00401D4D|>8B55 EC |/mov edx,
00401D50|.8B45 E4 ||mov eax,
00401D53|.01D0 ||add eax,edx
00401D55|.0FB618 ||movzx ebx,byte ptr ds:
00401D58|.8D55 DB ||lea edx,dword ptr ss:
00401D5B|.8B45 E8 ||mov eax,
00401D5E|.01D0 ||add eax,edx
00401D60|.0FB608 ||movzx ecx,byte ptr ds:
00401D63|.8B55 EC ||mov edx,
00401D66|.8B45 E4 ||mov eax,
00401D69|.01D0 ||add eax,edx
00401D6B|.31CB ||xor ebx,ecx
00401D6D|.89DA ||mov edx,ebx
00401D6F|.8810 ||mov byte ptr ds:,dl
00401D71|.8345 E8 01 ||add ,0x1
00401D75|>837D E8 08 | cmp ,0x8
00401D79|.^ 7E D2 |\jle short crackme.00401D4D
00401D7B|.8345 EC 01 |add ,0x1
00401D7F|>8B45 EC mov eax,
00401D82|.3B45 0C |cmp eax,
00401D85|.^ 7C BD \jl short crackme.00401D44
00401D87|>8B45 E4 mov eax,
00401D8A|.83C4 34 add esp,0x34
00401D8D|.5B pop ebx ;003F3838
00401D8E|.5D pop ebp ;003F3838
00401D8F\.C3 retn
这里面到底是干了啥能让程序输出错误信息(wrong flag!)
仔细观察堆栈窗口却只发现了
堆栈地址=0022FCA3, (ASCII "bfchediag 9?")
edx=00000000
猜测bfchediag这是正确的字符串?也不对啊!
实在是想不出来在哪做的比较!
只会用OD,强行装X分析一波,望各位看客不要见笑。(虽然啥也没分析出来) 无奈不懂算法
int sub_40217D()
{
unsigned int v0; // eax
int v1; // eax
int v3; //
__int16 v4; //
int v5; //
int Str; //
_BYTE v7; //
int v8; //
int v9; //
int v10; //
sub_402340();
Str = 0;
v8 = 0;
memset(
(void *)((unsigned int)v7 & 0xFFFFFFFC),
0,
4 * (((unsigned int)((char *)&Str - ((unsigned int)v7 & 0xFFFFFFFC) + 255) & 0xFFFFFFFC) >> 2));
v3 = 0;
v5 = 0;
memset(
(void *)((unsigned int)&v4 & 0xFFFFFFFC),
0,
4 * (((unsigned int)((char *)&v3 - ((unsigned int)&v4 & 0xFFFFFFFC) + 255) & 0xFFFFFFFC) >> 2));
v10 = 0;
sub_401B66();
sub_402147(&Str);
if ( sub_402097((char *)&Str) )
{
v0 = strlen((const char *)&Str);
v10 = sub_401F8D((int)&Str, v0, &v3);
v9 = sub_401E7D(&v3, v10);
if ( v9 )
{
v1 = sub_401D90();
sub_401E18(v1);//好像是显示成功或失败
}
else
{
sub_401E18(0);//这里是失败
}
}
else
{
sub_401E18(0);//这里也是失败
}
return 0;
} weikun444 发表于 2019-8-22 21:15
无奈不懂算法
只F5这里是不够的,深入分析才能找到答案,好好加油吧。 玖公子 发表于 2019-8-23 19:57
我别的工具都不会用,只会OD!
因此,看到这个程序,那就OD打开程序,停在这
0040 ...
你进去00401E5C算是对了一点,在这里flag正确就会输出“awesome!”,强行爆破的话会因为flag不正确输出乱码,所以真正的比较不是在这里,比较处会对结果做判断,当然是一个写得很别扭的判断,至于我比较的是什么,请加油。 不过算法类的CM玩的人是真的少,有没有大佬来玩一下{:1_909:} base64我也没看出来,更找不到main函数{:1_937:} wgz001 发表于 2019-8-29 12:50
base64我也没看出来,更找不到main函数
main函数的话只要找到scanf,回溯一下堆栈就行。base64可以看那个函数输出结果的长度,能看出标准的“4转3”(base64的4个字符转为明文的3个字符),只要有这种特征的基本可以定为base64,剩下的就是找到码表了。 建议感兴趣的同学玩一下,应该可以学到不少东西:lol
页:
[1]