首页
社区
课程
招聘
[原创]不走寻常路——谈“喝啥哟”CrackMe的制作过程
发表于: 2010-3-7 11:11 11861

[原创]不走寻常路——谈“喝啥哟”CrackMe的制作过程

2010-3-7 11:11
11861

  上次制作的CrackMe,至今没有一个人给出一个明确的破解思路,所以到此为止吧,现在公布此CrackMe的制作过程:
    1、不走寻常路,是此CrackMe的最大特点——从破解者的思路出发,一般的破解者喜欢从OnBut函数处突破,也就是说查找按钮函数,然后从按钮函数分析!此CrackMe就利用这一点,偏偏OnButPlay函数中什么也没有,唯一有的只是一个“标志”。什么标志?线程函数继续运行的标志!在OnButPlay函数中,会对输入进行诱惑式地判断,如果 答案!=“蜜汁” 线程函数才继续验证,否则将会什么也没有。

//下面是OnButPlay函数中的代码
void CCrackMeDlg::OnButPlay()
{
        // TODO: Add your control notification handler code here
        CString sFront4;
        BYTE s[4] = {0};
        UpdateData();
        if (m_sHSY.GetLength() < 4){
                return;
        }
        sFront4 = m_sHSY.Left(4);
       
        s[3] = sFront4[3]^0x44;
        for (int i = 0; i < 3; i ++){
                s[i] = sFront4[i]^sFront4[i+1];
        }
        DWORD x = *(PDWORD)s;
        //这里是一个诱惑,实际上只是设置线程是否继续向下执行的标志
        //如果nRun 为零,将永远不可能会得到正确答案。
        nRun = ~(x ^ 0xe97b0d18);  //诱惑答案:“蜜汁”
}

而关键的校验却在一个线程函数中进行,只要nRun的标志一满足,线程就走了:

DWORD CCrackMeDlg::MyCalcThread1(LPVOID lp1)
{
        //在这里是验证的第一部分,非常简洁
        //直接取前两个汉字的四个字节
        //s0 ^= s1, s1 ^= s2, s2 ^= s3 s3 ^= 0x44
        //结果直接取DOWRD共四字节,正好直接比较!
        BYTE a[4] ={0};
        while (! nRun){  //nRun如果为0,就一直“死”在这里,由于是在线程中,所以没有一点“迹象”
                Sleep(100);
        }
        if (sHSY.GetLength() < 4){
                return 0;
        }
        a[3] = (sHSY.GetAt(3) ^ 0x44);
        for (int i = 0; i < 3; i ++){
                a[i] = sHSY.GetAt(i) ^ sHSY.GetAt(i+1);
        }
        DWORD x = *(PDWORD)a;
        if (! (x ^ 0x830b3548)){ //如果相等,则以消息方式进入第二部分验证
                ::PostMessage(AfxGetApp()->m_pMainWnd->m_hWnd, MY_MSG_CALC, NULL, NULL);
        }
        return 0; //否则线程结束,陷入泥沼……
}

此线程的启动却是由TextChange引发的,嘿嘿!这一点可能也是很多人没有想到的地方吧:

void CCrackMeDlg::OnChangeEditHsy()
{
        // TODO: If this is a RICHEDIT control, the control will not
        // send this notification unless you override the CDialog::OnInitDialog()
        // function and call CRichEditCtrl().SetEventMask()
        // with the ENM_CHANGE flag ORed into the mask.
       
        // TODO: Add your control notification handler code here
        nRun = 0;
        UpdateData();
        sHSY = m_sHSY;  //sHSY是一个静态变量,全局校验并没有依赖于m_sHSY
       
        if (NULL != m_pCalcThread1){
                TerminateThread(m_pCalcThread1->m_hThread, 0);
                delete m_pCalcThread1;
                m_pCalcThread1 = NULL;
        }
        m_pCalcThread1 = AfxBeginThread(AFX_THREADPROC(MyCalcThread1), NULL, 0, 0, CREATE_SUSPENDED, NULL);
        m_pCalcThread1->m_bAutoDelete = FALSE;
        m_pCalcThread1->ResumeThread();
}

    2、利用有的破解者过分依赖于IDA的强大,除对部分代码进行运行时解密处理以外,另专门自己PEDIY一个tls回调函数,此tls函数具有以下特点:
  a、对OEP进行解码处理,也就是间接检查是否在OEP处有断点存在,解码不正确,则直接崩溃!我承认这比较狠,但十分有效。
  b、tls函数系手动添加,处于代码段的间隙中,而且除关键的AddressOfIndex以及PIMAGE_TLS_CALLBACK以外,tls目录中不存在任何其它信息!所以IDA的Ctrl+E所看到的就是“空气”!

    下面看此函数的纯手工汇编代码(前面的地址是在编译好以前多次试验中取下来的一次):

00423580   .  837C24 08 01  cmp     dword ptr [esp+8], 1             ;  //从这里开始
00423585   .  75 2B         jnz     short 004235B2
00423587   .  60            pushad
00423588   .  64:A1 1800000>mov     eax, dword ptr fs:[18]
0042358E   .  8B40 30       mov     eax, dword ptr [eax+30]
00423591   .  8B40 08       mov     eax, dword ptr [eax+8]            //得到当前的模块基址,这里直接跳过GetModuleHandle使用,

使代码更加隐蔽!
00423594   .  8B50 38       mov     edx, dword ptr [eax+38]           //得到加密值
00423597   .  8B58 3C       mov     ebx, dword ptr [eax+3C]           
0042359A   .  03D8          add     ebx, eax
0042359C   .  8B5B 28       mov     ebx, dword ptr [ebx+28]
0042359F   .  03D8          add     ebx, eax                         //得到OEP
004235A1   .  33F6          xor     esi, esi
004235A3   .  B9 C8000000   mov     ecx, 0C8                        //解码OEP前200字节代码
004235A8   >  8A03          mov     al, byte ptr [ebx]
004235AA   .  32C2          xor     al, dl                           
004235AC   .  8803          mov     byte ptr [ebx], al            
004235AE   .  43            inc     ebx
004235AF   .^ E2 F7         loopd   short 004235A8
004235B1   .  61            popad
004235B2   >  C3            retn

//说明,加密值存放在dos头的e_lfanew前四字节中。(自已设定)

    3、对文件的修改时间以及相关联的文件大小进行校验,将计算的校验值写入dosheader的第0x30处,占16字节,即两个BYTE长度,在

CrackMe初始实例的时候进行校验,注意,并不是在OnInitDialog()中校验,发现不对,直接退出,下面是校验函数原码(代码中间标记部分进

行了加密处理,运行时自已再解码):

BOOL CFileCheck::IsFileChanged()
{
        //记录文件最后修改的时间
        DWORD dwStart, dwEnd;
        static bFirstIn = TRUE;
        if (bFirstIn){ //只进行一次解码
                bFirstIn = FALSE;  
                _asm{
                        mov dwStart, offset lbStart
                        mov dwEnd, offset lbEnd
                }
                for (PBYTE pbyteDecode = (PBYTE)dwStart; pbyteDecode <= (PBYTE)dwEnd; pbyteDecode ++){

                        *pbyteDecode ^= 0xE8;
                }
        }

        try{
                _asm{  //标记下面的加密代码段,对应机器码 404890
                        inc eax
                        dec eax
                        nop
                }
lbStart:
        FILETIME ftLastChange = {0};
        PFILETIME pFileTime = NULL;
        SYSTEMTIME stLastChange = {0};
        _IMAGE_DOS_HEADER dosHeader = {0};
        DWORD dwLowFileTime, dwHighFileTime; //分别保存文件最后修改时间的低32与高32位
        DWORD dwLowFileTime2, dwHighFileTime2; //读取原来的时间高低位
        CString szFilePath = GetFilePath();
        try{
                CStdioFile stFile(szFilePath.GetBuffer(0), CFile::modeRead);
                DWORD dwFileSize = GetFileSize(HANDLE(stFile.m_hFile), NULL);
                dwFileSize = (((dwFileSize<<4)+1234)^4567)-8901;
                if (GetFileTime(HANDLE(stFile.m_hFile), NULL, NULL, &ftLastChange)){
                        dwLowFileTime = ftLastChange.dwLowDateTime;
                        dwHighFileTime = ftLastChange.dwHighDateTime;
                        _asm{
                                rol dwLowFileTime, 30
                                ror dwHighFileTime, 30
                        }
                        dwLowFileTime ^= dwFileSize;
                        dwHighFileTime ^= dwFileSize;
                }
                stFile.Read(&dosHeader, sizeof(_IMAGE_DOS_HEADER));
                pFileTime = PFILETIME((PDWORD)&dosHeader+12); //读取距离文件头部 12*sizeof(DWORD)处的标记!
                stFile.Close();
                dwLowFileTime2 = pFileTime->dwLowDateTime;
                dwHighFileTime2 = pFileTime->dwHighDateTime;
                if (! (dwLowFileTime2 ^ dwLowFileTime) && ! (dwHighFileTime2 ^ dwHighFileTime)) return FALSE;
                ftTime.dwLowDateTime = dwLowFileTime;   //保存文件修改时间低32位
                ftTime.dwHighDateTime = dwHighFileTime;  //保存文件修改时间高32位
lbEnd:
                _asm { //标记上面的加密代码段
                        inc eax
                        dec eax
                        nop
                }
        }catch(...){  //遇到任何问题,直接返回TRUE
                return TRUE;
        }
        }
        catch(...){
                return TRUE;
        }
        return TRUE;

}

4、当第一部分线程函数验证通过之时,会直接发送一个自定义消息,而此消息函数里面却什么也没有,除了显示“不错”的消息提示框:

LRESULT CCrackMeDlg::OnMyMsgCalc(WPARAM wParam, LPARAM lParam){
        //这里直接调用AfxMessageBox,由于它被重载,所以会进入第二部分验证。
        char k[]={0x3a,0x33,0x3c,0x65,0x2b,0x29, 0}; //提示字串"不错!"与0x88异或后的密串
        for (int i = 0; i < 6; i ++){
                k[i] ^= 0x88;
        }
        AfxMessageBox(k, MB_OK|MB_ICONINFORMATION);
        return 1;
}

利用MFC的强大,AfxMessageBox被重载,在这里面只有跟踪进入,才能发现隐藏的第二部分验证:

int CCrackMeApp::DoMessageBox(LPCTSTR lpszPrompt, UINT nType, UINT nIDPrompt)
{
        // TODO: Add your specialized code here and/or call the base class
        if (CCrackMeDlg::sHSY.GetLength() < 8){ //如果输入的汉字少于四个,就悄悄地离开。
                return TRUE;
        }
        MyOwnMsgBox(lpszPrompt, nType, nIDPrompt); //否则,进入第二部分验证。
        return TRUE;
}

//重载的AfxMessageBox,在这里将进行第二部分验证
void CCrackMeApp::MyOwnMsgBox(LPCTSTR lpszPrompt, UINT nType, UINT nIDPrompt)
{
        //第二部分验证(取中部的两个汉字,4字节):
        //s0-s2 = 0x36
        //s3-s1 = 2
        //s1-s2 =0x42
        //s3 ^ 0xcc = 0x36

        CString s = CCrackMeDlg::sHSY.Mid(4, 4);
        BYTE a = s.GetAt(0) - s.GetAt(2);
        BYTE b = s.GetAt(3) - s.GetAt(1);
        BYTE c = s.GetAt(1) - s.GetAt(2);
        BYTE d = s.GetAt(3) ^ 0xcc;
        if (d != 0x36 || a != 0x1c || b != 0x2 || c != 0x42) return;
        //这里直接调用MessageBox后由于设置了CBT钩子,所以会进入第三部分验证过程
        ::MessageBox(m_pMainWnd->m_hWnd, lpszPrompt, "", MB_OK);
}

而这里的验证也是一个更大的诱惑,当验证通过之里,直接提示“不错”对话框!殊不知软件早已埋了伏笔,暗渡陈仓!

5、钩子的利用,验证“喝啥哟”第三部分,在OnInitDialog()中早已设下钩子:

hHook = SetWindowsHookEx(WH_CBT, HOOKPROC(CBTHookProc), 0, GetCurrentThreadId());

当跳出“不错”对话框时,实际在钩子函数中将会发生第三部分验证:

LRESULT WINAPI CBTHookProc( long nCode,WPARAM wParam,LPARAM lParam){
#define  IDPROMPT 65535
        byte s1[]={0x7f,0x05, 0x75, 0x6a,0x00};  //“成功” 异或0xCC后的加密串
        //“好样的!”异或0xCC后的加密串
        byte s2[]={0x76, 0x0f, 0x1d, 0x35, 0x79, 0x08, 0xa3, 0xa1, 0x00};
       
        //这里验证第三部分,取字段最后六字节,三个汉字
        //自己设计的算法:(设6个字节分别为s0……s6)
        // s0 = s3
        // s1 = s5
        // s4 - s2 = 8
        // s1 - s0 = F = (2*8-1)
        // s1 = E0 = (EE-0E)
        // s4 = ROR(C7^AA), 4

        if (nCode == HCBT_ACTIVATE)
        {
                if (HWND(wParam) != AfxGetApp()->m_pMainWnd->m_hWnd){
                        HWND hWnd = GetDlgItem(HWND(wParam), IDPROMPT);
                        if (NULL != hWnd){
                                if (CCrackMeDlg::sHSY.GetLength() <14){
                                        return CallNextHookEx(hHook, nCode, wParam, lParam);        
                                }
                                CString s = CCrackMeDlg::sHSY.Right(6);
                                if (0 != (s.GetAt(0) ^ s.GetAt(3))) {
                                        return CallNextHookEx(hHook, nCode, wParam, lParam);
                                }
                                if (0 != (s.GetAt(1) ^ s.GetAt(5))){
                                        return CallNextHookEx(hHook, nCode, wParam, lParam);
                                }
                                BYTE a = s.GetAt(4)-s.GetAt(2);
                                if (a != 8){
                                        return CallNextHookEx(hHook, nCode, wParam, lParam);
                                }
                                BYTE b = s.GetAt(1)-s.GetAt(0);
                                if (b != 2*a-1){
                                        return CallNextHookEx(hHook, nCode, wParam, lParam);
                                }
                                if ((0xEE-0x0E) != (BYTE)s.GetAt(1)){
                                        return CallNextHookEx(hHook, nCode, wParam, lParam);
                                }
                                BYTE c = (BYTE)s.GetAt(4);
                                _asm{
                                        rol c, 4
                                        xor c, 0xaa
                                }
                                if (0xc7 != c){
                                        return CallNextHookEx(hHook, nCode, wParam, lParam);
                                }
                                for (int i = 0; i < 4; i ++){
                                        s1[i] ^= (BYTE)0xcc;
                                }
                                for (i = 0; i < 6; i ++){
                                        s2[i] ^= (BYTE)0xcc;
                                }
                                SetWindowText(HWND(wParam), (PCHAR)s1);
                                SetDlgItemText(HWND(wParam), IDPROMPT, PCHAR(s2));
                                UnhookWindowsHookEx(hHook);
                        }
                }
        }
        return CallNextHookEx(hHook, nCode, wParam, lParam);        
}

6、其他暗礁:
    a、对ExitProcess以及CreateThread函数进行了前21个字节的软断检测,发现断点,立即退出。检测方法为直接用函数指针读取法:

//下面是其中一个函数的检测:
        typedef void(WINAPI *_pExitProcess)(UINT); //检测ExitProcess断点
        _pExitProcess pExitProcess = ExitProcess;
        PBYTE pData = (PBYTE)pExitProcess;
        for (int i = 0; i <= 20; i ++){
                BYTE b = *pData++ ^ 0x88;
                if (0x44 == b) //如果遇到了0xcc断点,就88了
                        return FALSE;
                //pData++;
        }

    b、故意引发int3断点异常,如果被调试器忽略,嘿嘿,那也是再会了:

    try{ //故意引发一个int3异常,反调试
                _asm int 3
                ExitProcess(0);
        }catch(...){}

7、知道以上的制作思路以后,相信大家要破解此CM也不会有太大问题了……!

总结:
    编写软件的时候从逆向者的角度出发,会比较针对性地反破解,从而让逆向者处于山重水覆疑无路的困境。其实逆向者也可以这样从正面

来思考,这样就会避免陷入定向思维的陷阱中。从软件的编写角度来讲,要避免固定的将所有代码全放进一个按钮函数中的老套路,将代码验

证人为地故意分开,分三段,四段或更多段,隐蔽于各个角落处,将会让软件更加地难于分析……


[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

收藏
免费 7
支持
分享
最新回复 (28)
雪    币: 119
活跃值: (10)
能力值: ( LV9,RANK:160 )
在线值:
发帖
回帖
粉丝
2
对这个CM无可奈何,终于盼到解密了!!学习学习!!非常感谢不问年少分享!!
2010-3-7 11:18
0
雪    币: 238
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3
将是一篇精华
2010-3-7 11:40
0
雪    币: 485
活跃值: (12)
能力值: ( LV9,RANK:490 )
在线值:
发帖
回帖
粉丝
4
想法很暴力,学习了
2010-3-7 11:49
0
雪    币: 218
活跃值: (10)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
5
学到了不少东西~~
2010-3-7 11:52
0
雪    币: 223
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
6
学习了,多线程搞猥琐就是强
2010-3-7 12:01
0
雪    币: 2067
活跃值: (82)
能力值: ( LV9,RANK:180 )
在线值:
发帖
回帖
粉丝
7
MsgBox("不错") 我以为完了哩
后来你附上成功照片
其实也就完了.
2010-3-7 12:09
0
雪    币: 136
活跃值: (1475)
能力值: ( LV6,RANK:80 )
在线值:
发帖
回帖
粉丝
8
厉害。。学习了。。哈
2010-3-7 12:21
0
雪    币: 1074
活跃值: (160)
能力值: ( LV13,RANK:760 )
在线值:
发帖
回帖
粉丝
9
呵呵,完没完这其实完全在于一个思路,甚至可以内联hook MessageBox进行第四部分验证,修改输入表进行第五部分验证都行,无穷尽也~,关键是不能陷入定势思维
2010-3-7 12:21
0
雪    币: 220
活跃值: (55)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
10
楼主真强,学习了!!!!!!!
2010-3-7 12:21
0
雪    币: 2067
活跃值: (82)
能力值: ( LV9,RANK:180 )
在线值:
发帖
回帖
粉丝
11
下次弄个更强更WS的, 弟弟是支持你的.
不过要先附上成功图
否则若你等10分钟才最后验证不就晕死人了
2010-3-7 12:30
0
雪    币: 1074
活跃值: (160)
能力值: ( LV13,RANK:760 )
在线值:
发帖
回帖
粉丝
12
呵呵,大侠又谦虚了,其实这个CM让我自己来,我更可能不知所措,大侠却能迅速来到第二部分验证处,已经很强大了,我对大侠其实很佩服地!
2010-3-7 12:38
0
雪    币: 485
活跃值: (12)
能力值: ( LV9,RANK:490 )
在线值:
发帖
回帖
粉丝
13
S说的确实是我们写CM应该注意的,误导别人的提示要么放在按正常流程不可能走到的地方,要么就必需暗示出这不是正确的地方。否则是应该放出成功图。
2010-3-7 13:20
0
雪    币: 5334
活跃值: (3724)
能力值: ( LV13,RANK:283 )
在线值:
发帖
回帖
粉丝
14
设计的好复杂啊,有时间再看
2010-3-7 13:50
0
雪    币: 119
活跃值: (10)
能力值: ( LV9,RANK:160 )
在线值:
发帖
回帖
粉丝
15
思路挺清晰的,程序设计的很好,制作过程写的也很不错!
2010-3-7 14:02
0
雪    币: 13089
活跃值: (4087)
能力值: ( LV15,RANK:1673 )
在线值:
发帖
回帖
粉丝
16
学习了...果然BT...



偶开始也只是分析到"不错"...看到照片后还以为走错路了...
要不是瞎猫碰到死耗子偶也不知道后面的"燕窝粥"就会改变乾坤...
2010-3-7 15:02
0
雪    币: 135
活跃值: (26)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
17
while(1)
{
     AfxMessageBox("强大!");
}
2010-3-7 15:21
0
雪    币: 218
活跃值: (10)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
18
LZ放出用OD跟踪的过程吧。。。
2010-3-7 20:38
0
雪    币: 1074
活跃值: (160)
能力值: ( LV13,RANK:760 )
在线值:
发帖
回帖
粉丝
19
不要什么事都等别人做好,自已动手试一试才能进步的。再说已经给出了此CM的明确思路,只有自己亲自动手才会进步!
2010-3-8 08:13
0
雪    币: 145
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
20
很好,很强大
2010-3-8 08:32
0
雪    币: 10902
活跃值: (3288)
能力值: (RANK:520 )
在线值:
发帖
回帖
粉丝
21
写的很不错,有新意,有技术  可见作者下了不少功夫 很值得学习
作者一路走来进步很快,期待下一个更加有技术性 趣味性的CrackMe!
2010-3-11 04:50
0
雪    币: 1074
活跃值: (160)
能力值: ( LV13,RANK:760 )
在线值:
发帖
回帖
粉丝
22
谢谢版主支持,没有看雪,也不会有现在的不问年少,谢谢大家的帮助!
2010-3-11 09:51
0
雪    币: 65
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
23
思路很好,很强大 !
2010-3-11 10:29
0
雪    币: 65
活跃值: (12)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
24
lz能不能把整个工程直接给我们,看你这代码晕晕的
2010-3-12 20:42
0
雪    币: 1074
活跃值: (160)
能力值: ( LV13,RANK:760 )
在线值:
发帖
回帖
粉丝
25
给你整个工程也不能正常运行,还得用第三方软件加密,再给你第三方软件的工程,你还是不能正常运行,还得手动PEDIY……
2010-3-13 08:34
0
游客
登录 | 注册 方可回帖
返回
//