【吾爱】逆向实战crackme160破解记录(二)
前言
最近在拿吾爱上的crackme程序练练手,发现论坛上已经有pk8900总结好的160个crackme,非常方便,而且有很多厉害的前辈已经写好经验贴和方法了,我这里只是做一下自己练习的记录,欢迎讨论学习,感谢吾爱论坛的各位大神。
程序本身无病毒,杀毒报错可无视,如果实在担心也可也用虚拟机练习
由于cm160里面有不少是相同逻辑的程序,所以如果再遇到比较类似的,会精简一下或者直接跳过。
aLoNg3x.1
peid查一下是32位没有加密壳,得知编写语言是Delphi。
程序打开,发现是输出Nome和codice
点开about后发现了提示,把程序拖进darkde4里面看一下;
找到了okclicl和cancellaclick的两个按钮
根据dark给的地址,回到od里面去找到cancellaclick对应的地址0x442ea8。
输入111和222然后点击按钮,od里面逐步走一下看一下,发现下面的寄存器分别先读取了密码,然后是账户,再然后是一个je跳转,之后call了一个函数就直接返回了,所以猜测这个je是关键的判断,直接nop掉看一下
nop掉之后发现这个按钮消失了 ,然后是ok按钮从开始的不可选中变成了可以点击。
证明这个je确实是判断正误的关键跳转,这个je上面有三个被call的函数,用了od的SwissArmyKnife插件看一下函数名称。
前两个是getText函数,第三个是sub_442AF4
函数
回到ida里面看一下:
分析一下逻辑:
a1,a2是传进来的账户和密码,这是一个验证账户胡序列号的函数。
首先检查用户名长度 v12(即 a1)是否 ≤5,若长度≤5,直接返回 0(验证失败)。
然后计算动态乘数 v2,通过用户名第5个字符(索引4)计算一个动态值。
取用户名第5字符的ASCII值,模7后加2,作为参数传递给 sub_442A20
(点进去分析发现是个阶乘函数),得到v2。
遍历用户名所有字符,计算它的ascii码与v2的积的和 v3
。计算 v3 - v11
(v11 是输入的序列号 a2)
检查结果是否为 31337
(十六进制0x7A69
)。相等就返回1,表示成功;不相等返回0,表示失败。
所以验证的条件就是
S = (v2 * Σ username[i]) - 31337其中:
v2 = (username[4] % 7 + 2)!
Σ username[i] 是用户名所有字符的ASCII值之和。
扔给ai写个脚本:
import mathdef generate_serial(username):if len(username) <= 5:return "错误:用户名长度需>5字符"# 计算阶乘因子(第5字符ASCII%7+2)n = (ord(username[4]) % 7) + 2factor = math.factorial(n) # 计算加权和total = 0for char in username:total += factor * ord(char)return total - 31337if __name__ == "__main__":username = input("请输入用户名(长度>5):")print(f"序列号:{generate_serial(username)}")
输入后可以发现右边按钮已经没了。
接着处理ok按键。
根据刚刚的经验,发现俩getText和一个没名字的函数还有一个je跳转。
猜测跟刚刚按钮的逻辑是一样的,下断点然后给je判断nop掉。
发现按钮消失了,我们的猜测是正确的,那么去这个sub_442ba0
看看是啥逻辑。
int __usercall sub_442BA0@<eax>(int a1@<eax>, int a2@<edx>, int a3@<ebx>, int a4@<esi>)
这段代码实现了一个字符串验证逻辑。
它首先将输入整数转换为字符串,若字符串长度超过5则从末尾开始对每个字符进行变换:取字符ASCII值的平方乘以当前字符位置索引,再对25取模后加65(生成A-Z范围的大写字母)。
变换后的字符串与原始输入字符串进行严格比较,若完全匹配则返回验证成功标志1,否则返回0。
按字符位置从后向前处理(v6 从字符串长度递减至 1):
取原字符的 ASCII 值(*(unsigned __int8 *))
计算平方值(原字符²)
乘以当前位置索引(v6)
取模 25(% 25)
加 65(转换为大写字母 A-Z 的 ASCII 范围)
结果:将序列号整数的每个字符转换为特定大写字母
核心逻辑:
(v7 + v6 - 1) = v6 * (*((v16 + v6 - 1) * *(v16 + v6 - 1)) % 25 + 65;
写出脚本:
def generate_username(serial):user_name = [''] * len(serial)# 从最后一个字符向前处理(索引从高到低)for i in range(len(serial)-1, -1, -1):# 计算新字符: (serial[i]² × (i+1)) % 25 + 65char_val = ord(serial[i])new_char = chr((char_val * char_val * (i + 1)) % 25 + 65)user_name[i] = new_charreturn ''.join(user_name)def main():while True:serial = input("\n请输入序列号(输入0退出): ")if serial == "0":breakusername = generate_username(serial)print(f"生成用户名: {username}")if __name__ == "__main__":main()
现在写出来了两个脚本,试着去测试一下:
账号输入qwerty
,脚本计算序列号是461143
然后用序列号反推字符串EHDEUG
好了,现在根据要求 已经让两个按钮消失了,得到了图片RingZ3ro
aLoNg3x.2
peid打开:
跟前面的cm一个作者,而且还是Delphi编写的。
点开程序:
点开about看过之后,发现要求依旧是让按钮消失,能够看到图片。
一样的思路,扔进dede里面看一下
发现了按钮的地址,然后扔进ida里面导出map文件,把程序和map文件扔进xdbg里面调试。
回到程序上,发现register挡着图片,cancella在侧面,先看一下register按钮。
所以找到地址0x442f28
下断点,然后逐步跑一下看一下。
发现一个字符串443038:"You MUST insert a valid Long Integer Value in the Code Editor... Thank you :)"
这里应该是格式判断的函数。
所以接着往下看,又是一个提示44309C:"Please... The Code Must be > 0"
序列号必须大于零。
根据动调逻辑,会发现0x442f64
和0x442f9b
这俩分别是俩格式判断,所以根据动调逻辑,0x442fc0
这里跳过的部分代码可能就是 核心内容判断逻辑。nop一下看看内容。
按钮现在消失了,所以核心的判断逻辑就在这里。跳转前被call的函数是sub_4429A8
这里应该就是第一个按钮的验证函数。
回到上一层看看传入的a1,a2,a3是啥。
if ( (unsigned __int8)sub_4429A8(dword_445830, v4, (int)v9) )
dword_445830
应该是个固定参数。
v4是一个Int数字,v9是一个指针,那应该指的是字符串nome。
现在去看一下函数的逻辑:
# 伪代码描述验证条件
if (abs(calc_result) % 666666 == (codice % 80 + codice // 89 + 1):return True
但是这个验证里面是需要获得dword_445830
的值,回到点击按钮的函数里面,看一下这个参数在哪里会被赋值。
发现在30行这个分支(根据逻辑猜测是判断序列号是负数),下面参数变成0了,在23行分支这里,参数被赋值了。根据逻辑赋值为0是错误的。所以要看如何进入到23行的逻辑里面,想起来在od里面正确进入验证前有两个格式判断,先判断是整型数据,在判断是正数,跟这里的逻辑是一样的,所以可以知道23行的判断是要求序列号不是整型数据,然后参数dword_445830
才能被赋值,赋值是用dword_445830 = Libmain::TWindowDesigner::SelectAll(v9);
来进行的。
下面的方法有两种,要么我们进到 Libmain::TWindowDesigner::SelectAll(v9)
里面去看静态逻辑,v9这里是用户名的指针,所以跟用户名有关系,要么我们指定一个用户名,回到xdbg里面动调一下看一下dword_445830
的值。
进到Ida里面看了一下,
这个this是nome的字符串指针。
把逻辑扔给ai转成python代码跑一下
def calculate_serial(username: str) -> int:# 检查长度if len(username) <= 5:return 0# 初始化累加器(对应v1)result = 891# 遍历所有相邻字符对(长度-1次循环)for i in range(len(username) - 1):current_char = username[i] # 当前字符(对应v3-1)next_char = username[i + 1] # 下一个字符(对应v3)# 核心计算公式:# result += (当前字符ASCII) * ((下一字符ASCII % 17) + 1)result += ord(current_char) * ((ord(next_char) % 17) + 1)# 最终处理:取绝对值后模29000return abs(result % 29000)# 交互式脚本
if __name__ == "__main__":while True:try:# 获取用户输入username = input("请输入字符串: ").strip()# 计算并输出结果serial = calculate_serial(username)print(f"\n计算结果:{serial}")print("-" * 40)# 询问是否继续cont = input("是否继续计算? (y/n): ").lower()if cont != 'y':print("程序已退出")breakexcept Exception as e:print(f"发生错误: {e}")
这里指定字符串是NULLPTR,得到参数是6026。
写出sub_4429A8
的逆向逻辑:
def generate_serial(account: str, a1: int = 6026) -> int:n = len(account)if n <= 4:raise ValueError("账户长度必须大于4个字符")total = 0for i in range(n): for j in range(n-1, -1, -1): total += a1 * ord(account[i]) * ord(account[j])v = abs(total) % 666666 if v == 0:return 0 x = (v - 1) * 89return x + (80 - x % 80) if __name__ == "__main__":try:account = input("请输入账户名称(长度>4): ")serial = generate_serial(account)print(f"账户 '{account}' 的有效序列号为: {serial}")except ValueError as e:print(f"错误: {e}")
用户名设置成12345678
,得到序列号13682800
,回到程序去验证下。但是这里需要先让程序给参数赋值,所以输入流程是序列号先输入字母NULLPTR
,确认后再输入正确的序列号。发现成功了,但是按钮消失后又出现了一个Agian
的按钮,说是要再来一次,nome框不让修改了,那就按照刚刚的逻辑再来一次,顺带把上面两个代码合并成一个脚本。
def calculate_dword(username: str) -> int:# 检查长度if len(username) <= 5:return 0# 初始化累加器(对应v1)result = 891# 遍历所有相邻字符对(长度-1次循环)for i in range(len(username) - 1):current_char = username[i] # 当前字符(对应v3-1)next_char = username[i + 1] # 下一个字符(对应v3)# 核心计算公式:# result += (当前字符ASCII) * ((下一字符ASCII % 17) + 1)result += ord(current_char) * ((ord(next_char) % 17) + 1)# 最终处理:取绝对值后模29000return abs(result % 29000)def generate_serial(account: str, a1) -> int:n = len(account)if n <= 4:raise ValueError("账户长度必须大于4个字符")total = 0for i in range(n): for j in range(n-1, -1, -1): total += a1 * ord(account[i]) * ord(account[j])v = abs(total) % 666666 if v == 0:return 0 x = (v - 1) * 89return x + (80 - x % 80) if __name__ == "__main__":try:account = input("请输入账户名称(长度>4): ")dword_445830 = input("请输入序列号的字符串: ")serial = generate_serial(account,calculate_dword(dword_445830))print(f"账户 '{account}' 的有效序列号1为: {serial}")dword_445830 = input("请输入Agian序列号的字符串: ")serial = generate_serial(account,calculate_dword(dword_445830))print(f"账户 '{account}' 的有效序列号2为: {serial}")except ValueError as e:print(f"错误: {e}")
结合之后的脚本,就重新改下用户名字改一下,用户名就用admin
,第一次的字符串用asdfgh
,第二次字符串用qwerty
。(这里发现如果字符串是大小写字母混合,脚本计算的结果是错误的,不知道是不是逆向的时候少了某个限制条件,但是纯小写字母或者纯大写字母就能过,就不再回头检查了)
脚本跑一下,然后输入:
现在就完成了cm的完整破解。
简要总结
原本想再写几个cm的,但是这两个代码量也不小,写完感觉博客有点长了,那就这两个单独一篇吧,而且还是同一个作者aLoNg3x的。程序的逻辑设计的都很好,还挺适合练习的。