首页
论坛
课程
招聘
[原创]第三题wp
2017-10-30 01:59 1368

[原创]第三题wp

2017-10-30 01:59
1368
程序OD载入后,点击验证,程序直接退出,看来有AntiDebug。但是程序不防IDA,IDA载入后发现无壳无花,F5能直接看程序源码。
根据获取输入字符API:GetDlgItemTextA来到消息处理过程:
int __stdcall WindowMsgProc(HWND hDlg, int a2, int arg8, int a4)
{
  memset(&v14, 0xCCu, 0x1A40u);
  v31 = (unsigned int)&savedregs ^ dword_49B344;
  v30 = 0;
  v28 = 0;
  myMemSet((int)&v29, 0, 1023);
  v14 = a2;
  if ( a2 == 0x10 )
    ExitProcess(0);
  if ( v14 == 0x110 )
  {
    v30 = sub_42D4F1();
    if ( v30 == 1 )
      ExitProcess(0);
    v30 = 0;
    v30 = sub_42E428();
    if ( v30 == 1 )
      ExitProcess(0);
    v30 = 0;
    v30 = sub_42D825();
    if ( v30 == 1 )
      ExitProcess(0);
    j_setWindowCenter(hDlg, 1);
  }
  else if ( v14 == 0x111 )
  {
    v14 = (unsigned __int16)arg8;
    if ( (unsigned __int16)arg8 == 1002 )
    {
      String = 0;
      myMemSet((int)&v26, 0, 1023);
      a3 = 0;
      myMemSet((int)&v24, 0, 1023);
      v4 = GetDlgItemTextA(hDlg, 1001, &String, 1025);
      v27 = myCheckException(&v11 == &v11, v4);
      v21 = 0;
      myMemSet((int)&v22, 0, 1023);
      j_myBase64Decode((BYTE *)&String, 0x400u, &a3);
      v19 = 0;
      myMemSet((int)&v20, 0, 1023);
      j_myBase64Decode(&a3, 0x400u, &v21);
      j_myCodeToString((int)&v21, (int)&v19, 1024);
      v18 = 3;
      j_mySM3calc((int)&v21, 3, (int)v17);
      for ( i = 0; i < 32; ++i )
        sprintf(&v16[2 * i], "%02x", (unsigned __int8)v17[i]);
      v5 = myStrLen((int)v16);
      v6 = &String + myStrLen((int)&String);
      v7 = myStrLen((int)v16);
      if ( !myStrCmp((int)v16, (int)&v6[-v7], v5) )
      {
        sub_42D0B4(v11, v12, v13);
        if ( (unsigned __int8)j_myRunPuzzle((int)&a1, (int)&v19) == 1 )
        {
          v8 = MessageBoxA(0, "ok", "CrackMe", 0);
          myCheckException(&v11 == &v11, v8);
        }
      }
    }
  }
  sub_42D65E(&savedregs, &dword_435250);
  v9 = sub_42D1E5((unsigned int)&savedregs ^ v31);
  return myCheckException(1, v9);
}
可以看到程序处理了3个消息:0x10是退出,0x110是启动并居中显示窗口,0x111就是按钮点击处理过程了。花了四五个小时的时间把大部分函数的功能标出来了,流程还是比较清晰的:输入字符key经过2次BASE64解码,得到一串字符代码key1,再用myCodeToString()把代码转换为字符key2,然后验证key1的hash值是否与key最后0x40位相同;最后用key2去跑一个迷宫,返回1时弹出"ok"的成功提示。
先总结一下分析函数功能的方法:其实主要是靠经验和运气,比如BASE64算法,刚开始没往这方面想,完全不理解这二次字符处理是干什么的,后来看到解码过程中验证字符为0x3D即"="时停止解码,而且解码后的字符长度会缩短一点,突然想到这不就是BASE64解码的特征吗,直接用一串BASE64码验证一下,果然是BASE64算法。估计有经验的人一眼就看出这算法了。另一个SM3算法比较好找,初始化数据时用了一串常数,百度一下其中一个常数0x7380166F就直接出SM3算法了。还有一个字符替换函数,这个比较复杂,但是把其中3个子函数功能分析出来了就简单了,那3个子函数分别是把字符替换为数字,把字符替换为小写字母,把字符替换为符号。替换表如下。
num:0-9
-----
.----
..---
...--
....-
.....
-....
--...
---..
----.
sign:
.-.-.-*.
---...*:
--..--*,
-.-.-.*;
..--..*?
-...-**=
.----.*'
-..-.**/
-.-.--*!
-....-*-
..--.-*_
.-..-.*"
-.--.**(
-.--.-*)
...-..-$
.-...**&
.--.-.*@
str:a-z
.-**
-...
-.-.
-..*
.***
..-.
--.*
....
..**
.---
-.-*
.-..
--**
-.**
---*
.--.
--.-
.-.*
...*
-***
..-*
...-
.--*
-..-
-.--
--..
再总结一下程序的AntiDebug:估计作者收集了一堆反调试的函数,在程序的验证过程中随机插入,用OD调试时非常容易中招。不过作者留下了后门,几乎所有反调试函数最后都会调用同一个函数退出程序,可以根据这个退出函数的调用参考来直接跳过反调试代码。
再来仔细分析一下走迷宫的函数:
int __cdecl myRunPuzzle(int *a1, char *key)
{
  col = 0;
  row = 0;
  upstep = -1;
  downstep = 1;
  leftstep = -1;
  rightstep = 1;
  while ( 1 )
  {
    v2 = (int *)key;
    if ( *key == 0x20 )
      break;
    if ( col != 8 || row != 3 )
    {
      if ( *key == 0x7A ) // 'z':down
      {
        v2 = (int *)(downstep + row);
        if ( downstep + row >= 10 )
        {
          LOBYTE(v2) = 0;
          return myCheckException(1, (int)v2);
        }
        if ( !*(&a1[10 * (downstep + row)] + col) )
          row += downstep;
      }
      if ( *key == 0x6C && rightstep + col < 10 )// 'l':right
      {
        v2 = &a1[10 * row];
        if ( v2[rightstep + col] )
        {
          LOBYTE(v2) = 0;
          return myCheckException(1, (int)v2);
        }
        *(&a1[10 * row] + col) = 4;
        col += rightstep;
      }
      if ( *key == 0x71 && upstep + row >= 0 ) // 'q':up
      {
        v2 = &a1[10 * (upstep + row)];
        if ( v2[col] )
        {
          LOBYTE(v2) = 0;
          return myCheckException(1, (int)v2);
        }
        *(&a1[10 * row] + col) = 4;
        row += upstep;
      }
      if ( *key == 0x70 && leftstep + col >= 0 )// 'p':left
      {
        v2 = &a1[10 * row];
        if ( v2[leftstep + col] )
        {
          LOBYTE(v2) = 0;
          return myCheckException(1, (int)v2);
        }
        col += leftstep;
      }
    }
    ++key;
  }
  LOBYTE(v2) = 1;
}
可以看出使用了4个字符“zlqp”分别代表下右上左,用二个变量分别记录row、col行列坐标,迷宫限制在10*10大小以内,根据用户输入的字符串调整坐标位置,最后遇到空格0x20时结束循环。同时为了防止来回走,在向右和向上走时会把走过的路标为4。
再看看迷宫数据,由于迷宫大小限制为10*10,把0049B000处100个int数值复制出来,按10*10格式化一下:
0111111110
0011111000
1000001011
1111101001
1000101001
1010001011
1011111001
1000011100
1111000010
1111111000
最开始位于左上坐标(0,0)处,沿迷宫中为0的位置可以走到右上角(0,9)位置。
但是作者在迷宫检测处有重大设计漏洞,可以看出作者的意图是最终要走到坐标(3,8)处,但是走到终点时并没有结束游戏,在结束游戏时也没有验证是否到达终点,必须用0x20来结束解迷,这样在碰到墙之前都能用空格跳到胜利,在第一步直接用空格就直接通关了!还有一个小漏洞就是,通往终点(3,8)的路有二条,也会造成多解。
最后,使用作弊码0x20,也就是空格来作例子,总结一下注册码的生成过程:
在myCodeToString()函数里,'/'会被替换为空格,也就是说二次BASE64解码结果应为'/';
再对'/'进行二次BASE64加密,得到"THc9PQ==";
然后把'/'的SM3值2f725aaf8d9fa538554e9f3589ddc785364d52ab1a6760c12caa2ec01ae4ba9e直接附到后面,不用担心会影响BASE64解码,因为二次BASE64解码时会把乱码过滤掉;
最后,由于注册码限制必须是字母数字,可以直接把中间的对齐码“==”删除,得到THc9PQ2f725aaf8d9fa538554e9f3589ddc785364d52ab1a6760c12caa2ec01ae4ba9e。
从这里,又可以看出一个的设计漏洞,二次BASE64解码可以过滤很多字符,我们可以在中间插入任意被过滤掉的字符,可以造成无数解。经测试,最短注册码为THc92f725aaf8d9fa538554e9f3589ddc785364d52ab1a6760c12caa2ec01ae4ba9e
最后总结一下,按作者意图走迷宫的key应为zlzllllzzzppqppzzzlllzlllzllqqpqpqqql。代码替换比较麻烦,不过程序里预留了key生成代码的函数00436160,可以在调用代码替换函数时直接跳到436160处生成代码。生成的代码如下:
--.. .-.. --.. .-.. .-.. .-.. .-.. --.. --.. --.. .--. .--. --.- .--. .--. --.. --.. --.. .-.. .-.. .-.. --.. .-.. .-.. .-.. --.. .-.. .-.. --.- --.- .--. --.- .--. --.- --.- --.- .-..
PS:后来测试时,发现程序在走迷宫时如果没有正确结束解迷,迷宫会一直走下去造成溢出,再结合base64解码结果会保存到解迷key的下方,如果base64解码第1个字符为空格,当溢出到这个位置时同样可以直接通关!又得到一组比较有意思的注册码:SUE90065d1e2a1f4b71b08971092d29e9e3a1791ba2570c543e04aa76847815ea380


[培训]12月3日2020京麒网络安全大会《物联网安全攻防实战》训练营,正在火热报名中!地点:北京 · 新云南皇冠假日酒店

收藏
点赞0
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回