请使用C++程序编写一个带有一定加密技术(对抗逆向分析)的CrackMe程序,并使用IDA等工具逆向分析其原理。
说明:
1、如果编写的CrackMe程序过于复杂,则不强制要求使用IDA分析。
2、提交时,请提交实验报告、源代码(.cpp)文件以及编译好的exe程序。
3、除界面不做要求以外,CrackMe程序编写规则请参考看雪CTF规则 https://bbs.pediy.com/thread-249064.htm
实验02:CrackMe攻防.rar
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------
实验开始准备阶段:
首先从网络教学平台下载资源,然后把自己电脑的杀毒软件关掉,因为Sample_3带有自动复制文件的功能,会被杀毒软件认为是病毒。所以要把杀毒软件关掉。
注意:这个实验期末考试会选出一段进行考试,所以要注意
.text:0000000140003080
.text:0000000140003080
.text:0000000140003080 ; Attributes: bp-based frame fpd=57h
.text:0000000140003080
.text:0000000140003080 ; void __cdecl CMFCApplication1Dlg__OnBnClickedButton1(CMFCApplication1Dlg *this)
.text:0000000140003080 ?OnBnClickedButton1@CMFCApplication1Dlg@@QEAAXXZ proc near
.text:0000000140003080
.text:0000000140003080 var_70= qword ptr -70h
.text:0000000140003080 var_68= qword ptr -68h
.text:0000000140003080 var_60= qword ptr -60h
.text:0000000140003080 var_58= dword ptr -58h
.text:0000000140003080 var_54= word ptr -54h
.text:0000000140003080 Str= byte ptr -50h
.text:0000000140003080 var_48= qword ptr -48h
.text:0000000140003080 var_40= qword ptr -40h
.text:0000000140003080 var_38= dword ptr -38h
.text:0000000140003080 var_34= word ptr -34h
.text:0000000140003080 var_30= qword ptr -30h
.text:0000000140003080 var_28= qword ptr -28h
.text:0000000140003080 var_20= qword ptr -20h
.text:0000000140003080 var_18= dword ptr -18h
.text:0000000140003080 var_14= word ptr -14h
.text:0000000140003080 var_10= qword ptr -10h
.text:0000000140003080 arg_8= qword ptr 18h
.text:0000000140003080
.text:0000000140003080 mov [rsp-8+arg_8], rbx
.text:0000000140003085 push rbp
.text:0000000140003086 lea rbp, [rsp-57h]
.text:000000014000308B sub rsp, 90h
.text:0000000140003092 mov rax, cs:__security_cookie
.text:0000000140003099 xor rax, rsp
.text:000000014000309C mov [rbp+57h+var_10], rax
.text:00000001400030A0 xor eax, eax
.text:00000001400030A2 lea r8, [rbp+57h+Str] ; lpStr
.text:00000001400030A6 mov edx, 3E8h ; nID
.text:00000001400030AB mov qword ptr [rbp+57h+Str], rax
.text:00000001400030AF mov rbx, rcx
.text:00000001400030B2 mov [rbp+57h+var_48], rax
.text:00000001400030B6 mov [rbp+57h+var_40], rax
.text:00000001400030BA lea r9d, [rax+1Eh] ; nMaxCount
.text:00000001400030BE mov [rbp+57h+var_38], eax
.text:00000001400030C1 mov [rbp+57h+var_34], ax
.text:00000001400030C5 mov [rbp+57h+var_30], rax
.text:00000001400030C9 mov [rbp+57h+var_28], rax
.text:00000001400030CD mov [rbp+57h+var_20], rax
.text:00000001400030D1 mov [rbp+57h+var_18], eax
.text:00000001400030D4 mov [rbp+57h+var_14], ax
.text:00000001400030D8 mov [rbp+57h+var_70], rax
.text:00000001400030DC mov [rbp+57h+var_68], rax
.text:00000001400030E0 mov [rbp+57h+var_60], rax
.text:00000001400030E4 mov [rbp+57h+var_58], eax
.text:00000001400030E7 mov [rbp+57h+var_54]
解释:
// TODO: 在此添加控件通知处理程序代码
char szUser[MAXBYTE] = { 0 };
char szPassword[MAXBYTE] = { 0 };
char szTmpPassword[MAXBYTE] = { 0 };
--------------------------------------------------------------------------------------------------------------------------------------
.text:00000001400030EB call ?GetDlgItemTextA@CWnd@@QEBAHHPEADH@Z ; CWnd::GetDlgItemTextA(int,char *,int)
.text:00000001400030F0 mov r9d, 1Eh ; nMaxCount
.text:00000001400030F6 lea r8, [rbp+57h+var_30] ; lpStr
.text:00000001400030FA mov edx, 3E9h ; nID
.text:00000001400030FF mov rcx, rbx ; this
.text:0000000140003102 call ?GetDlgItemTextA@CWnd@@QEBAHHPEADH@Z ; CWnd::GetDlgItemTextA(int,char *,int)
.text:0000000140003107 mov r9, 0FFFFFFFFFFFFFFFFh
.text:000000014000310E lea rcx, [rbp+57h+var_30]
.text:0000000140003112 mov rax
解释:
// 获取输入的账号和密码
GetDlgItemText(IDC_EDIT_USER, (LPTSTR)szUser, MAXBYTE);
GetDlgItemText(IDC_EDIT_PASSWORD, (LPTSTR)szPassword, MAXBYTE);
注:
GetDlgItemText
GetDlgItemText是C++中的函数,调用这个函数以获得与对话框中的控件相关的标题或文本。GetDlgItemText成员函数将文本拷贝到lpStr指向的位置并返回拷贝的字节的数目。
函数原型:
int GetDlgItemText( HWND hDlg , int nID, LPTSTR lpStr, int nMaxCount) const;
int GetDlgItemText( int nID, CString& rString) const;
参数说明:
nID 指定了要获取其标题的控件的整数标识符。 lpStr 指向要接收控件的标题或文本的缓冲区。 nMaxCount 指定了要拷贝到lpStr的字符串的最大长度(以字节为单位)。如果字符串比nMaxCount要长,它将被截断。 rString 对一个CString对象的引用。
返回值:
如果函数调用成功,返回值为拷贝到缓冲区中的 TCHAR 字符个数(不包括结束空字符)。
如果函数调用失败,返回值为 0 。要获取更多错误信息,请调用 GetLastError 函数。
说明
如果字符串的长度大于缓冲区的长度,则会被截断,并以空字符结束。
GetDlgItemText 函数向控件发送一条 WM_GETTEXT 消息。
在 ANSI 版本中,长度指的是字节数,在 Unicode 版本中,长度指的是字符的个数。
-------------------------------------------------------------------------------------------------------------------------------------------------------------
.text:0000000140003115
.text:0000000140003115 loc_140003115:
.text:0000000140003115 inc rax
.text:0000000140003118 cmp byte ptr [rcx+rax], 0
.text:000000014000311C jnz short loc_1400
解释:
// 判断账号是否为空
if (szUser == 0)
{
return;
}
注:
linc 加1指令
ldec 减1指令
一、加一指令inc
inc a 相当于 add a,1 //i++优点 速度比sub指令快,占用空间小
这条指令执行结果影响AF、OF、PF、SF、ZF标志位,但不影响CF进位标志位.
二、减一指令dec
dec a 相当于 sub a,1
004012D7 > 83E8 01 SUB EAX,1
004012DA 836D FC 01 SUB DWORD PTR SS:[EBP-4],1
004012DE 41 INC ECX
004012DE FF41 FC INC DWORD PTR DS:[ECX-4]
优点 速度比sub指令快,占用空间小
这条指令执行结果影响AF、OF、PF、SF、ZF标志位,但不影响CF进位标志位.
--------------------------------------------------------------------------------------------------------------------------------------
.text:000000014000311E test rax, rax
.text:0000000140003121 jz loc_140
解释:
// 判断密码是否为空
if (strlen(szPassword) == 0)
{
return;
}
--------------------------------------------------------------------------------------------------------------------------------------
text:0000000140003127 xor r10d, r10d
.text:000000014000312A lea rax, [rbp+57h+Str]
.text:000000014000312E db 66h
.text:000000014000312E
注:
作用:
在这里B是byte的缩写,即字节,所以,该伪操作所定义的每个操作数占有1个字节(8位)。
如:
1 |
MESSAGE DB 'HELLO' |
此时,字符串“HELLO”中每个字符会占用1个字节
51单片机中,用于定义字节的内容。
DB 指令以表达式的值的字节形式初始化代码空间。
格式和注意:
表达式中可包含符号、字符串、或表达式等项,
各个项之间用逗号隔开,字符串应用引号括起来。
括号内的标号是可选项,如果使用了标号,
则标号的值将是表达式表中第一字节的地址。
DB 指令必须位于 数据段之内,否则将会发生错误。
-------------------------------------------------------------------------------------------------------------------------------------------------
解释:
进行三个比较
if (!((szUser[i] >= 'a' && szUser[i] <= 'z') || (szUser[i] >= 'A' && szUser[i] <= 'Z') || (szUser[i] >= '1' && szUser[i] <= '9')))
{
MessageBox(NULL, "输入的账号错误,输入的账号只能是字母或数字");
return;
}
如果结果是真, 跳转loc_140003210
如果是假,继续执行下去
解释:
比较三次,如果有一次是假,执行000000014000317E的指令,如果是真,执行000000014000319D的指令
if (szUser[i] == 'Z' || szUser[i] == 'z' || szUser[i] == '9')
{
szTmpPassword[i] = szUser[i];
}
--------------------------------------------------------------------------------------------------
解释:
继续比较6次,如果最后结果是假,跳转000000014000319D,如果是真,进入括号内赋值,跳转00000001400031A2
if (!((szTmpPassword[i] >= 'a' && szTmpPassword[i] <= 'z') || (szTmpPassword[i] >= 'A' && szTmpPassword[i] <= 'Z') || (szTmpPassword[i] >= '1' && szTmpPassword[i] <= '9')))
{
szTmpPassword[i] = szUser[i];
}
解释:作比较,在进行for循环,如果跳出循环跳转00000001400031B0,否则跳转0000000140003150
-----------------------------------------------------------------------------------------------------------------------------------
解释:
比较字符串,如果是假,跳转00000001400031E1,赋值密码错误,如果是真,跳转00000001400031E8,输出密码正确
// 把生成的密码和输入的密码进行匹配
if (strcmp(szTmpPassword, szPassword) == 0)
{
MessageBox("密码正确");
}
else
{
MessageBox("密码错误");
}
------------------------------------------------------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------------------------------------------
对应的c++语言:
void CMFCApplication1Dlg::OnBnClickedButton1()
{
// TODO: 在此添加控件通知处理程序代码
char szUser[MAXBYTE] = { 0 };
char szPassword[MAXBYTE] = { 0 };
char szTmpPassword[MAXBYTE] = { 0 };
//CString szUser;
// 获取输入的账号和密码
GetDlgItemText(IDC_EDIT_USER, (LPTSTR)szUser, MAXBYTE);
GetDlgItemText(IDC_EDIT_PASSWORD, (LPTSTR)szPassword, MAXBYTE);
/*
作者:
时间:2019.03.18
错误:老师使用的GetDlgItemText(IDC_EDIT_USER, (LPTSTR)szUser, MAXBYTE)接受输入的数据,szUser原本是char
类型,使用(LPTSTR)进行接收会强制转换,char类型的编码是ascii码(一个字节),而LPTSTR编码是unicode(两个字节),
用强制转换的unicode接收到szUser,而szUser原本是char类型,所以会使用两个char单元保存一个unicode单元的数据,数
据保存到低位,第一个char单元,而高位单元(双数)初始化就是\0,所以是'\0'
功能:处理szUser里的'\0',就是遍历数组,只取数组的单数单元(即低位单元),高位不取,造成的结果是长度30的字符串
只能保存15长度的有效字符串,这个输入注意就是了,也可以把长度变长,足够容纳账号
*/
//char tempSzUser[MAXBYTE] = { 0 };
//char tempSzPassword[MAXBYTE] = { 0 };
//int i;
//for (i = 0; i < MAXBYTE; i++)
//{
// if (i % 2 == 0)
// {
// tempSzUser[i / 2] = szUser[i];
// szUser[i / 2] = tempSzUser[i / 2];
// tempSzPassword[i / 2] = szPassword[i];
// szPassword[i / 2] = tempSzPassword[i / 2];
// }
//}
//tempSzUser[i / 2] = '\0';
//szUser[i / 2] = tempSzUser[i / 2];
//tempSzPassword[i / 2] = '\0';
//szPassword[i / 2] = tempSzPassword[i / 2];
// 判断账号是否为空
if (szUser == 0)
{
return;
}
// 判断密码是否为空
if (strlen(szPassword) == 0)
{
return;
}
// 根据账号生成密码
for (int i = 0; i < strlen(szUser); i++)
{
//MessageBox(NULL, L"1");
//如果输入的账号带非字母和数字,则弹出警告窗口,并结束函数调用
if (!((szUser[i] >= 'a' && szUser[i] <= 'z') || (szUser[i] >= 'A' && szUser[i] <= 'Z') || (szUser[i] >= '1' && szUser[i] <= '9')))
{
MessageBox(NULL, "输入的账号错误,输入的账号只能是字母或数字");
return;
}
else
{
//MessageBox(NULL, L"ok");
}
if (szUser[i] == 'Z' || szUser[i] == 'z' || szUser[i] == '9')
{
szTmpPassword[i] = szUser[i];
}
else
{
//szTmpPassword[i] = szUser[i] + 1;
//
/*
作者:
时间:2018.03.18
修改的功能:把老师的生成密码的算法做一个改进,即是把老师把账号名的
ascii码静态加1生成字符(密码)修改为根据账号的长度动态
生成字符(密码),如果输入的账号转换成密码时生成不是字母
或数字就把它变回账号时的字符形式。
*/
szTmpPassword[i] = szUser[i] + strlen(szUser);
if (!((szTmpPassword[i] >= 'a' && szTmpPassword[i] <= 'z') || (szTmpPassword[i] >= 'A' && szTmpPassword[i] <= 'Z') || (szTmpPassword[i] >= '1' && szTmpPassword[i] <= '9')))
{
szTmpPassword[i] = szUser[i];
}
}
}
// 把生成的密码和输入的密码进行匹配
if (strcmp(szTmpPassword, szPassword) == 0)
{
MessageBox("密码正确");
}
else
{
MessageBox("密码错误");
}
}
流程图:
通过本次CrackMe攻防实验的联系,粗略地认识到了现实中破解行业从业者是怎么去通过逆向获得一些类似注册码,登录密码之类的技术。本次实验我也和上一次实验分析有所不同,上一次实验我是一字一句分析,把每一个汇编语言都解释了出来,这次本来我也是想一句句汇编解释,可是这样做未免太过复杂,一大堆代码跳转,一大堆汇编语句。上次我之所以可以一句句分析,是程序太过简单,同时也可以锻炼一下自己,训练自己认识汇编语言。而这次,工程比较大,一句句分析汇编是行不通了,又想起老师说的针对重点部分分析,大致了解,我于是便通过这种方式,终于把汇编的大致结构了解清楚,更深刻地认识到了自己不能去转牛角尖分析汇编代码,进行破解。