作者Editor

看雪.WiFi万能钥匙 CTF 2017第十一题 点评及解题思路

发布于:2017-06-28 18:25:19 阅读:365 回帖:0

看雪CTF 2017 比赛进行至第十一题

截止至今天中午12点,第十题破解人数为 8 人!

防守方 hsadkhk 依然位居首位~,第十一题作者冲进第三位。


此题过后,风间仁依然处于第一位,前十名排名基本保持不变。


比赛进入尾声,仅剩 4 题。

期待ing......

接下来我们来回顾一下第十一题

看看 看雪评委和出题者是怎么说的ヾ(๑╹◡╹)ノ"。


看雪评委 netwind 点评


该题运用多种反调试手段,并采用了修改过的DES算法来进行防护。首先为主程序加了一个壳,然后用自己实现的函数隐藏api调用;判断当前进程是否被调试,如果是则跳入错误流程;通过父进程调试子进程以及除0异常的方式来接管程序流程。绕过种种反调试,识别出经过变形后的DES算法后,就可以破解此题。


作者简介


TobeBetter,科锐学员,目前学完x86逆向,希望在看雪论坛这个技术氛围浓厚的平台,可以向各位前辈学习知识和经验。


看雪 CTF 2017 第十一题设计思路


参赛题目

Windows平台CrackMe

题目答案

75A29C09E180217C048420956C15DA309FF2B69170

详细的题目设计

1. 为程序写了一个简单的壳,对程序进行简单的异或加密(加了壳之后程序会报毒,水平有限暂时无法解决)。若对程序进行dump,会丢失藏在文件中但没有读入内存的DES解密函数,使序列号无法正常解密

2. 使用自己实现LoadLibrary将kernel32导入到内存中,并使用自己实现的GetProcAddress通过已加密的函数名获取函数地址,隐藏api调用

3. 程序入口处判断了是否处于调试状态,包括检查PEB的BeingDebugged、堆标志位和父进程名是否为explore.exe或cmd.exe

1)     如果不为调试状态,则以调试方式打开程序本身,创建子进程

2)     如果处于调试状态,则显示CrackMe对话框

4.     通过SetUnhandledExceptionFilter设置了异常筛选器

5. 当用户点击确定按钮时,会触发除0异常。如果本进程是通过父进程以调试模式打开,则该异常会被父进程捕获;否则,会被异常筛选器给捕获(正常情况不会来这里)

6. 程序本身没有正常调用验证序列号的流程。当父进程捕捉到除0异常之后(程序第一次点击确认按钮时),会对错误的验证序列号的函数进行hook,从而跳转到正确的流程。

7. 验证序列号流程

  1. 程序中提供前96字节的DES解密算法明文(记为fuc1)。验证前会将pe文件中已的DES解密算法密文(记为fuc2)读入内存中。

  2. 验证码的前10个字符组成4字节和1字节的两个秘钥,这两个秘钥对fuc2进行简单移位异或操作解密并与fuc1的前96字节比较,验证成功则继续。

  3. 验证码的后32字符组成16字节密文,通过程序中提供的秘钥调用fuc1对密文进行解密,验证解密出的明文与程序中的明文进行比较,字符串相同则为提示成功。

破解思路

1. 对程序进行脱壳dump。可以通过修改自己加载的kernel32的地址为系统kernel32的地址,使用OD调试时即可观察到程序调用api情况

2. 在WinMain中通过修改全局的调试状态标志位使得程序能够在调试状态下创建子进程(默认只能通过explore.exe和cmd.exe打开的进程才会创建子进程)

3. 在处理调试事件的函数中下断点,找到程序的hook点和hook的内容,直接对程序对应位置修改机器码。此时还原了程序正确的流程,点击确认按钮就可以正常验证序列号

4. 解密des函数的两个秘钥(共5个字节)可以通过穷举法获取,异或算法较为简单,很容易就能还原出来,通过解密密文和程序中的前96字节明文对比可以确认秘钥是否正确。需要注意的是,des函数需要在原文件中提取。这里获得的两个秘钥转为十六进制字符,得到序列号前10个字符。通过测试,对于i5-4210h CPU单线程需要跑20分钟

5. 识别出变形的des算法(在加密前和解密后需要对偶数位进行取反,即异或0x55),根据程序中提供的16位明文和秘钥进行加密,得到16位密文,对密文转为十六进制字符,得到序列号的后32个字符。此时组成的48位序列号即为最终答案

下面选取攻击者 loudy 的破解分析


使用工具:IDA6.8(反汇编分析) notepad(记录) vc6.0(解码) OD(调试)

步    骤:

一、代码自解密

IDA载入看入口点和导入表,没有发现任何有用的东西,考虑该程序已经加密处理。

二、识别真正流程

继续OD动态跟踪,在此处发现DialogBoxParamW,有过MFC编程经验的朋友应该不陌生,说明我们已经找到关键点了(其实不然)。

三、第一个关键算法

实际调试时可以在4016AF处将JE用NOP代替,将401A43处修改为jmp 402303,即可绕过,直接调试主进程。

另外,此题动态内存执行代码较多,建议关键位置下好“硬件执行”断点,便于调试。

考虑对第9、10位的数字进行枚举,动态调试找到待解码和比较的数据,编写代码如下。

#include    
unsigned m_DE0000[] = { 0x83F08EA7,0x3F0FBA29,0xE747E97C,0x93D03647,0xEC72CD2C,0x93C0BA2E,0x90A578A3,0x2A40BA2F,\
                            0xDB3FF233,0x9031FB09,0xD1477258,0x905E3DAC,0xAB817C35,0x6BD43434,0xC49E84E4,0x83B426AF,\
                            0x51C0BA3A,0x280080B8,0x93BE3FF3,0x8E36BA3B,0xE9C0BA3C,0x93C0BA29,0x93C0B2C5,0x1680CD3F};
   
unsigned m_4340B0[] = { 0x1070EC81,0x55530000,0xBC8B5756,0x00108424,0xBBF63300,0x00000001,0x0725C68B,0x79800000,\
                            0xC8834805,0x07B140F8,0xC68BC82A,0x07E28399,0xF8C1C203,0x38148A03,0xD322FAD2,0x10349488,\
                            0x46000002,0x7C40FE83,0x0002BDCF,0x05BA0000,0xBE000000,0x00000014,0x000008B9,0x8DC03300};
int main()
{
     unsigned xx = 0;
     unsigned yy,zz1,zz2;
     for(xx=0;xx<=0xff;xx++)
     {
         yy = 0x1010101*xx;
         zz1=(m_DE0000[0]+yy)^m_4340B0[0];
         zz2=(m_DE0000[1]+yy)^m_4340B0[1];
         if(zz1 == (zz2-1))
         {
              printf("%02X\n",xx);
              printf("%08X\n",zz1);
         }
     }
     return 0;
}

四、第二个关键算法

还是sub_402300中,刚解码的函数对输入注册码第10位以后的字符串进行处理,处理结果与“!HelloHaniella!”比较,返回值与sub_402240的返回值比较。

五、结论

组合两个关键算法的结果,得到注册码“75A29C09E180217C048420956C15DA309FF2B69170”,输入程序,结果如下。

当然,由于作者没有考虑到结尾的0x00,导致结尾加上(0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00)解密后的结果“FAB17827375A8685”后依然正确,如下。理论上讲有无数个即“75A29C09E180217C048420956C15DA309FF2B69170(FAB17827375A8685循环)”。


详细的解析,戳:http://bbs.pediy.com/thread-218701.htm



最后感谢 WiFi 万能钥匙安全应急响应中心的赞助支持,

接下来的比赛大家一定要使出洪荒之力哦!↖(^ω^)↗

比心 ❤



赞助商

上海连尚网络科技有限公司成立于 2013 年,是一家专注于提供免费上网和内容服务的移动互联网企业。连尚网络自主研发的核心产品 WiFi 万能钥匙,以分享经济的模式,通过云计算和大数据技术,利用热点主人分享的闲置WiFi资源,为用户提供免费、稳定、安全的上网服务,以帮助更多的人上网,找到属于他们的机会,改变自己的命运。



返回