简介 这篇笔记收集(参考)各个地方的 CrackMe 程序,用来作为练习。文章主要是分析算法,并编写注册机。
新 160 个 CrackMe 参考: 新160个CrackMe算法分析
001-abexcm5 查看程序基本信息
搜索字符串
F9 运行到程序领空
在当前模块搜索字符串
程序分析(静态+动态)
分析关键指令
在关键函数出设置断点,F9进行动态调试,找到序列号
向上溯源,找到第一个处理字符串的函数
程序分为3段:
第一步:将字符串 “OS” 和 “4562-ABEX” 拼接,结果为”OS4562-ABEX”
第二步:将 “OS45” 各字节+2,结果为 “QU6762-ABEX”
第三步:将 4023FD(“L2C-5781”) 和 40225C(“QU6762-ABEX”) 处的字符串拼接到 402000 地址处,结果为 “L2C-5781QU6762-ABEX”
编写注册机 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #define _CRT_SECURE_NO_WARNINGS #include <stdio.h> #include <windows.h> int main () { char str1[12 ] = "OS" ; char str2[] = "4562-ABEX" ; char str3[] = "L2C-5781" ; char result[20 ] = {0 }; strcat (str1,str2); for (int i = 0 ; i < 4 ; i++) { str1[i] = str1[i] + 2 ; } strcat (result,str3); strcat (result, str1); printf ("%s" , result); return 0 ; }
002-Cruehead-CrackMe-3 查看程序基本信息
exeinfo未识别使用的编程语言,打开程序也没有发现任何注册点,只有一个 exit 按钮。
动态调试程序
运行到程序领空
我们运行到程序领空后,直接就找到了CreateFile、ReadFile等API函数和关键字符串,未发现 main 函数的初始化操作,说明这个程序使用汇编写的。
程序分析 GetModuleHandle 函数 GetModuleHandle(NULL):获取 exe 进程的基地址
1 2 push 0 call <JMP.&GetModuleHandleA>
CreateFile 函数 CreateFile 函数可以创建新文件或打开现有文件。 必须指定文件名、创建说明和其他属性。 当应用程序创建新文件时,操作系统会将其添加到指定的目录。
CreateFile(CRACKME3.KEY,GENERIC_READ | GENERIC_WRITE,FILE_SHARE_READ|FILE_SHARE_WRITE,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL)
1 2 3 4 5 6 7 8 push 0 ; hTemplateFile push 80h ; dwFlagsAndAttributes push 3 ; dwCreationDisposition push 0 ; lpSecurityAttributes push 3 ; dwShareMode push 0C0000000h ; dwDesiredAccess push offset FileName ; "CRACKME3.KEY" call CreateFileA
CreateFileA 结构体1 2 3 4 5 6 7 8 9 HANDLE CreateFileA( [in] LPCSTR lpFileName, [in] DWORD dwDesiredAccess, [in] DWORD dwShareMode, [in, optional] LPSECURITY_ATTRIBUTES lpSecurityAttributes, [in] DWORD dwCreationDisposition, [in] DWORD dwFlagsAndAttributes, [in, optional] HANDLE hTemplateFile );
参数:
在此格式中,低顺序 16 位用于特定于对象的访问权限,接下来的 8 位用于 标准访问权限,这些权限适用于大多数类型的对象,4 个高阶位用于指定每个对象类型可以映射到一组标准权限和特定于对象的 泛型访问权限 。 ACCESS_SYSTEM_SECURITY位对应于 访问对象的 SACL 的权限。
这个程序用的是泛型访问权限,其中第 31 和 30 位置 1,其余位置 0,最终值为 C0000000h 。
dwShareMode 文件或设备的请求共享模式,可以读取、写入、删除所有这些或无 (引用下表) 。 对属性或扩展属性的访问请求不受此标志的影响。
[in, optional] lpSecurityAttributes 指向一个 SECURITYATTRIBUTES 结构的指针,该结构包含两个独立但相关的数据成员:可选的安全描述符,以及一个布尔值,该值确定返回的句柄是否可以由子进程继承。 此参数可以为 NULL。 如果此参数为 NULL,则应用程序可能创建的任何子进程都无法继承 CreateFile 返回的句柄,并且与返回的句柄关联的文件或设备获取默认的安全描述符。
dwCreationDisposition 对存在或不存在的文件或设备执行的操作。 对于文件以外的设备,此参数通常设置为 OPEN_EXISTING。
lpSecurityAttributes 文件或设备属性和标志, FILE_ATTRIBUTE_NORMAL 是文件最常见的默认值。
[in, optional] hTemplateFile 具有 GENERIC_READ 访问权限的模板文件的有效句柄。 模板文件为正在创建的文件提供文件属性和扩展属性。 此参数可以为 NULL。 打开现有文件时, CreateFile 将忽略此参数。 打开新的加密文件时,该文件将从其父目录继承任意访问控制列表。
从上面函数分析,这里的 CreateFile 函数是在尝试打开一个名为 CRACKME3.KEY 的文件。
动态调试 经过动态调试,发现后续代码是用来创建、更新 win32 窗口。
创建 CRACKME3.KEY 文件,再重新调试程序
判断 CRACKME3.KEY 文件的字符长度
将 key 长度改为 12h,再次调试
继续步过调试,发现两个关键函数
单步调试进入第一个关键call
根据汇编写下伪代码
1 2 3 4 5 6 7 8 9 10 esi=key指针;bl=0x41 ;cl=1 ;sum = 0 ;times=0 do {key[i]=key[i] xor bl; sum += key[i]; bl++; i++; cl++; }while (bl<0x4F ||key[i]!=0 ); times = cl;
紧接着还有1个关键指令
调试进入第二个关键 call
sum 值和 eax 进行比较,并且将 ZF 标志位的值传给 al 寄存器
从上图可以分析出这块代码的流程,再从代码 sete al
和 test al,al
这两条指令和下图代码来看,基本可以确认这是错误的执行流程。
现在基本可以确认指令 cmp eax,dword ptr ds:[4020F9]
是一个关键判断,执行该指令处,修改eax值后再继续调试看看程序的运行情况。
调试后发现程序成功运行
编写注册机 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #define _CRT_SECURE_NO_WARNINGS #include <stdio.h> int main () { char inputBuffer[15 ] = { 0 }; char key[5 ] = { 0 }; int sum = 0 ; int Count = 0x41 ; int i = 0 ; printf ("请输入 14 位的 key:" ); scanf ("%s" , inputBuffer); do { inputBuffer[i] ^= Count; sum += inputBuffer[i]; Count++; i++; } while (Count < 0x4F || inputBuffer[i] != 0 ); sum ^= 0x12345678 ; printf ("CRACKME3.KEY 文件的内容是:%s\n末尾4位是:0x%x\n" , inputBuffer,sum); return 0 ; }
补充 上面的程序执行的流程之分析了一半,当我重新调试一遍后,发现之前分析程序流程有问题。下面重新补充并纠正。
调用 GetModuleHandle 获取 EXE 进程的基地址
调用 CreateFile 读取文件
调用 ReadFile 读取文件内容
比较读取数据的长度(数据长度必须是18个字节)
处理前 14 个数据,并把结果存储到 sum( [0x004020F9] ) 中
判断 sum 和 eax(后4字节)数据是否相等
如果不相等,push eax
再调用 call 0x004012F5
函数在参数(0x40210E) “CrackMe v3.0” 后面插入 “-Uncracked”; 如果相等就 push eax
此时 al 为 1
注意: 这里的 0x004012F5 函数并没有做堆栈平衡, 也就是说,如果key错误,栈顶值就是 0x40210E; 如果key正确,栈顶的值就是 0x12345401
调用 FindWindow 寻找包含字符串 “No need to disasm the code!” 的窗口,如果没有值为0(NULL),如果有返回窗口句柄。
调用 LoadIcon 和 LoadCursor 加载图标和光标
调用 RegisterClass 注册一个包含字符串 “No need to disasm the code!” 的窗口
调用 CreateWindowEx 创建包含字符串 “CrackMe v3.0 -Uncracked” 的窗口
调用 ShowWindow 显示窗口
调用 UpdateWindow 更新窗口,如果更新成功,返回值为1,如果更新失败,返回值为0
0x40210E 处的结果与 1 比较
这里的 pop eax 是来自函数 call 0x004012F5,这个函数中未使用堆栈平衡
最后这一段使用消息机制,等待用户对窗口进行操作
最后在窗口标题就是字符串 “CrackMe v3.0 -Uncracked”
如果对上面创建窗口的函数熟悉的话,一看就知道 字符串 “CrackMe v3.0 -Uncracked” 是窗口标题
004-Acid Bytes.2 查看程序基本信息 使用PE工具,查看这是个32位可执行程序,且加了 UPX 壳
脱壳
重新查看程序信息
从上面的截图来看,程序使用的是 Delphi 语言,并且确定了程序的错误信息。
使用 IDR 加载脱壳程序,并进行分析 程序执行流程
使用 GetText 获取窗口数据
使用 StrCmp 判断窗口数据(serial)是不是 “12011982”
如果是就输出破解成功的消息框;如果serial为空,就输出消息框提示输入内容;如果不是就输出破解失败的消息框
从上图中可以很清晰地判断出 serial 的值就是 “12011982”
005-Andrnalin.1 查看程序基本信息
程序分析
F9运行程序到程序领空
在当前模块搜索字符串
双击字符串定位到相应指令处
向上查找,看看有没有可疑的函数
在strcmp指令处下断点
向 strcmp 函数后面查找第一个跳转指令
根据跳转指令后面的字符串和call,可以判断这一块是正确破解后的输出,这里跳转指令是关键跳转,我么在这里下断点。
调试程序
这里主要在就是看这两条指令 neg edi
和sbb edi,edi
这两条指令:
当 edi = -1 时,执行neg指令后,edi=1,CF标志位为1;执行 sbb 指令 edi=edi-edi-CF位=-1
当 edi = 0 时,执行neg指令后,edi=0,CF标志位为0;执行 sbb 指令 edi=edi-edi-CF位=0
指令复习:neg指令,求补码指令,对操作数执行求补运算:用零减操作数,然后结果返回到操作数。neg指令对CF标志位有影响。
后面又执行了两条指令:inc edi
和neg edi
当 edi=-1 时,执行指令后 edi=0
当 edi=0 时,执行指令后 edi=1
当 di=si=0 时,破解失败;当 di!=si时,破解成功。(esi 在之前就已经置为0了)
验证 key(SynTaX 2oo1)
006-AD_CM#2 查看程序基本信息
静态分析
该程序是用汇编写的,所以没有main函数。打开程序后,双击可疑函数 sub_4010FC,发现这个就是要分析的主函数。
从上图的静态分析可以得到结果: serial(byte_403280) = name(string) - cl
cl 的值会根据 loop 循环,进行减1的操作。
最终计算结果: name=9unkk serial=4qkij
1 2 3 4 5 9-5=4 u-4=q n-3=k k-2=i k-1=j
编写注册机 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 #define _CRT_SECURE_NO_DEPRECATE #include <stdio.h> #include <stdlib.h> int main (void ) { char name[10 ] = { 0 }; char serial[10 ] = { 0 }; int len = 0 ; printf ("请输入Name(大于等于5个字符):" ); scanf ("%s" , name); for (int i = 0 ; name[i] != 0 ; i++) { len++; } int a = len; printf ("您的serial是:" ); for (int i = 0 ,j=len; i < len; i++,j--) { serial[i] = name[i] - j; printf ("%c" , serial[i]); } return 0 ; }
007-Reg 查看程序基本信息
静态分析
搜索字符串,找到主程序的
通过IDA静态分析并没有找到中文字符串,这是因为IDA中文字符串搜索功能有点小问题,同时对于程序运行过程中产生的字符串是无法搜索出来的。
通过动态调试搜索字符串
在IDA中手动定位到字符串位置
按空格键查看程序流程图
经过多次手动调试之后发现,这一块的功能(TFmReg_btnOKClick)就是把 “UserName=9unk” 和 “SN=123456” 写入到 reg.dll 中,并没有验硬编码的功能。也就是说这个程序是通过读取 reg.dll 这个文件中的 “UserName” 和 “SN” 来验证硬编码的。
重新观察程序,发现左下角写着 “未注册”
重新在搜索字符串,并在 “提升栈顶” 处设置断点。
在 IDA 中定位到 “0x45D4A9” 位置中,静态程序。
复制字符串 “您的有效期” 和 “未注册” 到局部变量中,之后再查找 reg.dll 文件,如果该文件不存在就结束程序。
获取窗口 “UserName” 和 “SN” 窗口的值。
根据下面的 call 函数中使用了 “UserName” 和 “SN” 作为参数,这应该是关键函数。
delphi 逆向中的函数:
LStrLAsg 函数:将 EDX 指向的地址内容复制到EAX指向的地址中去。
FileExists 函数:查找文件是否存在,EAX作为入口点;返回值:al==”True” 或 “False”。
GetValue 函数:返回属性的串值,缺省时返回’(unknown)’,这应该被重载以返回适当的值。
StrCat(Dest: PChar; const Source: PChar) 函数:将参数source中的字符添加到Dest字符串的尾部,Dest缓冲区中必须有足够的空间。其中,Dest表示目的缓冲区;Source表示源缓冲区。
strcopy(Dest: PChar; const Source: PChar):将参数Source中的字符复制Dest字符的尾部,Dest缓冲区中必须有足够的空间。
strtoint 函数:将字符串转换为整数
strclr 函数:引用计数为0了,释放字符串
动态调试
从这里可以发现从 0x412500 地址往后存储的可能是自定义函数的数组指针。后面的这些函数比较多不必要一个个函数去分析,只要了解大概就行
关键函数暂时先跳过。在后面修改跳转指令,F9 运行程序,看看是否能正确注册程序。
可以看到,程序正常运行。后续就是分析这个关键函数再做什么操作。
经过第一轮分析可得出如下结果,修改 reg.dll 文件,重新调试程序。
循环计算 serial 中的每个字符,如果字符 (x + 0xD0(存储大小为字节) ) < 0xA 就会继续循环计算,如果不符合第一个要求 (上一个计算结果 + 0xF9) >= 6 跳出该子程序。
补充:这一段循环表示 ”输入的 SN 必须在 09 和 AF 之间“。
计算程序激活时间的函数
执行下一个函数后看到有一串16位的字符,这可能是正确的serial
继续调试,发现出现了一串新的字符,这很像 MD5 加密,我们尝试使用加密算法验证一下。
发现对输入的 serial 也进行了两次 MD5 加密,最后在对两个字符串进行比较
最终输入前面的serial,程序正确激活
算法分析:函数0x0045C5E0
入口参数:ECX=”正确的SN”,EDX=”激活日期期限”,EAX=”UserName”
第一步将 “11”、”05”、”12” 转换为二进制字符串拼接为 “0001011010101100”
第二步:将“0001011010101100” 经过一系列操作变为 “01010110”,再转换为十六进制0x56
第三步:将“0001011010101100” 经过一系列操作变为 “00110100”,再转换为十六进制0x34
第二步和第三步不是重点可以忽略,只要知道大概在进行什么操作就行。
“3456”+”MD5(9unk)” 通过函数 0x45C244 计算得出 “0x4E”
拼接为 “34564E”,再进行 MD5 加密,再取其中第8个和第9个字符 “59”
MD5(9unk)+MD5(MD5(9unk)+MD5(110512)) 通过函数 0x45C244 计算得出 “0xD4”
MD5(110512)+MD5(MD5(9unk)+MD5(110512)) 通过函数 0x45C244 计算得出 “0xD8”
取 MD5(MD5(9unk)+MD5(110512)) 第7个字节,第14个字节,第23个字节,第11个字节
字节从0开始算,所以按字符个数算少了一个。
从下面的压入堆栈的值可以看出来这就是正确的 “SN”
函数 0x45C244 分析 call 0x45C244 将参数转换为字符串:入口参数 EAX=”3456”+”MD5(9unk)”
判断参数是否有错误
字符串转换为十六进制、通过函数 0x0045C09C 算出 “0x4E”、最后清空函数 0x45C244 使用的局部变量 [ebp-0x10C]
函数 0x0045C09C 分析 call 0x0045C09C 函数:入口参数 EAX=十六进制MD5值内存地址,ecx=参数长度,EDX=出口参数。
函数算法,使用内外两层循环进行进算,详细算法如下:
外循环
带入第n个字节,一共循环18次
内循环
循环将字节逻辑右移8次
每次循环得到的结果放入 cl
cl xor dl
cl and 1
如果 cl 不为0,dl=dl xor 0x18,edx=edx shr 1,dl or 0x80,eax = eax shr 1
如果 cl 为 0 ,edx=edx shr 1,eax = eax shr 1
总结 函数0x0045C5E0算法: 按顺序存储第5步得出的结果;存储固定值 “3456”,因为激活日期是固定的 ;存储第6步结果;存储第9步第8个字符;存储第9步第15个字符;存储第7步结果;存储第9步第24个字符;存储第9步第11个字符;存储第6步结果
编写注册机 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 #define _CRT_SECURE_NO_DEPRECATE #pragma warning (disable:4996) #pragma comment(lib,"libssl.lib" ) #pragma comment(lib,"libcrypto.lib" ) #include <stdio.h> #include <string.h> #include <openssl/md5.h> void HexToStr (unsigned char * md, char * result,int start,int len) { for (size_t i = start; i <= len; i++) { sprintf (result + i * 2 , "%02x" , md[i]); } } void xormd5 (char a[], unsigned char dest[],int len) { char num[1 ] = { 0 }; int result[1 ] = { 0 }; int x = 0 ; for (int j = 0 ;j < len; j++) { num[0 ] = a[j]; for (int i = 0 ; i < 8 ; i++) { x = num[0 ]; x ^= result[0 ]; x &= 1 ; if (x != 0 ) { result[0 ] ^= 0x18 ; result[0 ] >>= 1 ; result[0 ] |= 0x80 ; num[0 ] >>= 1 ; } else { result[0 ] = result[0 ] >> 1 ; num[0 ] = num[0 ] >> 1 ; } } dest[0 ] = result[0 ]; } } int main (int argc,char * argv[]) { unsigned char md[32 ] = { 0x34 ,0x56 }; char mds[65 ] = { 0 }; char mds1[33 ] = { 0 }; unsigned char md1[2 ] = { 0 }; char name[20 ] = {0 }; char next1[6 ] = { 0 }; char result[18 ] = { 0 }; int len=0 ; MD5_CTX c; printf ("请输入用户名:" ); scanf ("%s" , &name); len=strlen (name); MD5_Init(&c); MD5_Update(&c, name, len); MD5_Final(md+2 , &c); xormd5(md,md1,18 ); HexToStr(md1, result,0 ,2 ); for (int i = 0 ; i < 6 ; i++) { if (result[i] >= 0x61 ) { result[i] -= 0x20 ; } } int x = 0x33 ; for (int i = 2 ; i < 6 ; i++,x++) { result[i] = x; next1[i - 2 ] = x; } next1[4 ] = result[0 ]; next1[5 ] = result[1 ]; MD5_Init(&c); MD5_Update(&c, next1, 6 ); MD5_Final(md, &c); md1[0 ] = md[3 ]; md1[1 ] = md[4 ]; HexToStr(md1, &result[6 ], 0 , 4 ); result[6 ] = result[7 ]; result[7 ] = result[8 ]; MD5_Init(&c); MD5_Update(&c, name, len); MD5_Final(md, &c); HexToStr(md, mds, 0 , 15 ); MD5_Init(&c); MD5_Update(&c, "110512" , 6 ); MD5_Final(md, &c); HexToStr(md, mds+32 , 0 , 15 ); MD5_Init(&c); MD5_Update(&c, mds, 64 ); MD5_Final(md+16 , &c); HexToStr(md+16 , mds1, 0 , 15 ); result[8 ] = mds1[7 ]; result[9 ] = mds1[14 ]; xormd5(md, md1,32 ); HexToStr(md1, result+14 , 0 , 0 ); MD5_Init(&c); MD5_Update(&c, name, len); MD5_Final(md, &c); xormd5(md, md1, 32 ); HexToStr(md1, result + 10 , 0 , 0 ); result[12 ] = mds1[23 ]; result[13 ] = mds1[11 ]; printf ("SN = " ); for (int i = 0 ; i < 17 ; i++) { if (result[i] >= 0x61 ) { result[i] -= 0x20 ; } printf ("%c" , result[i]); } printf ("\n" ); return 0 ; }
008-Afkayas.1 查看程序基本信息
静态分析
搜索字符串,没有找到相关有用的字符串
查看函数窗口,找到了三个自定义函数,依次点开查看,发现 sub_402191 函数和可能是关键函数。
浏览 sub_402191 函数
确认函数关键函数后,在 strcmp 函数处,按空格,找到偏移地址
动态调试
使用 ctrl+G 定位到 IDA 找到的偏移地址,并设置断点。
运行程序,断点后找到关键的 serial
验证 “AKA-390181” 是否为正确的serial
找到关键算法,从strcmp向上找第一个,且与 [ebp-0x1C]
或 [ebp-0x18]
的函数。
设置断点,动态调试看看,该函数不属于程序领空不是算法函数
继续向下调试,确认 [ebp-0x1C]
存储的是serial
重新从函数开头找到与 [ebp-0x1C]
相关的函数,进行一步一步调试,最终在函数 vbastrI4 位置处返回了serial字符串
向上看还发现了两个函数:
vbaLenBstr 获得一个字符串的长度
rtcAnsiValueBstr —>传回字符码(返回第一个字符的字符代码)
重新调试程序,并找到关键算法
算法: “AKA-“+(name长度*0x17CFB)+name[0]
编写注册机 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 #define _CRT_SECURE_NO_DEPRECATE #pragma warning (disable:4996) #pragma comment(lib,"libssl.lib" ) #pragma comment(lib,"libcrypto.lib" ) #include <stdio.h> #include <string.h> #include <openssl/md5.h> int main (int argc,char * argv[]) { char name[20 ] = { 0 }; int key = 0 ; printf ("请输入用户名:" ); scanf ("%s" , name); printf ("serial = AKA-%d" , strlen (name) * 0x17CFB + name[0 ]); return 0 ; }
VBA逆向常用函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 VB程序逆向常用的函数 1) 数据类型转换: a)__vbaI2Str 将一个字符串转为8 位(1个字节)的数值形式(范围在 0 至 255 之间) 或2 个字节的数值形式(范围在 -32,768 到 32,767 之间)。 b)__vbaI4Str(vbastrI4) 将一个字符串转为长整型(4个字节)的数值形式(范围从-2,147,483,6482,147,483,647) c)__vbar4Str 将一个字符串转为单精度单精度浮点型(4个字节)的数值形式 d)__vbar8Str 将一个字符串转为双精度单精度浮点型(8个字节)的数值形式 e) VarCyFromStr (仅VB6库. 要调试,则在WINICE.DAT里必须有 OLEAUT32.DLL)字符串到变比型数据类型 f) VarBstrFromI2 (仅VB6库. 要调试,则在WINICE.DAT里必须有 OLEAUT32.DLL)整型数据到字符串: 2) 数据移动: a) __vbaStrCopy 将一个字符串拷贝到内存,类似于 Windows API HMEMCPY b) __vbaVarCopy 将一个变量值串拷贝到内存 c) __vbaVarMove 变量在内存中移动,或将一个变量值串拷贝到内存 3) 数学运算: a) __vbavaradd 两个变量值相加 b) __vbavarsub 第一个变量减去第二个变量 c) __vbavarmul 两个变量值相乘 d) __vbavaridiv 第一个变量除以第二个变量,得到一个整数商 e) __vbavarxor 两个变量值做异或运算 4) 程序设计杂项: a) __vbavarfornext 这是VB程序里的循环结构, For... Next... (Loop) b) __vbafreestr 释放出字符串所占的内存,也就是把内存某个位置的字符串给抹掉 c) __vbafreeobj 释放出VB一个对象(一个窗口,一个对话框)所占的内存,也就是把内存某个位置的一个窗口,一个对话框抹掉 d) __vbastrvarval 从字符串特点位置上获取其值 e) multibytetowidechar 将数据转换为宽字符格式,VB在处理数据之都要这样做,在TRW2000显示为7.8.7.8.7.8.7.8 f) rtcMsgBox 调用一个消息框,类似于WINDOWS里的messagebox/a/exa,此之前一定有个PUSH命令将要在消息框中显示的数据压入椎栈 g) __vbavarcat 将两个变量值相连,如果是两个字符串,就连在一起 h) __vbafreevar 释放出变量所占的内存,也就是把内存某个位置的变量给抹掉 i) __vbaobjset j) __vbaLenBstr 获得一个字符串的长度,注:VB中一个汉字的长度也为1 k) rtcInputBox 显示一个VB标准的输入窗口,类似window's API getwindowtext/a, GetDlgItemtext/a l) __vbaNew 调用显示一个对话框,类似 Windows' API Dialogbox m) __vbaNew2 调用显示一个对话框,类似 Windows' API Dialogboxparam/a n) rtcTrimBstr 将字串左右两边的空格去掉 5) 比较函数 a) __vbastrcomp 比较两个字符串,类似于 Window's API lstrcmp b) __vbastrcmp 比较两个字符串,类似于 Window's API lstrcmp c) __vbavartsteq 比较两个变量值是否相等 d)__vbaFpCmpCy - Compares Floating point to currency. sp; Compares Floating point to currency 6) 在动态跟踪,分析算法时,尤其要注意的函数: rtcMidCharVar 从字符串中取相应字符,VB中的MID函数,用法MID("字符串","开始的位置","取几个字符") rtcLeftCharVar 从字符串左边取相应字符,VB中的用法:left("字符串","从左边开始取几个字符") rtcRightCharVar 从字符串右边取相应字符,VB中的用法:Right("字符串","从右边开始取几个字符") __vbaStrCat 用字符串的操作,就是将两个字符串合起来,在VB中只有一个&或+ __vbaStrCmp 字符串比较,在VB中只有一个=或<> ASC()函数 取一个字符的ASC值,在反汇编时,还是有的movsx 操作数 7) 在函数中的缩写: bool 布尔型数据(TRUE 或 FALSE) str 字符串型数据 STRING i2 字节型数据或双字节整型数据 BYTE or Integer ui2 无符号双字节整型数据 i4 长整型数据(4字节) Long r4 单精度浮点型数据(4字节) Single r8 双精度浮点型数据(8字节) Double cy (8 个字节)整型的数值形式 Currency var 变量 Variant fp 浮点数据类型 Float Point cmp 比较 compare comp 比较 compare 8) Btw: __vbavartsteq系列的还有__vbavartstne 不等于 __vbavartstGe,__vbavartstGt,__vbavartstLe,__vbavartstLt等,比较大于或小于 8) 拦截警告声: bpx rtcBeep —>扬声器提示 9) 数据移动: bpx vbaVarCopy —>数据移动将一个变量值串拷贝到内存 bpx vbaVarMove —>数据移动变量在内存中移动,或将一个变量值串拷贝到内存 bpx vbaStrMove —>移动字符串 bpx vbaStrCopy —>移动字符串 将一个字符串拷贝到内存,类似于 Windows API HMEMCPY 10) 数据类型转换: bpx vbaI2Str —>将一个字符串转为8 位(1个字节)的数值形式(范围在 0 至 255 之间) 或2 个字节的数值形式(范围在 -32,768 到 32,767 之间)。 bpx vbaI4Str —>将一个字符串转为长整型(4个字节)的数值形式(范围从-2,147,483,6482,147,483,647) bpx vbar4Str —>将一个字符串转为单精度单精度浮点型(4个字节)的数值形式 bpx vbar8Str —>将一个字符串转为双精度单精度浮点型(8个字节)的数值形式 bpx VarCyFromStr —>(仅VB6库. 要调试,则在WINICE.DAT里必须有 OLEAUT32.DLL)字符串到变比型数据类型 bpx VarBstrFromI2 —>(仅VB6库. 要调试,则在WINICE.DAT里必须有 OLEAUT32.DLL)整型数据到字符串: 11) 数值运算: bpx vbaVarAdd —>两个变量值相加 bpx vbaVarIdiv —>除整,第一个变量除以第二个变量,得到一个整数商 bpx vbaVarSub —>第一个变量减去第二个变量 bpx vbaVarMul —>两个变量值相乘 bpx vbaVarDiv —>除 bpx vbaVarMod —>求余 bpx vbaVarNeg —>取负 bpx vbaVarPow —>指数 bpx vbavarxor —>两个变量值做异或运算 12) 针对变量: bpx vbaVarCompEq —>比较局部变量是否相等 bpx vbaVarCompNe —>比较局部变量是否不等于 bpx vbaVarCompLe —>比较局部变量小于或等于 bpx vbaVarCompLt —>比较局部变量小于 bpx vbaVarCompGe —>比较局部变量大于或等于 bpx vbaVarCompGt —>比较局部变量大于 13) 程序结构: bpx vbaVarForInit —>重复执行初始化 bpx vbaVarForNext —>重复执行循环结构, For... Next... (Loop) 14) 比较函数: bpx vbaStrCmp —>比较字符串是否相等 ****** bpx vbaStrComp —>比较字符串是否相等 ****** bpx vbaVarTstEq —>检验指定变量是否相等 bpx vbaVarTstNe —>检验指定变量是否不相等 bpx vbaVarTstGt —>检验指定变量大于 bpx vbaVarTstGe —>检验指定变量大于或等于 bpx vbaVarTstLt —>检验指定变量小于 bpx vbaVarTstLe —>检验指定变量小于或等于 15) 字符串操作: bpx vbaStrCat —>用字符串的操作,就是将两个字符串合起来,在VB中只有一个&或+ bpx vbaStrLike bpx vbaStrTextComp —>与指定文本字符串比较 bpx vbaStrTextLike bpx vbaLenBstr —>字符串长度 bpx vbaLenBstrB —>字符串长度 bpx vbaLenVar —>字符串长度 bpx vbaLenVarB —>字符串长度 bpx rtcLeftCharVar —>截取字符串,从字符串左边取相应字符,VB中的用法:left("字符串","从左边开始取几个字符") bpx vbaI4Var —>截取字符串 bpx rtcRightCharVar —>截取字符串,从字符串右边取相应字符,VB中的用法:Right("字符串","从右边开始取几个字符") bpx rtcMidCharVar —>截取字符串,VB中的MID函数,用法MID("字符串","开始的位置","取几个字符") bpx vbaInStr —>查找字符串位置 bpx vbaInStrB —>查找字节位置 bpx vbaStrCopy —>复制字符串 bpx vbaStrMove —>移动字符串 bpx rtcLeftTrimVar —>删除字串的空白 bpx rtcRightTrimVar —>删除字串的空白 bpx rtcTrimVar —>删除字串的空白 bpx vbaRsetFixstrFree —>字符串往右对齐 bpx vbaRsetFixstr —>字符串往右对齐 bpx vbaLsetFixstrFree —>字符串往左对齐 bpx vbaLsetFixstr —>字符串往左对齐 bpx vbaStrComp —>字符串比较 bpx vbaStrCompVar —>字符串比较 bpx rtcStrConvVar2 —>字符串类型转换 bpx rtcR8ValFromBstr —>把字符串转换成浮点数 bpx MultiByteToWideChar —>ANSI字符串转换成Unicode字符串 bpx WideCharToMultiByte —>Unicode字符串转换成ANSI字符串 bpx rtcVarFromFormatVar —>格式化字符串 bpx rtcUpperCaseVar —>小写变大写 bpx rtcLowerCaseVar —>大写变小写 bpx rtcStringVar —>重复字符 bpx rtcSpaceVar —>指定数目空格 bpx rtcAnsiValueBstr —>传回字符码(返回第一个字符的字符代码) bpx rtcByteValueBstr —>传回字符码(返回第一个字节的字符代码) bpx rtcCharValueBstr —>传回字符码(返回第一个Unicode字符代码) bpx rtcVarBstrFromAnsi —>传回字符(返回 String,其中包含有与指定的字符代码相关的字符 ) bpx rtcVarBstrFromByte —>传回字符(返回 String,其中包含有与指定的字符代码相关的单字节) bpx rtcVarBstrFromChar —>传回字符(返回 String,其中包含有与指定Unicode 的 String) 16)其他函数: vbaHresultCheckObj 对某个控件进行控件校验。 VbaGenerateBoundsError 数组下标越界 VbaErrorOverflow 错误 溢出 vbaExceptHandler,VB的万能断点,VB在每个过程的开始都要安装一个线程异常处理过程。
参考:VB程序逆向常用的函数
009-keygenme1 查看程序基本信息
运行程序后,并没有看到任何提示信息。
在窗口左下角分别有三个按钮 “check”、”about”、”exit”,点第一个按钮会出现如下错误。
静态分析
使用IDA分析程序,点开自定义函数,并没有找到任何有用信息
搜索字符串,发现关键字符串
双击,定位到字符串位置处
快捷键 ctrl+x 定位到代码位置
向上翻,找到 strcmp 函数,这很有可能是比较 serial 的
动态调试
在IDA查询的 strcmp 位置处下断点,并运行
验证是否为正确的serial
算法分析
手动调试分析算法
编写注册机 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 #define _CRT_SECURE_NO_DEPRECATE #pragma warning (disable:4996) #pragma comment(lib,"libssl.lib" ) #pragma comment(lib,"libcrypto.lib" ) #include <stdio.h> #include <string.h> #include <openssl/md5.h> int main (int argc,char * argv[]) { char name[20 ] = { 0 }; int x = 0 ; int y = 0 ; printf ("请输入用户名:" ); scanf ("%s" , name); for (int i = 0 ; i < strlen (name); i++) { x -= name[i]-0x19 ; } y = x * x * x; printf ("serial = Bon-%X-%X-41720F48" , x,y); return 0 ; }
010-ceycey(UPX壳) 查看程序基本信息
程序有 upx 壳(压缩壳)
点击check,并未返回错误信息
脱壳基础 脱壳基础
手脱压缩壳 压缩壳 压缩壳的工作过程包括以下步骤:
压缩PE文件:使用数据压缩算法对PE文件进行处理,生成一个新的压缩PE文件。
解压内存中的数据:在执行程序时,壳内部解压缩代码启动,并在内存中创建一个新的节空间。
映射节数据:将之前压缩的节数据解压缩并映射回原来内存的位置,使程序能够在内存中正常运行。
保证内存内容一致:确保解压缩后的PE文件在内存中的内容与未压缩前的正常PE文件完全相同,以维持程序的行为一致性和功能的完整性。
简单来说就是将硬盘上的exe文件进行压缩,当运行程序时,会先解压缩exe并在内存中创建新的空间,再将解压缩数据放到刚才新建的内存中,最后再验证数据的完整性。执行完解压缩和验证过程后,程序会跳转到程序正常运行的入口点。
脱壳过程就是找到最后程序正常运行的入口点,然后再将改内存块数据导出另存。
脱壳 esp 定律脱壳(通常用于压缩壳): 一般加壳程序(压缩壳)在运行时,会先执行壳代码,然后在内存中恢复还原原程序,再跳转到原始OEP,执行原程序的代码。这些壳代码首先会使用 PUSHAD 指令保存寄存器环境,在解密各个区段完毕,跳往 OEP 之前会使用 POPAD 指令恢复寄存器环境。
F8,执行 PUSHAD 指令
右键esp,设置硬件访问断点(在指令重新访问 12FFC4 后,会断点停下来)
F9 运行程序,此时程序会停在 POPAD 下面
F8 运行程序,跳转到OEP,程序入口点位置处
可以看到有一堆乱码,这是因为OD自动分析指令,而这些指令因压缩无法解析,所以变成乱码。解决方法就是删除OD的分析,将指令还原。
使用 ollydbg 脱壳进程,进行脱壳,并另存文件
验证脱壳是否成功
静态分析
程序运行中没有弹出任何提示,但还是按流程查找可疑字符串进行测试。
发现可疑字符串,双击定位到字符串,然后用 ctrl+x 定位到程序,并在上方找到可疑的 strcmp 函数
动态调试
动态调试后发现关键指令就是 strcmp 位置。
继续回到IDA分析,发现这只是一个字符串比较,没有算法。
验证注册码(ULTRADMA……………………………………………………)
011-wocy.1 查看程序基本信息
运行程序后,发现有个可疑的 Unregister
静态分析
使用IDA搜索字符串
找到相关字符串,但是这两个字符串在两个不同的函数中
分别看看这两个个函数中有没有比较字符串相关的函数
在 401600 函数中发现了一个宽字符函数 _mbscmp
动调试
在_mbscmp 函数位置处设置断点,看看这是不是我们需要的 id 值
F9 运行程序
验证是否为正确 id
寻找与 [esp+0x4] 相关的指令,设置断点,找到关键算法
查找 MakeReverse 函数相关信息
编写注册机 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 #define _CRT_SECURE_NO_DEPRECATE #pragma warning (disable:4996) #pragma comment(lib,"libssl.lib" ) #pragma comment(lib,"libcrypto.lib" ) #include <stdio.h> #include <string.h> #include <openssl/md5.h> int main (int argc,char * argv[]) { char name[20 ] = { 0 }; int x = 0 ; int y = 0 ; names: printf ("请输入用户名(长度不小于4):" ); scanf ("%s" , name); printf ("id = " ); for (int i = strlen (name); i >=0 ; i--) { if (strlen (name) < 4 ) { goto names; } printf ("%c" , name[i]); } printf ("\n" ); return 0 ; }
012-crcme1 查看程序基本信息
程序没有加壳
点击 sacan 分析程序
打开程序,查看功能和信息,运行程序后发现错误弹窗
点击旁边的info按钮,也发现一些信息,能大概猜出一些信息
静态分析 分支一
使用IDA搜索字符串,发现IDA可以正常解析这些字符串(使用的IDA8.3),大概能分析出这个程序有两种解法。一种是通过输入name和serial,另一种是通过读取KEYFILE。
搜索对应的字符串,定位到代码位置,发现程序有三个输出结果
向上溯源,找到适合动态调试的断点位置
分支二
定位到字符串 ACG.KEY
定位到指令位置
动态调试 分支一
设置断点,动态调试
分支二
打开 ACG.key 文件失败
文件内容要 12 个字符
任意输入12个字符后继续调试程序
算法分析 算法一
累计name值,左移3位,异或 0x51A5
serial值 异或 0x87CA
name 结果 + serial 结果 = 0x797E7
算法二
(key[0] xor 0x1B) rol 2 = 0x168
(key[1] xor 0x1B) rol 2 = 0x160
(key[2] xor 0x1B) rol 2 = 0x170
(key[3] xor 0x1B) rol 2 = 0xEC
(key[4] xor 0x1B) rol 2 = 0x13C
(key[5] xor 0x1B) rol 2 = 0x1CC
(key[6] xor 0x1B) rol 2 = 0x1F8
(key[7] xor 0x1B) rol 2 = 0xEC
(key[8] xor 0x1B) rol 2 = 0x164
(key[9] xor 0x1B) rol 2 = 0x1F8
(key[10] xor 0x1B) rol 2 = 0x1A0
(key[11] xor 0x1B) rol 2 = 0x1BC
去除 NAG 窗口 NAG 窗口指的是不断弹出的窗口。这个程序中我们每次关闭程序时都会弹出窗口提示编写程序的作者。
搜索字符串,定位到函数为位置,并动态调试
运行程序到断点位置,看到该函数没有修改堆栈,直接从堆栈中找到函数返回地址 0x4011F6
跳转到地址 0x4011F6 地址处,记录修改指令地址 0x4011EF。
通过手动修改exe文件去除 NAG
Image Base:表示程序在内存加载的基地址 Base Of Code:表示程序开始运行的位置,距离 Image Base 得到偏移量(即 程序入口点)。 File Offset:当PE文件储存在某个磁盘当中的时候,某个数据的位置相对于文件头的偏移量。
“Image Base” + “Base Of Code(Entry Point)” = 文件运行的代码在内存中的基地址(也可以理解为第一条指令在内存中的基地址)
“File Offset” 就表示文件在磁盘中(未运行)时,代码的相对的偏移地址(也可以理解为第一条指令在磁盘中的基地址)。
内存地址计算:内存中需要修改地址 - 内存中的入口点地址 = 0x4011EF - 0x401000 = 0x1EF
磁盘地址计算:文件偏移地址 + 0x1EF = 0x600 + 0x1F6 = 0x7EF(磁盘中需要修改的地址)
jmp的硬编码是 0xEB,复制文件并在 0x7EF 偏移处的 0x75 改为 0xEB,并保存。
最后验证程序,发现 NAG 窗口已去除。
编写注册机 注册机一 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 #define _CRT_SECURE_NO_DEPRECATE #pragma warning (disable:4996) #pragma comment(lib,"libssl.lib" ) #pragma comment(lib,"libcrypto.lib" ) #include <stdio.h> #include <string.h> #include <openssl/md5.h> int main (int argc,char * argv[]) { char name[20 ] = { 0 }; int x = 0 ; int y = 0 ; int serial=0 ; printf ("请输入用户名(长度大于4):" ); scanf ("%s" , name); printf ("serial = " ); for (int i = 0 ; i < strlen (name); i++) { x += name[i]; } x <<= 3 ; x ^= 0x515A5 ; y = 0x797E7 - x; serial = y ^ 0x87CA ; printf ("%d\n" ,serial); return 0 ; }
注册机二 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 #define _CRT_SECURE_NO_DEPRECATE #pragma warning (disable:4996) #pragma comment(lib,"libssl.lib" ) #pragma comment(lib,"libcrypto.lib" ) #include <stdio.h> #include <string.h> #include <openssl/md5.h> int main (int argc,char * argv[]) { int key[12 ] = { 0 }; key[0 ] = (0x168 >> 2 ) ^ 0x1B ; key[1 ] = (0x160 >> 2 ) ^ 0x1B ; key[2 ] = (0x170 >> 2 ) ^ 0x1B ; key[3 ] = (0xEC >> 2 ) ^ 0x1B ; key[4 ] = (0x13C >> 2 ) ^ 0x1B ; key[5 ] = (0x1CC >> 2 ) ^ 0x1B ; key[6 ] = (0x1F8 >> 2 ) ^ 0x1B ; key[7 ] = (0xEC >> 2 ) ^ 0x1B ; key[8 ] = (0x164 >> 2 ) ^ 0x1B ; key[9 ] = (0x1F8 >> 2 ) ^ 0x1B ; key[10 ] = (0x1A0 >> 2 ) ^ 0x1B ; key[11 ] = (0x1BC >> 2 ) ^ 0x1B ; printf ("ACG.key = " ); for (int i = 0 ; i < 12 ; i++) { printf ("%c" , key[i]); } return 0 ; }
013-Acid_burn 查看程序基本信息
程序没有夹克,使用的是 Delphi 语言编写的
静态分析 分支一
搜搜字符串
定位到 “Try Agsin!!”,定位到关键跳转位置
分支二
通过观察流程图,确认两个关键跳转的位置
动态调试 分支一
动态调试发现函数 “CALL 004039FC” 的返回值,和入口参数,猜测这是比较函数
输入另一个参数字符串 “Hello Dude!” 进行测试
分支二
在两个关键跳转位置处设置断点
第一个断点判断name长度
同样是使用函数 “CALL 004039FC” 比较字符串
动态调试,找到关键算法,这是将 (name[0] * 0x29) * 2 的值转换为十进制。
逆向过程中先别急着分析关键函数,尽量先查看入口参数和结果的关系,然后再重新验证,就能确认函数功能。闷着头分析算法会浪费很多时间。
编写注册机 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #define _CRT_SECURE_NO_DEPRECATE #pragma warning (disable:4996) #pragma comment(lib,"libssl.lib" ) #pragma comment(lib,"libcrypto.lib" ) #include <stdio.h> #include <string.h> #include <openssl/md5.h> int main (int argc,char * argv[]) { char name[20 ] = { 0 }; printf ("请输入用户名:" ); scanf ("%s" , name); printf ("serial = CW-%d-CRACKED\n" , (name[0 ] * 0x29 ) * 2 ); return 0 ; }
014-Splish 查看程序基本信息
静态分析
硬编码流程图
验证硬编码
name 和 serial 流程图
动态调试
在关键跳转处,和 GetWindowsTextA 后面设置断点,并分析算法
算法
key = ((name[n] % 10) ^ n) + 2
if(key > 10);key = key - 10
key = serial[n] % 10
编写注册机 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 #define _CRT_SECURE_NO_DEPRECATE #pragma warning (disable:4996) #pragma comment(lib,"libssl.lib" ) #pragma comment(lib,"libcrypto.lib" ) #include <stdio.h> #include <string.h> #include <openssl/md5.h> int main (int argc, char * argv[]) { int key; char serial[20 ] = {0 }; char name[20 ] = { 0 }; printf ("请输入用户名:" ); scanf ("%s" , name); for (int i = 0 ,n = 0 ; i < strlen (name); i++) { key = ((name[i] % 10 ) ^ i) + 2 ; if (key > 10 ) { key -= 10 ; } for (int j = 0 , num = 0x30 ; j < 10 ; j++,num++) { if ((serial[n] == 0 )&&(num % 10 == key)&&(n==i)) { serial[n] = num; n++; } } } for (int k = 0 ; k < strlen (name); k++) { printf ("%c" , serial[k]); } return 0 ; }
015-Brad Soblesky.1 查看程序基本信息
静态分析
IDA搜索字符串找到关键函数
动态调试
在关键函数位置设置断点,并进行调试
验证 key = “”
经动态调试验证,发现这个就是一个常量字符串比较
IDA 中未搜索到字符串 ““ 是因为字符串设置问题,右键字符串窗口,点击 “setup” ,勾选 “Ignore instructions/data definitions”
Ignore instructions/data definitions(忽略指令/数据定义)。这个选项会使IDA扫描指令和现有数据定义中的字符串。使用这个选项,可以让IDA扫描二进制代码中错误地转换成指令的字符串,或扫描数据中非字符串格式。使用这个选项的效果类似于使用strings -a命令。
016-fty_crkme3 查看程序基本信息
脱upx壳
使用 esp 定律脱壳
静态分析
搜索字符串
定位到代码位置,分析程序
动态调试
动态调试后发现程序并在在关键位置断下来,说明在这之前已经有其他代码判断出输入的秘钥错误。
因为其中的代码有点多,只能从头开始分析其中的关键算法位置
经过第一轮的分析,找到serial写法规则
![15](https://github.com/9unkk/tuchuang/assets/25861639/ ad32ca8a-f420-4da2-9060-a756be5412b9)
重新输入秘钥 “12-345-67” ,并进行调试。分析出上半段指令的功能,找到了当serial长度为9时的关键跳转和函数
向上翻会发现有多处地方都用了这个关键函数且后面的指令都是一样的,我们依次设置好断点。一共发现了7个这样的函数,而我们 serial 中的数字也是7个。
重新调试分析关键函数算法
最终回到关键跳转位置,分析算法
最终再验证serial的长度为10的算法是否也是一样的
编写注册机 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 #define _CRT_SECURE_NO_DEPRECATE #pragma warning (disable:4996) #pragma comment(lib,"libssl.lib" ) #pragma comment(lib,"libcrypto.lib" ) #include <stdio.h> #include <string.h> #include <openssl/md5.h> int main (int argc, char * argv[]) { int num = 1 ; int num_x = 0 ; int num_y = 0 ; int cj; int cum; int ws; for (int i = 0 ; i < 999999999 ; i++,num++) { cum = 0 ; ws = 0 ; num_x = num; for (int j = 0 ; j < 9 &&num_x>0 ; j++) { num_x /= 10 ; ws++; } num_x = num; for (int k = 0 ; k < ws; k++) { cj = 1 ; num_y = num_x % 10 ; num_x /= 10 ; if (ws < 7 ) { ws = 7 ; } for (int m = 0 ; m < ws; m++) { cj *= num_y; } cum += cj; } if (num == cum&&ws==7 ) { printf ("%d%d-%d%d%d-%d%d\n" , num/1000000 ,(num/100000 )%10 , (num / 10000 )%10 , (num / 1000 )%10 , (num / 100 )%10 , (num / 10 ) % 10 ,num %10 ); } else if (num == cum && ws == 8 ) { printf ("%d%d-%d%d%d-%d%d%d\n" , num / 10000000 , (num / 1000000 ) % 10 , (num / 100000 ) % 10 , (num / 10000 ) % 10 , (num / 1000 ) % 10 , (num / 100 ) % 10 , (num / 10 ) % 10 ,num % 10 ); } else if (num == cum && ws == 9 ) { printf ("%d%d-%d%d%d-%d%d%d%d\n" , num / 100000000 , (num / 10000000 ) % 10 , (num / 1000000 ) % 10 , (num / 100000 ) % 10 , (num / 10000 ) % 10 , (num / 1000 ) % 10 , (num / 100 ) % 10 , (num / 10 ) % 10 , num % 10 ); } } return 0 ; }
017-Cabeca 查看程序基本信息
静态分析
找到关键跳转
因为IDA没有分析出具体函数名,我们只能先看最上面,将无用的指令跳过
记录想要调试指令的位置
动态调试
设置断点,并进行调试
对关键的函数进行断点调试
验证是否为正确的 serial
找到关键算法函数,分析发现这里面并没有对 0x42F714 和 0x42F718 赋值的指令,说明关键算法不在这
重新运行程序,看到 0x42F714 和 0x42F718 默认是 0 。我们在该地址处设置硬件写入(word)断点,之前设置的断点都删除掉。
因为最终结果大小是一个字,所以这里设置字大小的硬件断点
当我们输入两个字节后,程序断在如下位置,可以看到这一段都是 case 语句。
F8 继续执行程序,找到关键算法函数
删除设置的硬件断点,并在算法函数位置处设置断点算法分析
通过动态调试找到关键算法 cese 语句。
case语句在动态窗口看着有点累,我们在IDA中定位到该地址处,再按 F5 生成伪代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 int __fastcall TForm1_Edit1KeyPress (int result, int a2, _BYTE *a3) { switch ( *a3 ) { case 8 : sub_419E10(*(_DWORD *)(result + 480 ), 0 ); dword_42F714 = 0 ; result = 0 ; dword_42F718 = 0 ; break ; case 0x41 : dword_42F714 += 1064 ; dword_42F718 += 5648 ; break ; case 0x42 : dword_42F714 += 726576 ; dword_42F718 += 2 ; break ; case 0x43 : dword_42F714 += 3462 ; dword_42F718 += 9999 ; break ; case 0x44 : dword_42F714 += 4516 ; dword_42F718 += 74445628 ; break ; case 0x45 : dword_42F714 += 73482 ; dword_42F718 += 35644 ; break ; case 0x46 : dword_42F714 += 15554 ; dword_42F718 += 34328 ; break ; case 0x47 : dword_42F714 += 254376 ; dword_42F718 += 444444 ; break ; case 0x48 : dword_42F714 += 37348 ; dword_42F718 += 2615621 ; break ; case 0x49 : dword_42F714 += 27458 ; dword_42F718 += 3131331 ; break ; case 0x4A : dword_42F714 += 333476 ; dword_42F718 += 12121212 ; break ; case 0x4B : dword_42F714 += 275546 ; dword_42F718 += 71111 ; break ; case 0x4C : dword_42F714 += 1834457 ; dword_42F718 += 76628 ; break ; case 0x4D : dword_42F714 += 10349 ; dword_42F718 += 734348 ; break ; case 0x4E : dword_42F714 += 1025 ; dword_42F718 += 897376628 ; break ; case 0x4F : dword_42F714 += 1652 ; dword_42F718 += 3243223 ; break ; case 0x50 : dword_42F714 += 156 ; dword_42F718 += 8247348 ; break ; case 0x51 : dword_42F714 += 342 ; dword_42F718 += 236752 ; break ; case 0x52 : dword_42F714 += 34343 ; dword_42F718 += 783434 ; break ; case 0x53 : dword_42F714 += 7635344 ; dword_42F718 += 8734342 ; break ; case 0x54 : dword_42F714 += 42344 ; dword_42F718 += 78368 ; break ; case 0x55 : dword_42F714 += 87442 ; dword_42F718 += 12334 ; break ; case 0x56 : dword_42F714 += 7641 ; dword_42F718 += 7235 ; break ; case 0x57 : dword_42F714 += 15552 ; dword_42F718 += 323528 ; break ; case 0x58 : dword_42F714 += 9834 ; dword_42F718 += 732523528 ; break ; case 0x59 : dword_42F714 += 33553 ; dword_42F718 += 7238 ; break ; case 0x5A : dword_42F714 += 52763 ; dword_42F718 += 726628 ; break ; case 0x61 : dword_42F714 += 1063 ; dword_42F718 += 121 ; break ; case 0x62 : dword_42F714 += 1724 ; dword_42F718 += 111 ; break ; case 0x63 : dword_42F714 += 1169 ; dword_42F718 += 738 ; break ; case 0x64 : dword_42F714 += 18253 ; dword_42F718 += 762 ; break ; case 0x65 : dword_42F714 += 1024 ; dword_42F718 += 14 ; break ; case 0x66 : dword_42F714 += 1744 ; dword_42F718 += 13 ; break ; case 0x67 : dword_42F714 += 1661 ; dword_42F718 += 12 ; break ; case 0x68 : dword_42F714 += 1872 ; dword_42F718 += 11 ; break ; case 0x69 : dword_42F714 += 1084 ; dword_42F718 += 99 ; break ; case 0x6A : dword_42F714 += 1892 ; dword_42F718 += 888 ; break ; case 0x6B : dword_42F714 += 192 ; dword_42F718 += 77 ; break ; case 0x6C : dword_42F714 += 10109 ; dword_42F718 += 555 ; break ; case 0x6D : dword_42F714 += 2078 ; dword_42F718 += 90 ; break ; case 0x6E : dword_42F714 += 3591 ; dword_42F718 += 98 ; break ; case 0x6F : dword_42F714 += 142 ; dword_42F718 += 7468 ; break ; case 0x70 : dword_42F714 += 632432 ; dword_42F718 += 575475 ; break ; case 0x71 : dword_42F714 += 3415 ; dword_42F718 += 648 ; break ; case 0x72 : dword_42F714 += 24555 ; dword_42F718 += 538 ; break ; case 0x73 : dword_42F714 += 2224 ; ++dword_42F718; break ; case 0x74 : dword_42F714 += 1211 ; dword_42F718 += 64 ; break ; case 0x75 : dword_42F714 += 2242 ; dword_42F718 += 75 ; break ; case 0x76 : dword_42F714 += 7334 ; dword_42F718 += 78 ; break ; case 0x77 : dword_42F714 += 9502 ; dword_42F718 += 5 ; break ; case 0x78 : dword_42F714 += 917 ; dword_42F718 += 38 ; break ; case 0x79 : dword_42F714 += 11539 ; dword_42F718 += 8 ; break ; case 0x7A : dword_42F714 += 6400 ; dword_42F718 += 456 ; break ; default : return result; } return result; }
手动计算得出如下结果:1 2 3 4 5 6 7 8 9 name = "9unk" serial-1 = 6025 serial-2 = 250 计算过程: 39 = default(不进行任何计算) 75 = 2242 ; 75 6E = 2242+3591; 75+98 6B = 2242+3591+192;75+98+77
编写注册机 关键算法就是上面的伪代码,我们稍微修改优化一下就行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 #define _CRT_SECURE_NO_DEPRECATE #pragma warning (disable:4996) #pragma comment(lib,"libssl.lib" ) #pragma comment(lib,"libcrypto.lib" ) #include <stdio.h> #include <string.h> #include <openssl/md5.h> void __fastcall EditKeyPress (int * result1,int * result2,int a) { switch (a) { case 0x41 : *result1 += 1064 ; *result2 += 5648 ; break ; case 0x42 : *result1 += 726576 ; *result2 += 2 ; break ; case 0x43 : *result1 += 3462 ; *result2 += 9999 ; break ; case 0x44 : *result1 += 4516 ; *result2 += 74445628 ; break ; case 0x45 : *result1 += 73482 ; *result2 += 35644 ; break ; case 0x46 : *result1 += 15554 ; *result2 += 34328 ; break ; case 0x47 : *result1 += 254376 ; *result2 += 444444 ; break ; case 0x48 : *result1 += 37348 ; *result2 += 2615621 ; break ; case 0x49 : *result1 += 27458 ; *result2 += 3131331 ; break ; case 0x4A : *result1 += 333476 ; *result2 += 12121212 ; break ; case 0x4B : *result1 += 275546 ; *result2 += 71111 ; break ; case 0x4C : *result1 += 1834457 ; *result2 += 76628 ; break ; case 0x4D : *result1 += 10349 ; *result2 += 734348 ; break ; case 0x4E : *result1 += 1025 ; *result2 += 897376628 ; break ; case 0x4F : *result1 += 1652 ; *result2 += 3243223 ; break ; case 0x50 : *result1 += 156 ; *result2 += 8247348 ; break ; case 0x51 : *result1 += 342 ; *result2 += 236752 ; break ; case 0x52 : *result1 += 34343 ; *result2 += 783434 ; break ; case 0x53 : *result1 += 7635344 ; *result2 += 8734342 ; break ; case 0x54 : *result1 += 42344 ; *result2 += 78368 ; break ; case 0x55 : *result1 += 87442 ; *result2 += 12334 ; break ; case 0x56 : *result1 += 7641 ; *result2 += 7235 ; break ; case 0x57 : *result1 += 15552 ; *result2 += 323528 ; break ; case 0x58 : *result1 += 9834 ; *result2 += 732523528 ; break ; case 0x59 : *result1 += 33553 ; *result2 += 7238 ; break ; case 0x5A : *result1 += 52763 ; *result2 += 726628 ; break ; case 0x61 : *result1 += 1063 ; *result2 += 121 ; break ; case 0x62 : *result1 += 1724 ; *result2 += 111 ; break ; case 0x63 : *result1 += 1169 ; *result2 += 738 ; break ; case 0x64 : *result1 += 18253 ; *result2 += 762 ; break ; case 0x65 : *result1 += 1024 ; *result2 += 14 ; break ; case 0x66 : *result1 += 1744 ; *result2 += 13 ; break ; case 0x67 : *result1 += 1661 ; *result2 += 12 ; break ; case 0x68 : *result1 += 1872 ; *result2 += 11 ; break ; case 0x69 : *result1 += 1084 ; *result2 += 99 ; break ; case 0x6A : *result1 += 1892 ; *result2 += 888 ; break ; case 0x6B : *result1 += 192 ; *result2 += 77 ; break ; case 0x6C : *result1 += 10109 ; *result2 += 555 ; break ; case 0x6D : *result1 += 2078 ; *result2 += 90 ; break ; case 0x6E : *result1 += 3591 ; *result2 += 98 ; break ; case 0x6F : *result1 += 142 ; *result2 += 7468 ; break ; case 0x70 : *result1 += 632432 ; *result2 += 575475 ; break ; case 0x71 : *result1 += 3415 ; *result2 += 648 ; break ; case 0x72 : *result1 += 24555 ; *result2 += 538 ; break ; case 0x73 : *result1 += 2224 ; ++*result2; break ; case 0x74 : *result1 += 1211 ; *result2 += 64 ; break ; case 0x75 : *result1 += 2242 ; *result2 += 75 ; break ; case 0x76 : *result1 += 7334 ; *result2 += 78 ; break ; case 0x77 : *result1 += 9502 ; *result2 += 5 ; break ; case 0x78 : *result1 += 917 ; *result2 += 38 ; break ; case 0x79 : *result1 += 11539 ; *result2 += 8 ; break ; case 0x7A : *result1 += 6400 ; *result2 += 456 ; break ; default : break ; } } int main (int argc, char * argv[]) { char name[20 ] = { 0 }; int a=0 ; int b=0 ; printf ("请输入用户名:" ); scanf ("%s" , name); for (int i = 0 ; i < strlen (name); i++) { EditKeyPress(&a, &b, name[i]); } printf ("serial-1 = %d\nserial-2 = %d\n" , a, b); return 0 ; }
018-crackme_0006 查看程序基本信息
静态分析
搜索字符串
找到关键函数记录地址
向上找到 GetDlgItemTextA 函数,并记录地址
动态调试
获取输入的 name
经过第一次动态调试,发现了两个可疑的函数。经过二次调试分析出第一个函数是计算 name 的乘积,第二个函数是将结果 循环左移 1 位
再次经过调试分析程序
最终根据关键函数 strcmp 验证确认 serial
但是还有一个疑问,其中有一个局部变量 [ebp-0x10]=0x22347010,这个我们并不确定是固定值,还是由其他某个算法函数生成的。再次换另一种 name 和 serial 动态调试,确认一下。
但是在 win11 中测试程序时发现并未成功,重新调试后,发现两个不同的系统 [ebp+0x10] 的值不一样
向上找关于 [ebp-0x10] 的指令,发现[ebp-0x10]存储的是函数的返回值,其入口参数是 [ebp-8] 和 [ebp-4],继续向上找,最终定位到 0x4011D5 处
找到关键函数并分析:将 “C盘” 和 “D盘” 的 pVolumeSerialNum 参数值分别转为浮点数;再分别 * 2;最后相加、开方,转换成整数。
算法分析
第一步:将 “C盘” 和 “D盘” 的 pVolumeSerialNum 参数值分别转为浮点数;再分别 * 2;最后相加、开方,转换成整数 key 。
第二步:num = ((name[i] 的乘积 ) << 1) or key;第 8 位置 0;循环从 071362de9f8ab45c 中取第 (num % 0x10) 位,num /= 4
编写注册机 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 #define _CRT_SECURE_NO_DEPRECATE #pragma warning (disable:4996) #include <stdio.h> #include <stdlib.h> #include <windows.h> #define B_PATH 0x80 int AlAn (int x, int y) { int a = 0 ; __asm { fwait finit fild x fld st (0 ) fmulp st (1 ) , st (0 ) fild y fld st (0 ) fmulp st (1 ) , st (0 ) faddp st (1 ) , st (0 ) fsqrt fistp a } return a; } int main (void ) { unsigned int num1 = 0 ; unsigned int num2 = 0 ; unsigned long key1 = 0 ; unsigned long key2 = 1 ; unsigned long key3 = 0 ; char name[] = { 0 }; char str[16 ] = { '0' ,'7' ,'1' ,'3' ,'6' ,'2' ,'d' ,'e' ,'9' ,'f' ,'8' ,'a' ,'b' ,'4' ,'5' ,'c' }; char szVolumeNameBuf[MAX_PATH] = { 0 }; DWORD dwVolumeSerialNum; DWORD dwMaxComponetLength=0xFF ; DWORD dwSysFlags; char szFileSystemBuf[MAX_PATH] = { 0 }; GetVolumeInformationA("c:\\ " , szVolumeNameBuf, B_PATH,&dwVolumeSerialNum, &dwMaxComponetLength, &dwSysFlags, szFileSystemBuf, B_PATH); num1 = dwVolumeSerialNum; GetVolumeInformationA("d:\\ " , szVolumeNameBuf, B_PATH, &dwVolumeSerialNum, &dwMaxComponetLength, &dwSysFlags, szFileSystemBuf, B_PATH); num2 = dwVolumeSerialNum; key1=AlAn(num1, num2); printf ("请输入一个用户名:" ); scanf ("%s" , name); for (int i = 0 ; i < strlen (name); i++) { key2 *= name[i]; } key3 = ((key2 << 1 ) | key1) & 0x0FFFFFFF ; for (unsigned long j = key3; j > 0 ; j /= 4 ) { printf ("%c" , str[j % 0x10 ]); } printf ("\n" ); system("pause" ); return 0 ; }
019-Acid Bytes
程序有 upx 壳
程序有 nag 窗口
esp 定律脱壳
F8 执行指令 pushad,对 esp 设置硬件断点
F9 运行程序到断点处
F8 运行到程序真正入口点,并进行脱壳
验证是否脱壳成功
去除 nag 窗口
IDA 搜索字符串
定位,记录内存地址
设置在内存地址处设置断点
动态调试,返回到父函数
经过多次动态调试,发现 ebx 是一个动态值,根据 ebx 不同值输出不同的结果。所以最简单去 nag 的方式是将 SpeedButton2Click 函数 nop 掉。
分析程序
搜索字符串,找到关键函数,记录相关内存地址
动态调试后发现就是字符串比较:name = “Registered User”,serial = “GFX-754-IER-954”
020-cosh.3 查看程序基本信息
静态分析
搜索关键字,定位到程序位置
一直向上找到第一个跳转错误的位置 0x004014F5
动态调试
动态调试分析程序
找到算法部分,进行分析
验证算法
1 2 3 4 5 6 (39 xor 1) xor 0xA = 2 (75 xor 2) xor 0xB = | (6E xor 3) xor 0xC = a (6B xor 4) xor 0xD = b (6B xor 5) xor 0xE = ` (6B xor 6) xor 0xF = b
编写注册机 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #define _CRT_SECURE_NO_DEPRECATE #include <stdio.h> int main (void ) { char name[20 ] = { 0 }; printf ("请输入用户名:" ); scanf ("%s" , name); for (int i = 1 , j = 0xA ; i <= strlen (name); i++, j++) { printf ("%c" ,(name[i-1 ] ^ i) ^ j); } return 0 ; }
021-DIS[IP]-Serialme
程序无壳
静态分析
搜索字符串,定位到主程序
找到疑似算法部分
动态调试
设置断点,动态调试分析算法
编写注册机 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 #define _CRT_SECURE_NO_DEPRECATE #include <stdio.h> int main (void ) { char name[20 ] = { 0 }; char num = 0 ; char result[2 ] = { 0 }; printf ("请输入用户名:" ); scanf ("%s" , name); for (int i = 0 ; i < strlen (name); i++) { num = 0x61 ; if ((name[i] != 'Z' ) && (name[i] != 'z' ) && (name[i] != '9' )) { name[i] += 1 ; } num += i; result[0 ] = num; result[1 ] = name[i]; for (int j = 1 ; j >=0 ; j--) { printf ("%c" , result[j]); } } return 0 ; }
022-CM
程序无壳
静态分析
搜索字符串定位到主程序
找到最上面的指令记录地址
动态调试
在地址处设置断点,并分析算法
serial = 6287-A
023-TraceMe
程序无壳
静态分析
搜索字符串,定位到程序字符串位置
对程序分析后发现这个程序看不懂,并不是我们正常的程序运行流程,报错字符串前面也看不到任何的跳转和call指令。
既然有输入对话框,就一定会使用 GetDlgItemTextA 函数来获取字符串。
记录地址:0x40119C
既然要验证 serial,一般都会使用 strcmp 函数来比较。尝试点开其他自定义函数进行查询。
记录地址:0x401379
动态调试
在两个 GetDlgItemTextA 函数位置设置断点
在 strcmp 函数位置处设置断点
动态调试,分析算法
验证 serial
复习:neg 和 sbb 指令
1 2 3 4 5 6 7 8 neg:用零减去操作数 当操作数为0时,置CF位为0 当操作数不为0时,置CF位为1 SBB:带进位(CF)减法 sbb eax,eax(eax-eax-CF) strcmp 函数,通常会连起来使用这两个指令
注册机 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 #define _CRT_SECURE_NO_DEPRECATE #include <stdio.h> int main (void ) { char name[20 ] = { 0 }; int key[8 ] = { 0x0C ,0x0A ,0x13 ,0x09 ,0x0C ,0x0B ,0x0A ,0x08 }; int result = 0 ; printf ("请输入用户名:" ); scanf ("%s" , name); for (int i = 3 , j = 0 ; i < strlen (name); i++, j++) { if (j > 7 ) { j = 0 ; } result += key[j] * name[i]; } printf ("serial = %d" , result); return 0 ; }
024-reverseMe 查看程序
程序无壳
打开程序后提示:评估期已过期。购买新许可证
IDA 静态分析
IDA 搜索字符串找到 keyfile.dat 文件名
新建 keyfile.dat,并输入任意值,重新打开文件后层序提示:密钥文件无效。
分析出至少输入 16 个 key 值。
再次分析,发现算法部分,keyfile 中至少要有8个字母 ‘G’
验证算法
025-CRC-32crackme CRC 校验 CRC的目的是保证数据的完整性,其方法是在发送数据的后面再增加多余的若干位数据,接收方使用同样的CRC计算方法,检查接收到的数据CRC是否为0:
如果为0,则表示数据是完整的,接收方可以开开心心的去处理这个数据。
如果不为0,则表示数据不完整/出错,接收方就需要处理下这个数据(一般是丢弃/要求重发)。
CRC校验有多种类型如:
CRC12:用于某些电信标准。
CRC16:常见的变体有多用途链路层CRC(PLCC)和CRC-16-IBM等。
CRC32:广泛用于网络通信和文件校验。
CRC64:用于处理64位数据,例如在某些网络协议中。
不同的应用可能会根据其特定需求选择不同的CRC算法。而每种类型的CRC校验算法都有其固定的多项式,最后根据模 2 除法生成多项式表。
如果想要计算某一数据的校验码,程序会使用循环得出校验和,其步骤如下:
按字节异或 0xFF
根据返回值查CRC表
对查到的CRC值进行累加
模 2 除法: 也称模 2 运算,其本质就是异或(xor)算法。
多项式: 在二进制形式下,多项式中的1表示对应的位被设置,而0表示未设置。例如多项式:x^3 + x^2 +x^0 = 1101,按顺序排列其中少了 x^1 ,所以有一个为 0 。
CRC 表的生成 CRC 校验表是通过循环依次计算 0~255 校验值作为表的元素。
这里用 CRC8 校验算法作为案例生成CRC表中 “0x01” 的校验值:
第一步:确认 CRC8 多项式的二进制值
第二步:多项式的值,最高位 1 隐藏(忽略),然后再将该值颠倒。
第三步:声明一个变量 temp 存储整数值
第四步:判断 temp 最低位。如果最低位为 1 ,先将 temp 右移 1 位,然后将 temp 和 颠倒后的多项式进行异或运算,结果保存到 temp。如果最低位为 0 ,则将 temp 右移 1 位。
第五步:重复第二步,直到的 temp 中的 8 位数全部右移出去(简单来说就是一共要右移 8 次)。
第六步:循环结束后,最终 temp 中存储的就是 temp原来整数的 CRC8 的校验值。
确认多项式值
CRC8 多项式 X^8+X^5+X^4+X^0 = 0x131= 1 0011 0001
隐藏最高位 1 = 0011 0001
按位颠倒后得到 1000 1100 = 0x8c
计算整数 1(temp)的 CRC8 校验值 如果 temp 末尾为1,右移 1 位,然后与 0x8C 异或,存储结果到 temp;如果 temp 最低位为 0 ,则 temp 右移 1 位
temp = 0000 0001
第 1 次右移:temp(0000 0001) >> 1 = 0000 0000
temp = temp(0000 0000) xor 1000 1100 = 1000 1100
第 2 次右移:temp = temp(1000 1100) >> 1 = 0100 0110
第 3 次右移:temp = temp(0100 0110) >> 1 = 0010 0011
第 4 次右移:temp = temp(0010 0011) >> 1 = 0001 0001
temp = temp(0001 0001) xor 1000 1100 = 1001 1101
第 5 次右移:temp = temp(1001 1101) >> 1 = 0100 1110
temp = temp(0100 1110) xor 1000 1100 = 1100 0010
temp = temp(0011 0000) xor 1000 1100 = 1011 1100
第 8 次右移:temp = temp(1011 1100) >> 1 = 0101 1110 = 0x5E
生成 CRC 表 由于CRC的计算过程中需要不停的循环做异或运算,占用CPU较多,算法上有一种空间换时间的做法:提前把0x00-0xFF共256个数据的CRC码提前算好保存,那么计算时可以节省CPU,这个提前算好的表叫CRC表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 #define _CRT_SECURE_NO_DEPRECATE #include <stdio.h> int CRC_Poly (int x, int y) { int temp=0 ; _asm { mov ecx, y mov edx, x xor eax, eax shl ecx, 3 dec ecx CRC_Rev: shr edx, 1 adc eax, 0 shl eax, 1 loop CRC_Rev mov temp,eax } return temp; } int CRC_Tab (int x, int y) { int temp=0 ; _asm { xor ecx,ecx mov eax,x CRC_Tab1: cmp ecx, 8 jz end1 inc ecx shr eax,1 jc CRTab_CF jmp CRC_Tab1 CRTab_CF: xor eax,y jmp CRC_Tab1 end1: mov temp,eax } return temp; } int main (void ) { int crc8[256 ] = { 0 }; int poly = 0x07 ; int proc_poly = 0 ; int temp1 = 0 ; proc_poly = CRC_Poly(poly, 1 ); for (int i = 0 ; i < 256 ; i++) { if (i > 0 ) { crc8[i] = CRC_Tab(i, proc_poly); } printf ("0x%x " , crc8[i]); } return 0 ; }
参考:
通过查CRC表法,计算校验和
先确认需要计算的数据串的指针和数据串长度(字节)
确认初始异或值,也可认为是CRC初始值。再确认最终需要异或的值
计算CRC表下标,并保留低字节:(数据串字节 xor CRC)& 0xFF
移出处理过的 CRC 值:CRC >> = 8
未处理过的 CRC xor CRC_Tab[下标]
循环重复 3~5 步
CRC 检验和 xor 最终需要异或的值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 int CRCSum (int * Dat, int Len, int Init_CRC, int END_XOR) { uint32_t CRC = Init_CRC; int Zj = 0 ; for (int i = 0 ; i < Len; i++) { Zj = ((Dat[i] & 0xFF )^ CRC) & 0xFF ; CRC >>= 8 ; CRC = CRC ^ crc_tab32[Zj]; } CRC ^= END_XOR; return (CRC); }
参考:
这里是为了理解CRC校验,所以参考别人的代码手写的。后面会直接利用
查看程序
查看程序基本信息
运行程序,未发现任何错误提示
IDA 静态分析
搜索字符串找到关键跳转,并记录地址 0x404388
继续分析上面的代码
这里我们字符串长度唯一不超过 5 的就是 name,修改后看到程序提示错误信息
在程序逻辑框架中还有不少自定义函数,我们需要通过动态调试才能分析程序算法。
动态调试
经过动态调试,找到了2个可疑的函数,这基本可以确定是两个关键算法函数
算法分析
分析第一个函数算法
分析第二个算法
我们将第二个值放到计算器中看一下,发现这个算法就是将字符串转换为整数
验证serial,将CRC32检验和(0x21495E8D)转为十进制输入看一下
注册机 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 #define _CRT_SECURE_NO_DEPRECATE #include <stdio.h> #include <stdlib.h> #define CRC_POLY_32 0xEDB88320L #define CRC_START_32 0xFFFFFFFFL static int crc_tab32_init = 0 ;static uint32_t crc_tab32[256 ];static void init_crc32_tab () ;uint32_t crc_32 (const unsigned char * input_str, size_t num_bytes) ;uint32_t crc_32 (const unsigned char * input_str, size_t num_bytes) { uint32_t crc; uint32_t tmp; uint32_t long_c; const unsigned char * ptr; size_t a; if (!crc_tab32_init) init_crc32_tab(); crc = CRC_START_32; ptr = input_str; if (ptr != NULL ) for (a = 0 ; a < num_bytes; a++) { long_c = 0x000000FF L & (uint32_t )*ptr; tmp = crc ^ long_c; crc = (crc >> 8 ) ^ crc_tab32[tmp & 0xFF ]; ptr++; } crc ^= 0xFFFFFFFF L; return crc & 0xFFFFFFFF L; } uint32_t update_crc_32 (uint32_t crc, unsigned char c) { uint32_t tmp; uint32_t long_c; long_c = 0x000000FF L & (uint32_t )c; if (!crc_tab32_init) init_crc32_tab(); tmp = crc ^ long_c; crc = (crc >> 8 ) ^ crc_tab32[tmp & 0xff ]; return crc & 0xFFFFFFFF L; } static void init_crc32_tab () { uint32_t i; uint32_t j; uint32_t crc; for (i = 0 ; i < 256 ; i++) { crc = i; for (j = 0 ; j < 8 ; j++) { if (crc & 0x00000001 L) crc = (crc >> 1 ) ^ CRC_POLY_32; else crc = crc >> 1 ; } crc_tab32[i] = crc; } crc_tab32_init = 1 ; } int main (void ) { char name[20 ] = { 0 }; char str[26 ] = { "DiKeN" }; printf ("请输入用户名:" ); scanf ("%s" , name); strcat (str, name); printf ("serial = %u\n" , crc_32(str, strlen (str))); return 0 ; }
026-KeygenMe 查看程序
程序是用汇编写的,程序未加壳。
静态分析
IDA 静态分析定位的程序
内存地址处设置断点,使用 OD 分析程序
我们可以直接静态分析出算法,分析不出来就动态调试一下。
还有一个指令需要注意: CMP ESI,DWORD PTR DS:[0x403138]
这里表示我们输入的name生成了正确的十六进制后,还要再将各字节反转,输出正确的字符才能注册成功。而我们name生成的十六进制值最大值值是 4 字节。
而我们seial的字符串可以使用系统中相应的GBK编码作为字符表,同样name也可以输入中文
编写注册机 ASCII 可打印 ASCII 32126(0x207E) GBK:可打印 0x8140 - 0xFEFE
注册机:
通过输入 name 来生成十六进制值
将生成的十六进制值进行反转
要求1:判断第一个字节是否在 0x20~0x7E 之间
要求2:如果不符合要求就判断 2 字节是否在0x8140 - 0xFEFE中
使用循环判断字符,如果符合要求1,就继续判断下一个字符。如果不符合要求1,就判断是否符合要求2。如果两个都不符合就报错。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 #define _CRT_SECURE_NO_DEPRECATE #include <stdio.h> #include <stdlib.h> int Ascii_Check (char * x,char * y) { if ((*x >= 32 ) && (*x <= 0x7E )) { *y = *x; } else { return -1 ; } } int Gbk_Check (int x,char * y) { if ((x >= 0x8140 ) && (x <= 0xFEFE )) { *y = x / 0x100 ; *(y+1 ) = x % 0x100 ; } else { return -1 ; } } int main (void ) { unsigned int num = 0 ; char rev_str[5 ] = { 0 }; char name[20 ] = { 0 }; char A_result[2 ] = { 0 }; char G_result[3 ] = { 0 }; int bytes = 0 ; printf ("请输入用户名:" ); scanf ("%s" , name); for (int i = 0 ; i < strlen (name); i++) { num += name[i] * name[i]; num += (((name[i] / 2 ) + 3 ) * name[i]) - name[i]; num *= 2 ; } for (int i = num,j = 0 ; i > 0 ; i >>= 8 ,j++) { rev_str[j] = i % 0x100 ; } bytes = strlen (rev_str); for (int i = 0 ; i <bytes;) { if (Ascii_Check(rev_str+i, A_result) != -1 ) { printf ("%c" , A_result[0 ]); i++; } else { if ((bytes-i) < 2 ) { if ((i + 2 ) <= bytes) { i--; } else { printf ("Serial gen Error\n" ); break ; } } num = (rev_str[i] << 8 ); num += (unsigned char )rev_str[i + 1 ]; num &= 0x0000FFFF ; if (Gbk_Check(num, G_result) != -1 ) { printf ("%s" , G_result); i += 2 ; } else { printf ("Serial gen Error\n" ); break ; } } } return 0 ; }
当然还可以写一个内存注册机直接将结果写入程序中,暂时还不会写,先放着留着以后写。
027-MexeliteCRK1 查看程序
程序是使用 Delphi 写的,无壳
静态分析
根据关键字搜索字符串
双击搜索字符串,从这里我们就可以猜到这个字符串大概率就是 serial
delphi逆向,通常找按钮触发事件一般在 “_TForm1_Button1Click” 中。
动态调试
动态调试验证一下
输入 name = Benadryl 进行验证
028-ArturDents-CrackMe#3
程序有 PEtitle2.x 壳
ESP 定律脱壳
在 ESP 第一次变动的时候设置断点
F9 运行程序,此时程序在系统领空
Ctrl+F9 运行到程序领空,此时看见我们跳到的指令是一个 SEH 异常指令。
经过前几次执行会发现,直接 F9 继续运行程序,程序会进入异常终止程序运行。
按 shift+F9 忽略异常,运行程序。
继续按 shift+F9 忽略异常运行程序
Ctrl+F9 运行到程序领空
当运行程序到一段看不懂的指令时,基本就说明已经运行到程序的入口地址。这里从模块中删除分析的代码,即可还原代码。
直接脱壳
验证程序是否能正常运行
静态分析
查看程序
搜索字符串,发现一个 “Well done Cracker, You did it!”,直接定位到相应的代码位置
因为 IDA 无法调用出流程图,我们直接动态调试,看看关键函数。
动态调试
设关键节点设置断点
基本可以断定这是strcmp函数
在报错:请求输入 name 或 serial 的后面,设置断点
分析suanfa
验证结果
编写注册机 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 #define _CRT_SECURE_NO_DEPRECATE #include <stdio.h> #include <stdlib.h> int main (void ) { char name[20 ] = { 0 }; printf ("请输入用户名:" ); scanf ("%s" , name); printf ("Serial:ADCM3-" ); for (int i = 0 ; i < strlen (name); i++) { printf ("%d" , name[i] / 3 ) ; } printf ("\n" ); return 0 ; }
029-figugegl.1
查看程序基本信息,程序使用的 LCC-Win32 编译器编译的。
静态分析
搜索字符串,找到程序关键跳转
分析出算法
验证结果
1 2 3 4 5 6 7 8 9 9unkk = 0x39,0x75,0x6E,0x6B,0x6B serial = 9tlhg 39-0=39 75-1=74 6E-2=6C 6B-3=68 6B-4=67
编写注册机 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 #define _CRT_SECURE_NO_DEPRECATE #include <stdio.h> #include <stdlib.h> int main (void ) { char name[20 ] = { 0 }; printf ("请输入用户名(name长度 >= 4):" ); scanf ("%s" , name); printf ("Serial:" ); for (int i = 0 ; i < strlen (name); i++) { printf ("%c" , name[i] - i) ; } printf ("\n" ); return 0 ; }
030-Acid Bytes.4
查看程序基本信息:程序有UPX壳;用户名至少 6 位;提示报错信息。
脱壳
ESP 定律脱壳
F9 运行程序到硬件断点处
F8运行程序,到入口地址处
脱壳,并验证程序是否能正常运行。
静态分析
IDA 无法展开流程图,我们直接在关键跳转和strcmp函数处设置断点
使用 OD 断点,运行程序,在配合 IDA 向上找可疑函数
执行程序后发现程序并未断点在可以函数位置,重新分析发现函数前面都有一个跳转。
重新分析,在第一个函数的跳转处设置断点,并查看上面几行指令,并对照IDA分析 eax 初始值。
动态调试
经过一遍循环下来基本就能理清算法逻辑了:第一个部分循环计算 name[i]*2(i<6) 的累计值
第二部分:strlen(name)*2;最终将这两个值相加,转换成十进制字符串。
验证算法是否正确
编写注册机 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 #define _CRT_SECURE_NO_DEPRECATE #include <stdio.h> #include <stdlib.h> int main (void ) { char name[20 ] = { 0 }; int result = 0 ; printf ("请输入用户名(name长度 >= 6):" ); scanf ("%s" , name); for (int i = 0 ; i < strlen (name); i++) { result += name[i] * 2 ; } result += strlen (name) * 2 ; printf ("Serial:%d\n" ,result); return 0 ; }
031-Cruehead.1
查看程序基本信息
静态分析
搜索字符串,定位到函数位置
继续使用 ctrl+x 定位函数 sub_40134D 的位置,此时我们已经进入主程序位置
找到关键跳转,发现这里并没有使用到循环,而且有可疑函数 sub_4013D8 和 syb_40137E
分析 sub_40137E 函数
如果 esi 指向的字符大于字母 Z,该字符减 20h
将 esi 指向的字符串累加,最终异或 5678h
分析 sub_4013D8
定位内存地址
动态调试
在内存地址处设置断点,并调试
我们直接看到 sub_40137E 函数的参数是name;sub_4013D8 的参数是 serial
之前已经通过静态分析出算法,我们直接设置一个符合程序的 name,并计算 serial
name:JUNKK
serial:JUNKK xor 5678h xor 1234h = 17871
编写注册机 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 #define _CRT_SECURE_NO_DEPRECATE #include <stdio.h> #include <stdlib.h> int main (void ) { char name[20 ] = { 0 }; int sum = 0 ; printf ("请属于用户名(只允许A~Z之间的字母):" ); scanf ("%s" , name); for (int i = 0 ; i < strlen (name); i++) { sum += name[i]; } printf ("%d" , sum ^ 0x5678 ^ 0x1234 ); return 0 ; }
032-Bengaly-Crackme2
查看程序基本信息,发现有 upx 压缩壳
esp 定律脱壳
IDA 静态分析
图片中的算法标记错误,第一个是 dl * dl
动态调试确认两个 算法输入的值
编写注册机1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 #define _CRT_SECURE_NO_DEPRECATE #include <stdio.h> #include <stdlib.h> int main (void ) { char name[20 ] = { 0 }; int sum = 0 ; printf ("请输入用户名:" ); scanf ("%s" , name); for (int i = 0 ; i < strlen (name); i++) { sum += (name[i] * name[i])+ (name[i] >> 1 )- name[i]; } printf ("%d" , sum); return 0 ; }
033-dccrackme1
查看程序基本信息
使用 IDA 和 OD 搜索字符串,都没找到。
delphi 程序事件通常是在双击按钮 _TForm1_Button1Click 触发,IDA 找到该函数并查看,发现有个 strcmp 函数
OD调试,尝试在 strmp 函数处设置断点,确认是不是我们要找的关键函数
静态分析算法
编写注册机验证
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 #define _CRT_SECURE_NO_DEPRECATE #include <stdio.h> #include <stdlib.h> int main (void ) { char name[20 ] = { 0 }; int sum = 0 ; printf ("请输入用户名:" ); scanf ("%s" , name); for (int i = 0 ; i < strlen (name); i++) { sum += (name[i] - 0x17 ) * (name[i] - 0x11 ); } printf ("%d\n" , sum); return 0 ; }
034-fireworx.5
查看程序基本信息,依然是没有弹出任何报错信息
静态分析
使用 IDA 和 OD 都没有搜索到任何可疑的字符串
在 IDA 中找到了两个 Tform1 窗体
在第一个窗体中发现了 strcmp 函数,这个很可疑,记录一下内存地址 0x00441A24
第二个是点击按钮的窗体,这个应该就是显示 about 按钮输出的内容。
动态调试
打开OD,并在该内存地址处设置断点,运行程序后可以看到输入的字符串在与 “Regcode” 做比较
修改输入字符串,看看字符串是否会根据我们输入不同的值而变动。调试后发现需要比较的字符串无任何变动,说明这是一个固定字符串比较。而这个字符串应该是通过上面的 call 来实现的。
验证 serial
035-Dope2112.2
查看程序基本信息
使用 IDA 搜索字符串和函数,因IDA反编译delphi存在缺陷,未搜索到相关字符串和函数
使用 PE Explorer 工具,反编译程序
动态调试
动态调试分析程序
运行程序验证分析结果
编写注册机 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 #define _CRT_SECURE_NO_DEPRECATE #include <stdio.h> #include <stdlib.h> int main (void ) { char name[20 ] = { 0 }; int result = 0x37 ; printf ("请输入用户名:" ); scanf ("%s" , name); for (int i = 0 ; i < strlen (name); i++) { result += name[i] << 9 ; } printf ("%d\n" , result); return 0 ; }
036-Andrnalin.2
查看程序基本信息
使用 IDA 搜索字符串(勾选搜索unicode字符串)
OD 运行程序,定位到关键函数和跳转位置处,然后向下查看堆栈数据,发现可疑的字符串
验证一下,看看是不是我们需要的key
算法分析
但不跟踪调试,找到一段循环在处理 name 字符串
在执行第二次循环执行到vbaVarAdd函数时发现,ECX 和 栈地址 0x0012F45C 的值都是 0xAE。第一次执行的时候这两个值是0x39 加上 0x57 等于 0xAE。这里的循环就是将 name[i] 转换成整数相加,最终计算结果是 0x187
循环结束后继续跟踪调试,发现有个 vbaVarMul 函数,执行后未找到乘积的结果。
重新调试并通过 CE 找到了有变动可疑的内存数据
执行 vbaMidStmtVar 函数发现出现了未处理完整的 key 值,这里将第4个字符替换为 ‘-‘
这里将第9个字符替换为 ‘-‘
编写注册机 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 #define _CRT_SECURE_NO_DEPRECATE #include <stdio.h> #include <stdlib.h> int main (void ) { char name[20 ] = { 0 }; long long num = 0 ; int result[20 ] = {0 }; printf ("请输入用户名:" ); scanf ("%s" , name); for (int i = 0 ; i < strlen (name); i++) { num += name[i]; } num *= 0x499602D2 ; for (int k=0 ; num > 0 ; num /= 10 ,k++) { if (k == 3 || k == 8 ) { result[k] = 0x2D ; } else { result[k] = num % 10 ; } } printf ("key:" ); for (int m = (sizeof (result)/8 )+1 ; m >= 0 ; m--) { if (m == 3 || m == 8 ) { printf ("%c" , result[m]); } else { printf ("%d" , result[m]); } } printf ("\n" ); return 0 ; }
037-fireworx.2
查看程序基本信息
搜索click,找到关键函数
找到关键函数 strcmp,定位到内存地址 0x0044175A
发现一个可疑的字符串,手动再验证一遍
发现 serial 是拼接字符串 :”name”+”name”+”625g72”
再次验证上面的分析是正确的
038-Eternal Bliss.3
查看程序基本信息
搜索字符串,找到程序关键指令,并定位到内存地址处
动态调试未被断点拦截,我们将上面跳转到错误信息的地址都记录设置断点:00403115、00402DD2
动态调试后仍然看不懂是这些比较的二进制含义,无法找到程序算法。
找到所有跳转到报错的关键函数处,结合IDA分析处的函数,进行多次调试后分析出算法
算法分析
将输入的字符串值相加,并与 0x2DC 向比较,如果不相等则报错
读取字符串中的2、4、7位字符,并且都要等于 0x65 = “e”
用固定数值做的障眼法,无任何意义
编写注册机 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #define _CRT_SECURE_NO_DEPRECATE #include <stdio.h> #include <stdlib.h> int main (void ) { char * str = "AedecdeA" ; printf ("Key = %s\n" , str); return 0 ; }
补充:类似于下面这段汇编,是用来比较 esi 和 [ebp-8] 的值是否相等
1 2 3 4 5 sub esi,[ebp-8] neg esi sbb esi,esi inc esi neg esi
039-eKH.1
查看程序基本信息
IDA 找到双击事件函数
动态调试
最终找到另一个可疑函数
跟进去发现可疑的比较
验证可疑字符串,发现这个是正确的 serial
重新调试程序,分析关键算法
算法分析 1 2 sum = (((sum+name[i]) << 8) | string + i) if(sum<0){sum *= -1}
1 2 serial[i] = key[sum % 0xA] sum /= 0xA
注册机 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 #include <stdio.h> #include <stdlib.h> int main (void ) { char name[]={0 }; int sum=0 ; char * string = "LANNYDIBANDINGINANAKEKHYANGNGENTOT" ; char * key="LANNY5646521" ; int len; printf ("请输入用户名:" ); scanf ("%s" ,name); len = strlen (name); for (int i=0 ;i<len;i++) { sum += (int )name[i]; if (sum > 0 ) { sum <<= 8 ; sum |= *(string +i); if (sum<0 ) { sum *= -1 ; } } } sum ^= 0x12345678 ; printf ("你的密钥是: " ); for (int pkey=0 ;sum>0 ;sum /= 0xA ) { pkey=sum%0xA ; printf ("%c" ,*(key+pkey)); } return 0 ; }
40-DaNiEl-RJ.1
查看程序基本信息
静态分析找到关键跳转 0x0042D56F
动态调试
尝试输入 “>zsp” 发现破解成功
0x3E == “>”,0x7A == “z”
注册机 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include <stdio.h> #include <stdlib.h> int main (void ) { char name[]={0 }; printf ("请输入用户名:" ); scanf ("%s" ,name); int len = strlen (name); printf ("你的密钥是: " ); for (int i=0 ;i<len;i++) { printf ("%c" ,name[i]+5 ); } return 0 ; }