前言
说明:本贴是关于自己逆向的一次记录。如果有问题,也请大佬多多指导。
本体名称:latvianfarmer17's Second? crackme [FIXED]
本体作者:latvianfarmer17
上传日期:8:05 PM 04/26/2023
运行平台:Windows
Difficulty:3.2 Rate!
Quality :4.0 Rate!
Arch :x86-64
下载地址: https://crackmes.one/crackme/6449842433c5d43938912c24
注写:这个网站需要自己先注册账号才能下载
1.本次逆向所用到的工具
1.IDA 7 Pro
2.x64Debug
3.ExeinfoPe
4.Microsoft Visual Studio 2022
2.开始逆向的第一步---查壳
首先通过工具 ExeinfoPe 查询该 CrackMe 是否有壳,以及这个程序编译器版本等。
从这里可以发现本程序是没有壳的,使用的编译器是 c++ ,连接器是 14.35 版本。
3.运行一下程序,初步了解一下程序的走向、流程:
发现是输入两个字符串,但是不能输入特殊字符:&……%等,会直接退出
4. 已经发现是无壳程序,并且也了解了一下大体流程,放入 IDA 按下 F5 反编译查看一下具体代码逻辑
从反编译后的代码可以看见刚刚流程的一部分字符串,比如:Enter username,Enter ID,但是也有一些其他没有看到过的字符串
考虑到这个程序可能没有 system("pause") 等暂停函数,效果也可能是显示了哪些没有看到的字符串但是程序很快,直接就退出了
便没有显示出来。
在反编译里可以看到很多这种函数,考虑到 c/c++ 的代码,合理猜测这是 printf 函数,将IDA里面的函数名修改一下,以便
理解。右键函数名,点击 rename global item
在 Name 框里输入 printf 函数名,直接点击确定。
其次,在IDA还看到这样的函数
上边的有百分号和加上一些框框,下边的是 %s ,推测一下这应该是输入函数,但是为什么上边有 %[^\n],我猜应该是过滤
用的,将这个函数我还是修改为 scanf ,以便理解。但是仅仅只有这个格式控制符,没有接受地址是很奇怪的,应该有一些没有反编译完毕,
再次按一下 F5 没有出现变化,选择直接点击这个 sub140001080 ,进入该函数,然后退出 Esc 出来,再次按一下 F5 ,该函数发生了变化
注:经常会出现这种反编译情况,就是“缺胳膊少腿”的情况,在第一次反编译之后,仍然需要点击进入函数,再次按 F5 的方式再次编译。
出现了 v10 ,应该是 v10 用来存放接受的 username,至于那个格式控制符,我对于过滤的情况不太了解,也希望大佬帮忙解释一下。不过不影响逆向
的整体逻辑。
后边也是 Username can only be up to 16 characters long 这个字符,猜测应该是最多16位。
通过下边的这个循环,我坚信这个猜测是正确的
查看这个地方的判定,里边有一些 97 65 48 这些值,通过右键点击 char 转换,发现这些字符应该是 A a 0,所以这里应该是过滤的地方,上边那个格式控制符可能不是?
通过点击 v5 发现 v5 是通过 v10 来的,所以这里是对输入的 username 进行 if 判断
根据这个字符串,里边的 alphanumerical 这个单词
说明只能输入字符数字,所以上边那个就是一些过滤,不能输入其他特效字符
经过自己的函数命名之后,这个也很清楚了,输入的 ID 字符串保存在 v11 里边
通过点击发现,v10 和 v11 是有交互处理的,处理之后的数值赋给了 v6
通过这里,可以查看到合法的话,需要 v7 == 1,如果 v7 == 0 就会打印 Sorry, wrong ID... 的字符串
点击 v7 发现,要想要 v7 == 1 就得执行上边的 while 里边的函数,但是满足 v11[v3 + v6] == v11[v3],这个情况
但是如要要满足,只有 v6 == 0 的时候才正确,或者是可以对称?里边所有的值都是一样的?只是有这样的猜测
进入 sub1400010E0 函数查看一下
看到有俩个字符串,旁边也有长度,0x10,0x14 分别是 16 和 20
也有跟上层函数一样的判断
看到下边有一系列运算,也有同样的上层函数判断,多了一个位运算 和 取余操作
return 的是 v8[16] 不是特别懂
总结:基本从 IDA 看出来的东西就这么多了。对输入的字符进行过滤,只能输入数字字母,输入长度不能超过16,如果正确的话
则输出 Valid ID! ,错误的话就输出 Sorry, wrong ID... ,但是由于本程序没有停断函数,所以这些字符串都是一闪而过。有一个函数对输入的
username 字符串进行了处理操作,在 IDA 上没有看太明白,考虑动态继续分析。
5. 进行 x64Debug 动态运行
运行之后直接对所有模块进行搜索字符串
可以很清新的看到一些熟悉的字符,点进去继续查看
在这里这俩个函数可以对照 IDA 识别出来是printf 和 scanf ,直接把这些函数修改名字,方便查看
修改方式如上,右键在标签里选择添加到地址
一直查看到下方
有这样一段 test al,al 这样的汇编代码,一般用来查看 al 是否是0
对照 IDA 里面的反编译源码,可以发现,这里应该是判断 al 是否是 1 ,然后后边用来输出 valid ID
尝试将 al 位 置 1
修改后果然跳出了正确的字符
到这里爆破基本上就可以实现了。也算是完成了逆向最基本的要求。
不过出于学习的态度,我们还应该再深一步,查看一下内部算法,完成这个 CrackMe 注册机的编写。
6. CrackMe 关键函数分析查看,以及注册机的编写
运行下来查看,这里应该是核心内部算法
再查看一下 IDA 里边的流程,确实在这些字符之下,有这样的算法函数,接下来就要进入这个内部计算函数,好好查看一下他的运算方式。
针对应该输入的字符串,我这里选择了 1234 作为 username ,5678 作为 ID ,上下不一致是为了可以区分哪个字符是 username ,哪个字符是 ID
刚进入会发现这些,上边的那几个应该是 ID 了,对于 xmm0,知道这是一个寄存器就好了。
并且这些 xmm0 在 IDA 里,也是有体现的
继续单步下来,也发现这些判断方法
继续往下分析
可以看到这些类似的代码执行了4次
同样在 IDA 里也是观察到有类似的出现四次的代码
但是这里的数值不太一样,第一个 +0,第二个 +0x539,第三个+0xA72,第四个+0xFAB
点击v9查看到开始为 0
而这里的 v6 是根据累加计算出来的一个数据,不太好看出来是什么,而后面也是通过多次动态调试,发现这个是序号
在动态调试过程中,也发现寄存器出现了一个很长的字符串
通过查看,这里字符串后半部分是 IDA 里开头出现的字符串
那么 IDA 在反编译时应该是少了一些东西,毕竟是静态分析
但是在 x64Debug 这里的字符串更长一些,数了一下有 61 位。
通过多次分析,可以看到这里 je 应该是从 61 位字符串中找到了,然后把序号位置放到 rsi 中(这里是多次动态调试发现的,调试过程就不过多阐释,这里只给发现结果)
可以发现 1 确实是在这个字符串中的第 3 位
2 在字符串中的 0x2f 位置,后边的 3,4 也是对应的位置,所以可以判断,那个 v6 就应该是 ID 在 61 位字符串中的序号位置
在后边也是发现,这里有一个大跳转,然后 f8 跟过去发现还是运算这 4 个循环,跟了一圈还是 1234 来找对应的位置
在 IDA 里,查看到原始是 do while 循环,这里应该是直到字符串16位的最后一位,因为之前一直在有暗示字符串是16 位,
虽然输入了 1234 ,只输入了 4 位,但是下一圈大跳转,还是原来的 1234 应该是有自动补全功能。后续 username 我输入 123456,123abfD,
可以发现确实是自带 16 位补全功能,如果不满 16 位,就依次按照顺序拼接在后边。
在追踪的过程中,如上,发现 0x4ec4ec4f , 在这里其实是有个 %26 取余操作,这里使用的是乘法替代除法来实现的,具体可以查看
https://www.lovesandy.cc/2023/03/16/%E5%8F%96%E4%BD%99%E8%BF%90%E7%AE%97/ 这个帖子了解一下编译器优化取余运算
跳出内部运算函数,在外边可以看到一个用来比对的指令,同时出现了 5678 就是之前输入的 ID 字串,这里应该是 username 运算后的
key 值来比对最后的 ID 值,跟入保存 key 值的地址 ( ctrl + g ),输入 rax + rdx 点击确定
在地址中看到一串字串,我们把这些字串复制下来,保持 username 不变,重新运行程序,再次在输入 ID 的地方输入这些字串尝试一下
发现这个字串确实是根据 1234 来定义的 ID 值,同时也可以确认一下这个 ID 值到底是不是固定的
可以看到输入不同的 username ,ID 值应该也是不同的
因此具体的密码会根据 username 计算的,需要再次进入核心算法查看一下
不过经过刚才的一通分析,了解到:核心算法应该是根据
Yo15z9atB8gZJRhpi0dLfCSA3wVFEyuHKOrTWG4mlXvPeU2j67NbMxsknQqcI
这个字串,来确定符号下标,一共要找 16 次,内部有 4 个为一个小循环,然后还有 4 次大循环。
在重新分析核心算法时,要紧盯这几个寄存器,明白这几个寄存器的用处
rsi 专门用来存放序号位置,r8 用来存放取出来的一个 username 字符值,rbp 是存放键值表
继续往下分析,在这里发现有一个熟悉的字符出现了,就是 L ,上次密码的第一个字母 (LRZFZCEJELIGIZVQ) 而这个 L 就是通过上边一条指令获取到的,所以应该目前该关注的就是上一条指令
跟进上一条指令的内存
发现 L 就在这个地方,下边还有那一开始的字串
往上滑一下,还发现了其他字符
“EYIPVMTKFXJSGWQOADRNCBLHZU”
将上边这一串字符也摘下来,发现这是 26 个长度的字符串
然后再结合 IDA 里那个取余的数字 “26”,可以猜测到就是从这26个字符中选择一个字符,然后填入到 ID 中
上下查看,发现 v9 第一次是 0 ,v6 是从 61个字符串中与 username 字符对比来的序号值,也就是1的位置 3
所以就是 3 << 16 位 :3 0000 然后对 26 取余 3 0000 % 0x1A = 0x16 也就是 22,下标就是 [22] ,
而 L 在这 26 个字串 EYIPVMTKFXJSGWQOADRNCBLHZU 的位置正好是第 23 位,也就是 [22] 的位置,所以运算应该就是这样:
先从 61 字符串取序号位置 v6 然后 v6 + v9 ,然后左移 16 位,然后对 26 取余
,计算获得字符下标,获取目标字符。
而其他三个也是有其他额外数值增加的,0x539,0xA72,0xFAB
从 x64Debug 中也能看到,所以运算方法就知道了,但是如果是 1234 1234 1234 1234 排列,那么下一组值在运算中
也应该是一样的,但是进行下一个大循环,1234 取值是不同的,经过再次查看发现到
v9 每次大循环都是要 + 0x14E4 的,这个 x64Debug 中也能看到,
所以现在的情况就很明朗了
具体的 ID 运算数值:
就是先从 61 字串中找到序号位置,然后 +v9 ,(v9 每个大循环会自增),取得的值左移 16 位之后对 26 取余,然后
从 26 字串中取一个字符,放入到结果中。
知道这些之后,我在下方提供我注册机代码的编写。其实在复测过程中发现,123abfD 一开始是计算错误的,因为在
那 61 个字符之中是没有 D 这个字符的,所以如果有查询不到的字符,就返回 0 ,这个在 x64Debug 中能自己追踪到。
(第一次写这些记录,有的地方可能会晦涩一点,大佬们都可以提出来,我也会努力更正)
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define LEN 16
#define NUM1 0x539
#define NUM2 0xA72
#define NUM3 0xFAB
#define TOTAL 0x14E4
char gKey[] = "Yo15z9atB8gZJRhpi0dLfCSA3wVFEyuHKOrTWG4mlXvPeU2j67NbMxsknQqcI";
char gKey2[] = "EYIPVMTKFXJSGWQOADRNCBLHZU";
char gResult[16] = "";
// 用于下标查找
int GetIndex(char ch)
{
char cTemp = 0;
int nLen = sizeof(gKey);
for (sizet i = 0; i < nLen; i++)
{
cTemp = gKey[i];
if (cTemp == ch)
{
return i + 1;
}
}
return 0;
}
void HandleString(char UserName)
{
int nTotal = 0;
int nCount = 0;
int nIndex = 0;
int nResultIndex = 0;
for (sizet i = 0; i < 4; i++)
{
// nTotal + Index
// 获取序号
nIndex = GetIndex(UserName[nCount]);
// 计算结果序号
nResultIndex = ((nTotal + nIndex) <<16) % 26;
// 将序列里的字符取出来放入结果字符里
gResult[nCount] = gKey2[nResultIndex];
nCount++;
// nTotal + Index + NUM1
// 获取序号
nIndex = GetIndex(UserName[nCount]);
// 计算结果序号
nResultIndex = ((nTotal + nIndex + NUM1) << 16) % 26;
// 将序列里的字符取出来放入结果字符里
gResult[nCount] = gKey2[nResultIndex];
nCount++;
// nTotal + Index + NUM2
// 获取序号
nIndex = GetIndex(UserName[nCount]);
// 计算结果序号
nResultIndex = ((nTotal + nIndex + NUM2) << 16) % 26;
// 将序列里的字符取出来放入结果字符里
gResult[nCount] = gKey2[nResultIndex];
nCount++;
// nTotal + Index + NUM3
// 获取序号
nIndex = GetIndex(UserName[nCount]);
// 计算结果序号
nResultIndex = ((nTotal + nIndex + NUM3) << 16) % 26;
// 将序列里的字符取出来放入结果字符里
gResult[nCount] = gKey2[nResultIndex];
nCount++;
nTotal += TOTAL;
}
}
int main()
{
char UserName[20] = "";
char p = nullptr;
int UserLen = 0;
printf("输入需要注册的用户名\n");
scanf_s("%s", UserName, 20);
UserLen = strlen(UserName);
if (UserLen > LEN || UserLen < 0 || UserLen == 0)
{
printf("输入错误\n");
exit(0);
}
// 对不满长度的字符串进行操作
// 1. 输入满 16 位
// 2. 输入不满 16 位,补齐 16 位
if (UserLen == LEN)
{
}
// 1234567 1234567 12
// 12345678 12345678
// 1 116
else if (UserLen < LEN)
{
int nMul = LEN / UserLen - 1;
int nRemainder = LEN % UserLen;
// 如果没有余数
if (nRemainder == 0)
{
char Temp[20] = "";
strcpy_s(Temp, 20, UserName);
// 复制倍数次,拼接上原始字符串
for (sizet i = 0; i < nMul; i++)
{
strcat_s(UserName, 20, Temp);
}
}
// 如果有余数
else
{
char Temp[20] = "";
strcpy_s(Temp, 20, UserName);
// 复制倍数次,拼接上原始字符串
for (sizet i = 0; i < nMul; i++)
{
strcat_s(UserName, 20, Temp);
}
// 将剩余的部分拼接到字符串里
Temp[nRemainder] = '\0';
strcat_s(UserName, 20, Temp);
}
}
HandleString(UserName);
printf("处理后的字符:%s\n",UserName);
printf("结果字符:%s\n",gResult);
system("pause");
return 0;
}