返回信息流题目:Crack菜鸟教学--入门
作者:frank
声明:本人菜鸟,发这篇文章希望帮助有兴趣的童鞋进入这个领域--同时增加本版技术含量:
)
Crack是什么不用我解释了吧,相信用过注册机得同学肯定不陌生。这里说点个人的见解,
Crack分为两种类型:1、为Crack而Crack--通过投机取巧的方法定位注册代码之后跳过注册算
法的跟踪直接爆破;2、Crack且Reverse--通过对软件使用语言或编译环境的熟悉定位注册函数
,逆向出注册算法并能做出注册机。第一种并没什么不对,但是却学不到任何东西--比如这次
你通过字符串查找定位到的注册函数,但是只要作者有心你就不可能在软件里搜索到字符串了
。如何这么做呢?假设你搜索的字符串是"frank:)", 作者完全可以把'f''r''a''n'写成整形
变量0x6e617266,'k'':'')''\0'写成0x00293a6B,然后在运行时把两个整形赋值给一个整形数
祖完成拼接,最后打出提示信息。对付这样的软件投机取巧的方法就一定好用了!
这个时候只有通过对软件使用语言及编译环境的分析去才能正确定位注册函数。肯定又会有
人问了:你怎么就知道人家采用什么语言以及编译环境开发的阿,你拿到的不过是个二进制的
文件而已!说得对,我们拿到的就只是一个二进制文件而已,但这部代表我们不能推断出去使
用的语言及编译环境!那么如何做呢?答案就是特征码--每种编译环境生成的启动代码都有其
自己的特点,比如VC6生成的启动代码总是按照如下序列执行:获得操作系统版本号,分配堆,
获得环境变量,...跳转到main或WinMain。正因为其启动代码总是如此,所以我们可以比较可
执行文件中启动代码部分来确认一个编译器--不用完全比较,一般比较前面一些字节。那么要
我们自己去比较?当然不用了,早有高人开发了很好用的工具--PEID, Fileinfo等。获得了编
译器信息自然也就有了语言信息了--一种编译器只针对一种语言。
接下来就是考验你编程的功底了。比如:本文使用的例子,注册方法就是填完信息后点击
Check按钮。我们都知道Windows是消息事件驱动的,那么Check按钮对应的按钮事件处理函数即
注册函数。同时假设你已经知道软件是由Win32 SDK编写。如果你对Win32 SDK很熟悉的话找到
它易如反掌。注意:文件检测工具一般只会给出VC6之类的信息,不会给出具体是Win32 SDK(c
语言)还是MFC编写的。区别的方法就是MFC会跳转到AfxWinMain而SDK直接跳转到WinMain函数
。这同样需要相关的知识。但是各位不用怕,因为你不需要太深的相关知识--毕竟是让你逆向
而不是开发,也不用专门买书去看--看也没有。我的建议是自己用这种编译器编译一个软件,
然后把它逆向一遍,保证比市面上的那些《XX天精通XXX》强得多!!至于你说如何起步?看相
关编译器的帮助文档--权威而且原汁原味。
[color=blue]In[/color] [color=black]Darkness[/color][color=blue], I Live.[/color]
[color=blue]In[/color] [color=black]Darkness[/color][color=blue], I Prey.[/color]
[color=blue]In[/color] [color=black]Darkness[/color][color=blue], I Hunt.[/color]
[color=blue]In[/color] [color=black]Darkness[/color][color=blue], I Eat.[/color]
[color=black]DARKNESS[/color] [color=blue]SHALL RULE THE WORLD!!![/color]
[color=blue]That Is When[/color] [color=black]Darkness[/color] [color=blue]Is Forever.[/color]
这是一条镜像帖。来源:北邮人论坛 / security / #21406同步于 2009/3/5
该镜像源已超过 30 天没有更新,可能在源站已被删除。
Security机器人发帖
Crack菜鸟教学--入门
Dark
2009/3/5镜像同步15 回复
订阅后,新回复会通过你的通知中心匿名送达。
9 条回复
好了,言归正传开始今天的入门实战。附件中是今天要破解的Crackme--这个软件有背景音乐
,运行时最好把声音关了,太吵了。(有兴趣的同学可以把其背景音乐去掉:))
1、直观印象
和中医诊病一样,我们先对这个软件望闻问切。这么做的目的是熟悉它,以获得直观地了解
。点开输入用户名:frank:)/密码:123456789, 点Check,没反应--看来软件默认对错误注册
码不回应。看来通过错误提示字符串定位注册代码的方法没有用武之地喽。。
2、工具侦查
PEID打开发现是MS VC6编译器--应该是SDK或MFC。注意:有些软件会通过伪装启动代码以期骗
过侦测工具。这种情况这里不考虑,毕竟只是个Crackme吗。
3、反汇编
IDA pro加载,通过观察入口点代码发现是SDK编写--直接跳转到WinMain。加下来就好办了:进
入WinMain,发现程序通过DialogBoxParam API创建模式对话框--这实在是最简单的实现方式了
。下面给出该API信息:
int DialogBoxParam(
HINSTANCE hInstance, // 应用程序实例号
LPCTSTR lpTemplateName, // 对话框模版名或ID
HWND hWndParent, // 父窗口句柄
DLGPROC lpDialogFunc, // 对话框消息处理函数
LPARAM dwInitParam // WM_INITDIALOG消息伴随参数
);
这里要啰嗦一下,我们知道Windows下每个窗口均对应一个窗口消息处理函数,这些函数完成对
窗口发生的各种消息的处理。这里可以把DialogFunc当作模式对话框的窗口函数--其实他们是
不一样的,这样便于表达。
BOOL CALLBACK DialogProc(
HWND hwndDlg, // 对话框句柄--可理解为指针
UINT uMsg, // Windows消息
WPARAM wParam, // 附加参数1
LPARAM lParam // 附加参数2
);
唉,写东西好累啊。。。继续,讲讲对话框上控件(按钮之流)与对话框通信过程。以按钮
为例:在你点击后,Windows会自动发送WM_COMMAND消息给对话框处理函数,lparam参数包含该
控件的句柄,wParam参数由两部分组成:低16bit代表控件ID,高16bit代表通知码。具体来说
:本例中Check按钮的ID为1003,通知码为BN_CLICK--这里我们不用管它。什么,我怎么知道控
件ID是多少?工具阿,我用的是exescope,当然你也可以用别的。我为什么用它?因为它要注
册我把它破解了,她就成我的人了。。它怎么知道得?唉,下次写文章不能假设对象是完全没
基础的菜鸟。。因为软件里面就有,只是按照规定好的格式存在二进制文件里了。这个软件不
过是把这些信息读出来并画出来--貌似日本人做的。。在软件的哪里?这个就要去看PE文件格
式了,可以去看雪上把这篇文章下来或者我再开一个帖转过来也成啊,可以进行了吧?可以!
通过exescope我们得到几个重要的控件ID:
===========
name对应Edit控件:1000, 16进制0x3e8
serial对应Edit控件:1005,16进制0x3ed
Check按钮控件:1003, 16进制0x3eb
Abour按钮控件:1002, 16进制0x3ea
这样我们就可以通过wParam的低16bit值来判断具体是哪个控件发来的消息了。
4、DialogProc的分析
在开始本节之前,我希望各位明白函数调用是如何通过栈来传递参数的。以对话框过程函数为
例,共四个参数,c语言的调用方式是从右向左一次压栈,之后压入函数返回地址,调用完毕由
调用者平衡栈顶。示意图如下:
|返回地址| 低地址
|hDlg |
|uMsg |
|wParam |
|lParam |
|————————| 高地址
栈帧:函数执行刚开始一般第一条语句即
push ebp
mov ebp, esp
这两条语句的作用是保存ebp原址(压入栈),把目前栈顶的值传递给ebp。因为esp会随着压栈
弹栈不断变化,不利于用于相对偏移访问--一般访问局部变量。ebp被称为栈帧,在函数返回前
一般不会改动,用于访问栈内分配的局部变量。
关于局部变量的分配:
局部变量分配极其简单,通过esp上移就可以完成--一般函数开始会一次性分配足够栈空间用于
本函数内的所有局部变量。比如本例,函数开始即执行
sub esp, 0F4h
完成局部变量空间分配。
基本知识终于介绍完了,下面开始分析这个函数:
注意:IDA pro会显示一些诸如var_XX的,这些是IDA分析好的局部变量。具体含义即相对于栈
帧(ebp)偏移xx(16进制)处的变量。
; BOOL __stdcall DialogFunc(HWND,UINT,WPARAM,LPARAM)
DialogFunc proc near
var_F4= dword ptr -0F4h
var_F0= dword ptr -0F0h
var_EC= dword ptr -0ECh
String= byte ptr -0E8h
String2= byte ptr -0D8h
var_74= dword ptr -74h
var_70= dword ptr -70h
String1= byte ptr -6Ch
var_8= dword ptr -8
var_4= dword ptr -4
hDlg= dword ptr 8
arg_4= dword ptr 0Ch
arg_8= dword ptr 10h
push ebp
mov ebp, esp //栈帧赋值完成
sub esp, 0F4h //分配局部变量空间
mov eax, [ebp+arg_4] //取uMsg值
mov [ebp+var_F0], eax //存到某个变量
cmp [ebp+var_F0], 110h //uMsg与WM_INITDIALOG比较--每个消息具体数值可以在VC下建
个source insight工程,同步一下,找的时候非常方便。
ja short loc_4046B9 //如果大于WM_INITDIALOG的话跳转
大于时跳转到的指令
==============
loc_4046B9:
cmp [ebp+var_F0], 111h //与WM_COMMAND比较
jz loc_404752 //相等跳转
WM_COMMAND处理
===============
loc_404752:
mov ecx, [ebp+arg_8] //取wParam
and ecx, 0FFFFh //取低16bit
mov [ebp+var_F4], ecx //存储该值
mov edx, [ebp+var_F4]
sub edx, 3EAh //减去1002--典型的switch语句开头
mov [ebp+var_F4], edx //存储
cmp [ebp+var_F4], 4 //差与4比较
ja loc_40496A //大于退出函数--因为本例中会传来WM_COMMAND的控件ID最大
为1005
===============
mov eax, [ebp+var_F4] //取差
jmp ds:off_40499D[eax*4] //跳转表--switch语句特有的得编译结果
跳转表具体信息
===============
.text:0040499D E4 48 40 00 off_40499D dd offset loc_4048E4
.text:0040499D ; DATA XREF:
DialogFunc+116r
.text:004049A1 8D 47 40 00 dd offset loc_40478D
.text:004049A5 FC 48 40 00 dd offset loc_4048FC
.text:004049A9 6A 49 40 00 dd offset loc_40496A
.text:004049AD 43 49 40 00 dd offset loc_404943
因为我们关心的是Check ID为1003, 所以去跳转表第2项oc_40478D。
Check按钮的处理分支
===============
loc_40478D:
mov [ebp+var_4], 0 //4byte的变量,初识化为0,命名为sum1
mov [ebp+var_8], 0 //另一个4byte的变量,同样初始化为0,命名为sum2
push 0Fh ; nMaxCount //最多取15字节
lea ecx, [ebp+String]
push ecx ; lpString //存放地址
push 3E8h ; nIDDlgItem //控件ID--Name对应Edit控件
mov edx, [ebp+hDlg]
push edx ; hDlg //对话框句柄
call ds:GetDlgItemTextA //对话框取控件内容的API
lea eax, [ebp+String]
push eax ; char * //取得的字符串地址
call _strlen //计算取得串长度len
add esp, 4 //平衡栈
mov [ebp+var_74], eax //存入变量len
cmp [ebp+var_74], 3 //比较len和3
jle loc_4048DF
//输入用户名长度小于3的情况退出函数
===============
loc_4048DF:
jmp loc_40496A
===============
loc_40496A:
jmp short loc_404995
===============
loc_404995:
xor eax, eax
===============
loc_404997:
mov esp, ebp
pop ebp
retn 10h
DialogFunc endp
//输入用户名长度大于3的情况
===============
mov [ebp+var_70], 0 //给个变量赋初值--命名该变量为count(通过后面的分析得名)
jmp short loc_4047E1 //无条件跳转
计算sum1的循环,执行顺序为(1)-(2)-(3)-(1)-...
===============
(3)
loc_4047D8:
mov ecx, [ebp+var_70] //取count
add ecx, 1 //count++
mov [ebp+var_70], ecx //存入count,进行下一轮循环
(1)、
loc_4047E1:
mov edx, [ebp+var_70]
cmp edx, [ebp+var_74] //比较count和len
jge short loc_40481F //大于等于结束循环
(2)
mov eax, [ebp+var_70] //取count
movsx ecx, [ebp+eax+String] //取name的第count个字节
mov edx, [ebp+var_70] //取count
xor eax, eax //清0
mov al, byte_41ACC1[edx] //取41ACC1地址处第count个字节
add eax, [ebp+var_4] //sum1+byte_41ACC1[count]
add eax, ecx //sum1+byte_41ACC1[count]+name[count]
mov [ebp+var_4], eax //存入sum1
mov ecx, [ebp+var_70] //取count
movsx edx, [ebp+ecx+String] //取name的第count个字节name[count]
imul edx, 0Ah //name[count]*10
mov eax, [ebp+var_4] //取sum1
add eax, edx //sum1+=name[count]*10
mov [ebp+var_4], eax //存入sum1
jmp short loc_4047D8 //无条件跳转
通过分析我们可以写出本段的源代码:
for (count=0; count<len; count++) {
sum1 += name[count] * 11; //前面加了一次,后面有乘10加了一次,相当于乘11加
sum1 += byte_41ACC1[count];
}
循环执行完毕跳转到loc_40481F:
==============
loc_40481F:
mov [ebp+var_70], 0 //count又一次被清零
jmp short loc_404831 //无条件跳转
计算sum2的循环,执行顺序为(1)-(2)-(3)-(1)-...
==============
(3)
loc_404828:
mov ecx, [ebp+var_70] //取count
add ecx, 1 //count++
mov [ebp+var_70], ecx //存入count,进行下一轮循环
(1)
loc_404831:
mov edx, [ebp+var_70] //取count
cmp edx, [ebp+var_74] //比较count和len
jge short loc_404876 //大于等于结束循环
(2)
mov eax, [ebp+var_70] ; count
xor ecx, ecx
mov cl, byte_41ACC0[eax]
imul ecx, 0Ah
mov edx, [ebp+var_8] ; sum2 += byte_41ACC0[count] * 10;
add edx, ecx
mov [ebp+var_8], edx
movsx eax, byte ptr [ebp-0E6h] ; name[2]
mov ecx, [ebp+var_70]
xor edx, edx
mov dl, byte_41ACC0[ecx]
add edx, [ebp+var_8] ; sum2 += byte_41ACC0[count] * 11;
add edx, eax
mov [ebp+var_8], edx ; sum2 += byte_41ACC0[count] * 11 + name[2];
mov eax, [ebp+var_8]
add eax, 31337h ; <suspicious>
mov [ebp+var_8], eax ; sum2 += byte_41ACC0[count] * 11 + name[2] + 0x31337;
jmp short loc_404828
分析方法与上一个循环一样,具体参见分号后的表达式。这里有一点值得注意,即byte_41ACC0
是上一个循环中byte_41ACC0的上一个字节,固他们应该是采用了同一个数祖取了不同偏移而已
。
这段可以逆向出源代码:
for (count=0; count<len; count++) {
sum2 += byte_41ACC0[count]*11;
sum2 += name[2];
sum2 += 0x31337;
}
循环执行完毕跳转到loc_404876:
==============
loc_404876:
mov ecx, [ebp+var_8] //取sum2
push ecx
mov edx, [ebp+var_8] //取sum2
push edx
mov eax, [ebp+var_4] //取sum1
push eax
push offset s_X-aboo-me-XI- ; "%X-aboo-me-%X%i-SCA"
lea ecx, [ebp+String1]
push ecx ; char *
call _sprintf // sprintf(String1, "%X-aboo-me-%X%i-SCA", sum1, sum2, sum2);
add esp, 14h //平衡栈
push 64h ; nMaxCount
lea edx, [ebp+String2]
push edx ; lpString
push 3EDh ; nIDDlgItem //Serial对应Edit控件
mov eax, [ebp+hDlg]
push eax ; hDlg
call ds:GetDlgItemTextA //取用户输入的Serial
lea ecx, [ebp+String2]
push ecx ; lpString2
lea edx, [ebp+String1]
push edx ; lpString1
call ds:lstrcmpA //把用户输入Serial和sprintf结果比较
mov [ebp+var_EC], eax
cmp [ebp+var_EC], 0
jnz short loc_4048DF //不相等,退出函数
push offset String ; "Good Work! now make a keygen! "
push 3EDh ; nIDDlgItem
mov eax, [ebp+hDlg]
push eax ; hDlg
call ds:SetDlgItemTextA //相等,把Serial Edit控件内容置成"Good Work! now make a keygen! " --注册成功!!
然后就是退出函数了。。
终于写完了,总结:写出来远比Crack麻烦的多。
给出注册函数如下:
#include <string.h>
unsigned char g_byte[] = {0x13, 0x16, 0x99, 0x11, 0x63, 0x15, 0x54, 0x52, 0x88,
0x1, 0x31, 0x56, 0x68, 0x55, 0x37, 0x0}; //定义byte_41ACC0数组
char name[] = "frank:)";
int main()
{
int len = 0, count = 0;
if ((len = strlen(name)) < 3)
printf ("\r\nERROR: Name string must has at least 3 characters!");
int sum1 = 0, sum2 = 0;
for (count=0; count<len; count++) {
sum1 += g_byte[count+1];
sum1 += name[count] * 11;
}
for (count=0; count<len; count++) {
sum2 += g_byte[count]*11;
sum2 += name[2];
sum2 += 0x31337;
}
printf ("\r\nKey Generate:");
printf ("\r\n============");
printf ("\r\nName: %s", name);
printf ("\r\nKey: %X-aboo-me-%X%i-SCA\r\n", sum1, sum2, sum2);
return 0;
}
后续:
文笔很差,大家讲究着看。另:这只是个非常简单的crackme,希望对有兴趣的同学入门有所帮助。
附件上传
【 在 Dark 的大作中提到: 】
: 题目:Crack菜鸟教学--入门
: 作者:frank
: 声明:本人菜鸟,发这篇文章希望帮助有兴趣的童鞋进入这个领域--同时增加本版技术含量:
: ...................
附件(62KB) aboome2.rar