首页
社区
课程
招聘
[原创] 第三题:街机少年解题步骤--连蒙带猜爆破
2019-12-6 13:56 3743

[原创] 第三题:街机少年解题步骤--连蒙带猜爆破

2019-12-6 13:56
3743
静态分析
工具:ida pro7.0
使用ida打开文件,可以看到函数表。其中有个名字较start的,本来想顺着这个start的调用一直跟下去找的处理逻辑,但跟了几层之后觉得有点烦,就决定先找字符串。

往上拖到开头,本来是想看下什么版本的分析之类的,结果第一个函数里就看到命令行输出的字符串,基本可以确认就是程序逻辑,于是分析调用和处理流程。

用于控制台输入输出的函数从上下文可以直接分析出来,比如这个sub_40471D显然就是输出,所以不用跟进去。
得到大致流程:
逐个字符的获取输入的用户名,序列号,以及长度,分别存入全局变量,从这里可以看出数据长度限制。

然后输入的序列号经过一个处理函数,生成真正的密码和长度。经过分析,这个处理函数将输入的四个58-122之间的字节为一组,通过位运算映射为三个字节。。。这就是一个base64算法的变形,58-122刚好也就是64个,'z'相当于标准base64的'=',这可能是我唯一熟悉的加密算法来。至少这一步可以确定是可逆的,有点感动。

接下来通过XREF找到调用getinput的地方,发现接下来过程很直接,就是将第一步得到的用户名和密码通过一个函数处理判断是否正确,因此关键的验证逻辑就是checkpass这个函数了。


解题的过程中顺便看了看题主的发帖记录,找到这样一篇,也是类似的题目。
题目设计非常精巧,反正我是看不懂。并且总是强调多解就女装什么的。。。不知道是自信还是其实想女装(小声)
虽然题目肯定不一样,但是还是能得到一些启发。例如里面的描述方法对理解本题有些帮助,可以把base64得到的密码当成指令来看待。
chekpass代码特别长,毕竟是核心算法,经过分析密码和用户名的调用(这里是只读的)以及如何让函数返回正确,可以将验证逻辑大致可以解释为以下几步:
  1. 将用户名的第一位经过一系列运算得到一个初始值。
  2. 将用户名其余部分调用一个函数经过一系列运算,修改了一个长度15的数组(实际是char[256]但是只有前十五个字符在使用),暂且把这个表当成hash(就是取个名),这个表在后续步骤中使用,而用户名再也没有用过。
  3. 进入循环,每次取出密码中的一项经过一系列复杂操作,分别取高位和低位经过几个循环迭代,得到结果,过程中用到并改变的1中生成的初始值(当成译码看待),最后用这个密码字符得到的指令,修改了hash表中的指定一项(这里里我称为主动修改)。          (这一步里有一个校验指令的过程,可能导致循环退出,具体条件太复杂我也看不懂。只能知道不能走到这个条件)因为代码译码的过程用的变量太多,难以一一分析,所以我暂时没有先研究具体过程,只看这整个步骤对结果的影响。
  4. 取出了密码中的该项和下一项,经过运算,修改了3中译码操作需要用到的一些值,再加上3中也修改了部分变量,也就是改变了下一次译码的规则,这就是说我们不能从数据到指令建立一个一一映射的关系,因此很难倒推回去。
  5. 从hash表中取出所有数据,经过一系列操作作为数组下标,几次迭代,最后从一个大的表中取出一个数据,根据这个数据来进入一个判断,也就是步骤6。
  6. case1:从另一个表中取出数据修改hash,并且是把hash中的每一项都改变,因此既有可能产生0,也有可能把之前已经消为0的值变为非0,造成破坏性后果,具体会发生什么很难分析;case2,有一个指针指向hash中的数据,每次进入这个分支会改变hash中的一个数据,并且重要的是,根据判断条件,只会改变>=1的一个数据(写的时候才突然意识到这一点),而且这里指针指向采用+4%15的方法,可以遍历整个hash表。这一步我称之为被动修改。
3到6为一个大循环,第3步和循环结束时都有一个判断:将hash中的值全部相或看是否为0
最终成功的条件为,在取出密码中最后一项完成步骤3之后,hash表中的值全为0;也就是说,根据输入密码的长度N,有N次执行主动修改机会,每次都要通过3中的校验,并且经过N-1次被动修改,在第N次主动修改之后,hash全为0。同时,被动修改之后不能全为0,因此最后一次修改必须用主动修改来完成。
。。。。。。
总结一下就是,太复杂了看不懂,还是不看了。
1、2是初始化,都是由用户名决定,用户名固定情况下完全可以当作常量不用管,重要的就是后面几步。
麻烦的地方有几点,一是译码过程太复杂,二是被动修改难以控制,第5步中的表虽然是常量,但是长度为65536,足以令人自闭,另外还有几个小一点的表,第三点就是不知道操作顺序应该怎样,从哪个地方开始修改,而且步骤环环相扣,假如真的只有一个解,那很可能要到最后一步才知道正确与否。
直接分析太麻烦,而且具体算法是什么也看不出来,主要是要从表中取数据和多次迭代让人很难跟踪。因为完全没有逆向分析算法的思路,我只能想到爆破法让程序自己去跑。
序列号最多128位,密码长度位128*3/4=96。其中每个密码8位共256种可能,直接爆破就是个256的N次方。但是,虽然在4中用到了下一个密码的值,但这只会影响下一次循环的步骤3,可以当成下一次主动修改的过程来判断,而5、6的判断逻辑是由hash表中全部数据来决定的,也与下次输入无关。因此,每个密码译码成指令和进行修改操作的过程(包括主动和被动两处修改),都只依赖于之前的密码对各种变量的修改,而与下一个密码无关。
有点像是个因果系统那样,还有LTI什么的,想不清楚。
如果能够单独确定每一位密码的操作是否正确,这样就能先爆破出第一个密码,再在第一个密码正确的前提下,利用第一个密码的环境,爆出下一个密码,这样只有256*N,完全可以接受。
这样过程的复杂就不用管了,反正累的是电脑不是我。
现在的问题是,怎么单独确定第一个密码是正确的,也就是没有后续密码来完成整个流程的情况下确定前面密码的值。如果没有被动修改这种难以预测的操作,但就"因果“这一点,就肯定时可以操作的,只要主动按顺序修改hash表就行,而且还可以换不同的顺序,简直是为所欲为。现在有了被动修改,没法确定现在把某个值改为0是否真的就对得到最终结果有利。但是这种算法可能有什么特殊的规律,按指定方法操作的化,有几种可能成功的情况,比如主动修改要依次修改固定项,比如能控制步骤5、6等。(而且提供的序列号base64解码长度显然不是15,说明不是每条指令都能新改出一个0,显然直接利用主动修改来改成0是不现实的。)
静态分析已经走到了尽头,没法看出算法的特殊点,不过既然提供了一个正确的密码和序列号,我觉得可以试试动态调试,看看有没有什么规律。要是没有或者看不出来的话,那就没办法了。

动态调试
工具:还是ida pro7.0,别的我也不会
直接调试,程序崩溃,附加调试,程序崩溃。
最后还把静态分析时 没保存好的idb整没了。。。。。 到这里我就有点慌,因为有反调试,很可能就会动态修改代码,这样静态分析得到的逻辑就是错的,而且题主的某篇文章里就有“变形”。

引用

可是我确实就不会反调试,也不知道断点要下在哪,也不想试,所以只能先假设没有什么变形,试试运气(实际上我觉得这个算法这么复杂,是假的可能性不大,而且各个数据表从ida的xref里也没找到别的读写,但是也有可能在初始化时产生了新的函数修改了部分表)。
于是动态调试部分就可以跳过了!!!!!

爆破
工具:反汇编ida,编译器Visual C++ 6.0,二进制阅读工具HxD,纯文本处理工具notepad++
反汇编的代码较为清晰,错误应该不会太多,所以这里决定把与验证流程有关的代码复制成c++项目自己跑。
为了尽量少动手,从百度上找了份ida中类型的宏定义,然后输入输出调整一下改成熟悉的cout和getchar,warning不管,error就修(曾经我也是一个括号都要斤斤计较的人,现在嘛,能跑就行了)。
其次是数据,有两个办法,一个是让程序直接根据偏移读文件,二是手动写成全局变量。因为c++的文件操作这些我都没怎么用过,选择手动法。从HxD里复制出来十进制是AB形式,改成0xAB,这里用正则匹配([0-9A-Z]{2})->0x\1,于是就生成了代码char table[]这样。这里变量的名字必须和ida中给内存中全局数组起的名字一样,这样就不用改代码了。
最后除了几个函数参数类型外没有别的修改就编译通过。把提供的用户名序列号一试,成功。说明程序没写错,而且动态修改程序的可能性降低了很多。
于是再中间加上各种输出来看程序运行情况,包括每次循环输入的密码,对hash表的改变,选择的分支等。
结果很清晰,每次都是进入case2,仔细一想,这样0的数目至少不会减少。
KCTF2019
Please input your username: ( A to Z, a to z, 0 to 9 )
f3a25f8b29bb9cbc
Please input your KEY: ( : to z )
JXg@vwrpfoLbRcxvdYrvv[kyRSso
hashis:1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 5, 6, 7, 14, 15,

hashis:5, 1, 6, 3, 3, 7, 7, 10, 9, 10, 7, 6, 6, 10, 11,
ind:0, pass:65
change:id14+=-5
hashis:5, 1, 6, 3, 3, 7, 7, 10, 9, 10, 7, 6, 6, 10, 6,
case2:
hashis:5, 1, 6, 3, 3, 1, 7, 10, 9, 10, 7, 6, 6, 10, 6,

ind:1, pass:-19
change:id14+=-6
hashis:5, 1, 6, 3, 3, 1, 7, 10, 9, 10, 7, 6, 6, 10, 0,
case2:
hashis:5, 1, 6, 3, 3, 1, 7, 10, 9, 7, 7, 6, 6, 10, 0,

ind:2, pass:-122
change:id13+=-3
hashis:5, 1, 6, 3, 3, 1, 7, 10, 9, 7, 7, 6, 6, 7, 0,
case2:
hashis:5, 1, 6, 3, 3, 1, 7, 10, 9, 7, 7, 6, 6, 4, 0,

ind:3, pass:-13
change:id12+=-1
hashis:5, 1, 6, 3, 3, 1, 7, 10, 9, 7, 7, 6, 5, 4, 0,
case2:
hashis:5, 1, 0, 3, 3, 1, 7, 10, 9, 7, 7, 6, 5, 4, 0,

ind:4, pass:-40
change:id13+=-2
hashis:5, 1, 0, 3, 3, 1, 7, 10, 9, 7, 7, 6, 5, 2, 0,
case2:
hashis:5, 1, 0, 3, 3, 1, 4, 10, 9, 7, 7, 6, 5, 2, 0,

ind:5, pass:-10
change:id13+=-1
hashis:5, 1, 0, 3, 3, 1, 4, 10, 9, 7, 7, 6, 5, 1, 0,
case2:
hashis:5, 1, 0, 3, 3, 1, 4, 10, 9, 7, 3, 6, 5, 1, 0,

ind:6, pass:-77
change:id12+=-4
hashis:5, 1, 0, 3, 3, 1, 4, 10, 9, 7, 3, 6, 1, 1, 0,
case2:
hashis:5, 1, 0, 1, 3, 1, 4, 10, 9, 7, 3, 6, 1, 1, 0,

ind:7, pass:82
change:id11+=-2
hashis:5, 1, 0, 1, 3, 1, 4, 10, 9, 7, 3, 4, 1, 1, 0,
case2:
hashis:5, 1, 0, 1, 3, 1, 4, 9, 9, 7, 3, 4, 1, 1, 0,

ind:8, pass:104
change:id10+=-3
hashis:5, 1, 0, 1, 3, 1, 4, 9, 9, 7, 0, 4, 1, 1, 0,
case2:
hashis:5, 1, 0, 1, 3, 1, 4, 9, 9, 7, 0, 1, 1, 1, 0,

ind:9, pass:98
change:id9+=-5
hashis:5, 1, 0, 1, 3, 1, 4, 9, 9, 2, 0, 1, 1, 1, 0,
case2:
hashis:3, 1, 0, 1, 3, 1, 4, 9, 9, 2, 0, 1, 1, 1, 0,

ind:10, pass:-98
change:id6+=-2
hashis:3, 1, 0, 1, 3, 1, 2, 9, 9, 2, 0, 1, 1, 1, 0,
case2:
hashis:3, 1, 0, 1, 0, 1, 2, 9, 9, 2, 0, 1, 1, 1, 0,

ind:11, pass:-4
change:id9+=-1
hashis:3, 1, 0, 1, 0, 1, 2, 9, 9, 1, 0, 1, 1, 1, 0,
case2:
hashis:3, 1, 0, 1, 0, 1, 2, 9, 6, 1, 0, 1, 1, 1, 0,

ind:12, pass:-87
change:id7+=-3
hashis:3, 1, 0, 1, 0, 1, 2, 6, 6, 1, 0, 1, 1, 1, 0,
case2:
hashis:3, 1, 0, 1, 0, 1, 2, 6, 6, 1, 0, 1, 0, 1, 0,

ind:13, pass:-8
change:id13+=-1
hashis:3, 1, 0, 1, 0, 1, 2, 6, 6, 1, 0, 1, 0, 0, 0,
case2:
hashis:3, 0, 0, 1, 0, 1, 2, 6, 6, 1, 0, 1, 0, 0, 0,

ind:14, pass:-4
change:id11+=-1
hashis:3, 0, 0, 1, 0, 1, 2, 6, 6, 1, 0, 0, 0, 0, 0,
case2:
hashis:3, 0, 0, 1, 0, 0, 2, 6, 6, 1, 0, 0, 0, 0, 0,

ind:15, pass:-14
change:id9+=-1
hashis:3, 0, 0, 1, 0, 0, 2, 6, 6, 0, 0, 0, 0, 0, 0,
case2:
hashis:3, 0, 0, 1, 0, 0, 0, 6, 6, 0, 0, 0, 0, 0, 0,

ind:16, pass:17
change:id8+=-2
hashis:3, 0, 0, 1, 0, 0, 0, 6, 4, 0, 0, 0, 0, 0, 0,
case2:
hashis:3, 0, 0, 0, 0, 0, 0, 6, 4, 0, 0, 0, 0, 0, 0,

ind:17, pass:-1
change:id0+=-1
hashis:2, 0, 0, 0, 0, 0, 0, 6, 4, 0, 0, 0, 0, 0, 0,
case2:
hashis:2, 0, 0, 0, 0, 0, 0, 3, 4, 0, 0, 0, 0, 0, 0,

ind:18, pass:97
change:id8+=-3
hashis:2, 0, 0, 0, 0, 0, 0, 3, 1, 0, 0, 0, 0, 0, 0,
case2:
hashis:0, 0, 0, 0, 0, 0, 0, 3, 1, 0, 0, 0, 0, 0, 0,

ind:19, pass:-103
change:id7+=-2
hashis:0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0,
case2:
hashis:0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0,

ind:20, pass:-11
change:id7+=-1
hashis:0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
You Win! You are very clever!
请按任意键继续. . .

于是假设正确的输入就是每次都进入case2,然后就可以根据是否进入的是case2来依次爆密码。不过还不能确定每个密码是不是只能有一个能进入case2,如果有每个密码有多个可以进入case2的话,仍然没法确定哪一个是正确的,复杂度还是N次方,有点麻烦。
先爆出第一位,发现和正确密码第一位一样,接着第二位也一样,说明这种方法可行。于是就能全部爆破出来。这里比较重要的就是每次都需要将被改写的变量恢复(只读的变量就不用担心),并且成功前只判断最后一位密码是否进入case2,没爆出移位,就在固定该位情况下爆破下一位。主程序大致如下:
int main()
{
  char tempcachetable[256]={0},temphashname[256]={0};
  strcpy(tempcachetable,cachetable);//保存初始表格
  strcpy(temphashname,hashname);
  char *constname="KCTF";
  len_name=4;
  strcpy(uname,constname);
  bool run=true;
  int k=0;
  while(run){
	  for(int i=0;i<256;i++){
		  strcpy(cachetable,tempcachetable);
		  strcpy(hashname,temphashname);
		  pass[k]=(char)i;
		  passlen=k+1;
		  if(checkpass((unsigned __int8 *)uname, len_name))
		  {run=false;cout<<"success!!!"<<endl;break;}//最后一次是主动修改完成,不需要进入case2,已经直接得到了答案
		  if(iscase2){
			  cout<<endl<<k<<"--------------"<<i<<endl<<endl;
			  iscase2=false;
			  break;
		  }
		  if(i==255){cout<<"failed"<<endl;return 0;}
	  }
	  k++;
	  if(k>=96){
	  cout<<"failed"<<endl;
	  break;
	  }
	  int jj;
    cout<<"pass:";
    for ( jj = 0; jj <passlen; ++jj )
      {
        cout<<(int)pass[jj]<<",  ";
      }
	cout<<"len is:"<<passlen<<endl;
  }
	char result[256]={0};//base64加密结果
	ab64e(pass,result,passlen);
	cout<<result<<endl;
      cout<<endl;
/*
  if ( getinput() )
  {
    if ( checkpass((unsigned __int8 *)uname, len_name) )
      cout<<"You Win! You are very clever!"<<endl;
    else
      cout<<"Game Over"<<endl;
    system("pause");
  }
  else
  {
    cout<<"Game Over"<<endl;
    system("pause");
  }
*/
  return 0;
完整程序见附件。
程序跑起来很快,把多余的输出关了基本就是秒解。
接下来就是从密码解出序列号,需要写一个base64编码的规则,本来以为这是最简单的,结果因为代码太渣,脑子太卡,甚至还写出了栈溢出。。。。主要就是各种左移右移掩码,原程序汇编代码里有一个sar算术右移,被ida翻译成signed类型,其实最高位都是0没什么影响,所以在编码的时候要全部用逻辑左/右移,也就是unsigned,另外比较麻烦的就是长度不能被三整除的情况,这样最后需要特殊处理,补上’z'。

然后把生成的序列号输入原程序,竟然失败了,当时就心里一凉。幸好把序列号输入自己的程序也失败了,虽然我的程序有问题,但至少还有机会。
经过检查,发现是从提供的用户名改为“KCTF”时忘了改长度,然后就得到正确结果。


这时候已经凌晨一点多了。我记得我本来都掏出手机想打游戏的,游戏加载的时候看了看代码,结果就没能停下来,最后发现游戏都已经被杀后台了。
提交的时候发现还没人提交,心情有点激动,差点睡不着。转念一想这题难不难不知道,但是我能做出了确实是凑巧。
其实最主要的原因是花的时间多吧,一下午加一晚上,十二个小时不知道有没有,真正的大佬估计是在忙别的没这么多时间,毕竟是工作日。
本人没什么经验,第一题就做了两小时,一半在MFC打转,第二题在py虚拟机里转了两天还去搞了patch文件然后子进程调试和脱壳之类的,最后终于醒悟过来拿到py代码查出来是rsa后也分解不了。

综上所述,做这道题,靠的就是运气好,蒙对了。不过我觉得这题挺有意思的,虽然看不懂原理但是感觉很精妙。

[培训]《安卓高级研修班(网课)》月薪三万计划,掌握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法

最后于 2019-12-6 19:28 被mb_ibocelll编辑 ,原因:
上传的附件:
收藏
点赞1
打赏
分享
最新回复 (6)
雪    币: 32410
活跃值: (18730)
能力值: (RANK:350 )
在线值:
发帖
回帖
粉丝
kanxue 8 2019-12-6 14:13
2
0
建议写详细些
雪    币: 3239
活跃值: (435)
能力值: ( LV6,RANK:151 )
在线值:
发帖
回帖
粉丝
mb_ibocelll 1 2019-12-6 18:56
3
0
我尽力了,再多已经写不出来了,最多贴点图
雪    币: 11704
活跃值: (966)
能力值: ( LV12,RANK:779 )
在线值:
发帖
回帖
粉丝
readyu 12 2019-12-8 12:26
4
0
大众队黑马车手,太强了
雪    币: 3239
活跃值: (435)
能力值: ( LV6,RANK:151 )
在线值:
发帖
回帖
粉丝
mb_ibocelll 1 2019-12-8 12:58
5
0
大佬别说了,我害怕[瑟瑟发抖]
雪    币: 3239
活跃值: (435)
能力值: ( LV6,RANK:151 )
在线值:
发帖
回帖
粉丝
mb_ibocelll 1 2019-12-8 12:59
6
0
我错了,这里不该用strcpy的,要是中间有个0就完了
雪    币: 200
活跃值: (87)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
DDragon 2019-12-9 11:20
7
0
牛皮哦
游客
登录 | 注册 方可回帖
返回