梦游枪手 发表于 2019-8-22 18:10

无壳无花无反调无VM,只有简单算法的CM

如题,也许应该叫ReverseMe,什么都没有只有几个简单算法,也是很常见的算法,就是用了比较别扭的方法实现了。只要知道是什么算法应该就秒了。不知道能活多久。由于没怎么限制输入字符串长度,应该有多解。
成功图

失败图

爆破应该是没有用的。

梦游枪手 发表于 2019-8-28 11:37

本帖最后由 梦游枪手 于 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的人太少了,并不能知道实际效果怎么样。
写完发现这字数快比得上之前发的帖子的字数了。

玖公子 发表于 2019-8-23 19:57

我别的工具都不会用,只会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分析一波,望各位看客不要见笑。(虽然啥也没分析出来)

weikun444 发表于 2019-8-22 21:15

无奈不懂算法


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;
}

梦游枪手 发表于 2019-8-22 22:12

weikun444 发表于 2019-8-22 21:15
无奈不懂算法




只F5这里是不够的,深入分析才能找到答案,好好加油吧。

梦游枪手 发表于 2019-8-24 11:04

玖公子 发表于 2019-8-23 19:57
我别的工具都不会用,只会OD!
因此,看到这个程序,那就OD打开程序,停在这
0040 ...

你进去00401E5C算是对了一点,在这里flag正确就会输出“awesome!”,强行爆破的话会因为flag不正确输出乱码,所以真正的比较不是在这里,比较处会对结果做判断,当然是一个写得很别扭的判断,至于我比较的是什么,请加油。

梦游枪手 发表于 2019-8-24 11:12

不过算法类的CM玩的人是真的少,有没有大佬来玩一下{:1_909:}

wgz001 发表于 2019-8-29 12:50

base64我也没看出来,更找不到main函数{:1_937:}

梦游枪手 发表于 2019-8-29 13:38

wgz001 发表于 2019-8-29 12:50
base64我也没看出来,更找不到main函数

main函数的话只要找到scanf,回溯一下堆栈就行。base64可以看那个函数输出结果的长度,能看出标准的“4转3”(base64的4个字符转为明文的3个字符),只要有这种特征的基本可以定为base64,剩下的就是找到码表了。

wgz001 发表于 2019-8-31 14:54

建议感兴趣的同学玩一下,应该可以学到不少东西:lol
页: [1]
查看完整版本: 无壳无花无反调无VM,只有简单算法的CM