上次制作的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直播授课