-
-
[原创]2020 KCTF by lelfei
-
2020-10-29 18:25 8632
-
注册码:10C7F30833B9C4563BF035C32D8C7709E040FCA64E211F34CD3FE773
说明:注册成功时输出“GOOD!”
设计说明:
0x00 这一题是这一段时间一些crackme想法的大集合,包含花指令、调试检查、内存校验、大数计算、溢出利用等等,如果能够理清各种干扰项,最后的算法其实比较简单。
0x01 自己写了一个大数计算类BigNum,数据位有0x20字节,实现了基本的加减乘除算法。有2个固定大数P=0x3FAFFA2B01B6BA9744C4B4E010010401和Q=0xFEA1BD9E6964129D8F5079E1,其中P共有4个DWORD组成,其中高位的3个DWORD分别是“INPUT:”、“GOOD!”、“ERROR!”的计算值,输出“INPUT:”等待用户输入。
0x02 用户输入注册码记为D,对D的前7位和后7位转换为数值记为sn1和sn2,对main()函数和调试检查函数分别计算内存校验,根据调试检查结果分别与这2个校验值进行异或。循环9次后,检查结果是否为P的第2、3个DWORD。
0x03 D转换为大数,取P的第一个DWORD记作E,计算D*E/0xE053D0F+P+Q记为F,计算F-P*Q记为m1,要求m1的数据位不超过0x10字节。
0x04 计算F*0x0E053D0F*25记为m2,再自加一次m2+m2会导致溢出覆盖掉P的长度位,导致P=1。
0x05 比较m1==P,即最终验证为D*E/0xE053D0F+P+Q-P*Q==1,正确则提示“GOOD!”。
0x06 程序源码中定义了4个花指令标志位:
#define JUNKCODE5() asm volatile("mov $0x55555555,%eax")
#define JUNKCODE7() asm volatile("mov $0x77777777,%eax;inc %eax;inc %eax;")
#define JUNKCODE9() asm volatile("mov $0x99999999,%eax;inc %eax;inc %eax;inc %eax;inc %eax;")
#define JUNKCODE11() asm volatile("mov $0xAAAAAAAA,%eax;mov $0xBBBBBBBB,%eax;inc %eax;")
在源码的main()和调试检测函数中添加了很多引用占位,编译后使用replace_byte.py把标志位替换成随机花指令。
0x07 由于替换花指令导致内存校验值发生变化,程序中使用了2个全局变量作为salt,需要在花指令替换完成后,调试时修改程序流程计算出校验值,再由校验值计算出2个salt手动patch到程序的数据段。
0x08 调试检测函数中有一个是硬件断点检查,程序源码中并没有调用设置硬件断点的函数SetDrxBreakPoint(),而是在编译之后手动patch到入口附近,代码为:
0040CA3A 50 push eax
0040CA3B E8 06000000 call 33.0040CA46
0040CA40 - FF25 AC734C00 *jmp dword ptr ds:[<&msvcrt.__lconv_init>]
0040CA46 58 pop eax
0040CA47 05 70900A00 add eax,0xA9070
0040CA4C FF10 call dword ptr ds:[eax]
0040CA4E EB 03 jmp short 33.0040CA53
0040CA50 31C0 *xor eax,eax
0040CA52 C3 *retn
0040CA53 58 pop eax
0040CA54 ^ E9 77FFFFFF jmp 33.0040C9D0
加*号的为原来数据不要动。目的是调用SetDrxBreakPoint()函数。其中有add eax,0xA9070让指针指向main()中保存SetDrxBreakPoint()的数据地址。这一步操作的主要目的是避免显示调用函数。
0x09 总结及破解建议:
1.“花指令+内存校验”让动态调试过程很恶心,估计很多人都会想到解决办法,就是把去花后的数据保存下来用于IDA分析。
2.“调试检测+内存校验”会让动态调试数据变得不可靠,其实分析完后面大数验证的代码可以发现,这一部分验证并没有起多大作用,通过大数验证就已经让结果唯一了。这一段验证的主要目的只有一个,就是防止在大数前面补0造成多解。
3.大数验证过程需要动态分析与静态分析相结合,静态分析很容易了解到验证过程就是D*E/0xE053D0F+P+Q-P*Q==P,但是却很难看到溢出漏洞改变了P值。
4.大数验证看似计算很乱,分析清楚之后其实很简单,根据这几个变量名也差不多能看出来,其实就是RSA算法中最基础的一步,根据d*e+k*Phi=1的解反求出Phi,再根据Phi=(P-1)*(Q-1)=P*Q-P-Q+1=N-P-Q+1来进行最终验证。
此crackme使用CodeBlocks设计,gcc编译,在win7x64下测试通过。
文件说明:
lelfei-KCTF2020.rar 参赛文件
src.rar 程序源码
replace_byte.py 把程序中预定义替换为随机花指令
readme.txt 本说明文件
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课