Executables:
Linux x64
Windows x64
It is just to be run with name and serial number as command line arguments. Valid ones are:
2Z7A7-EK270-TMHR4-BHC71-CEB52-HELL0-HELL0-EONP9
2Z7A7-6I7R9-MZGO9-FDQJ3-JN0Q6-HELL0-HELL0-72KJ9
The first serial number has E feature turned off, the second has E feature turned on.
There are two tasks:
Easy: by patching, turn on/off all 5 features.
Medium: generate a serial number with arbitrary features turned on/off.
Good luck!
hint:
https://down.52pojie.cn/Tools/Debuggers/x64dbg_2018-04-05.zip
https://stackoverflow.com/questions/5475790/how-to-disassemble-the-main-function-of-a-stripped-application
Step1:简单运行exe文件
先尝试一下题目所给的两个序列号,可以看到5个功能各自状态:
其中功能E在使用第1个序列号时被关闭,第2个序列号时被打开。
Step2:使用IDA反编译源程序
使用IDA反编译后查看其伪码,不难找到其进行功能判断的地方:
在汇编码中我们也可以轻松找到其对应的部分:
Step3:修改跳转部分的十六进制码
对于其中的JNZ,查找资料可知:
JNZ/JNE 不为零/不等于 对应16进制码:75
段内直接短转移Jmp short (IP)←(IP)+8位位移量 对应16进制码:EB
这里我们想无条件地执行或跳过判断语句中的操作,就必须要用到无条件跳转指令JMP,并且借助其后的跳转地址来控制功能的ON/OFF。若想实现所有功能全部打开(ON),那么地址偏移应为:1400012B2- 1400012AB=7。
修改前:
修改后:
Step4:验证程序修改后的运行结果
再次执行第1个序列号,原本4个ON、1个OFF的状态已经变成全ON。
Step5:再次修改程序使所有功能关闭
若想实现所有功能全部关闭(OFF),那么地址偏移应为:0。
再次修改:
运行结果:
从运行结果可以看出,所有功能都已关闭(OFF)。
要想生成打开/关闭任意功能的序列号则必须对源码的验证流程有充分的理解,仔细分析源程序,发现其在3个函数中都在不断地对序列号进行反复验证,要想完整理解透彻非常困难。具体的代码分析注释如下几幅图:
1、主函数main:
2、函数sub_140001480(验证2部分):
3、函数sub_140001150(验证3部分):
4、函数sub_1400010B0(在函数sub_140001150中):
最主要的是整个序列号都会在sub_1400010B0函数进行遍历,这一块觉得是最难的,很遗憾并没有研究清楚。所以虽然代码基本看懂,并如上图注释出来了,但是序列号背后的生成机理,特别是sub_1400010B0函数中的移位操作的实际意义并不是很理解。而由于位数太长,仅靠确定的几处去暴力破解剩下部分也基本是不可行的,很可惜第1题暂时只能到此结束,缺少继续下去的思路。
Step1:寻找校验函数
虽然IDA反汇编源程序后,没有看到主函数。但经过动态调试,可以找到主函数的位置。可以发现,0x140002E00这个函数被主函数调用且多达2400行,所以猜测这是校验函数。
Step2:分析校验函数
①第一处校验是长度校验,要求整个输入的长度必须为30:
②第二处校验是整个输入中‘9’的个数必须大于等于3。程序对每个输入计算FNVHASH(vc-x64标准库中std::hash的算法),将得到的哈希值和‘9’的哈希值进行比较:
③第三处校验规定了前9个字符的取值。程序给出了前9个字符的FNV值,通过暴力破解可以得出这9个字符:
④第四处校验规定整个输入的FNV值为5728707748789076223:
⑤第五处校验规定输入中第一个9往后5个字符为系统dll的名字,第二个9和第三个9之间为该dll调用的api名称。
分析如下:
a.调用findch函数搜索输入(从第10位开始)中9所在的位置,从而以9为分界符对输入进行分割:
b.使用FNV算法对kernel32.dll(通过动态调试可知)的api函数进行逐个哈希。将kernel32.dll的所有api进行枚举找到匹配哈希值的api,为LoadLibraryA:
c.对从输入中读取的dll名字调用LoadLibraryA:
d.调用GetProcAddress从对应dll基址处加载从输入读取的api名字,并调用该api函数。如果返回值是负数,则注册成功:
Step3:猜+暴力破解key
根据前面的分析,可知key的形式为{prefix}9{dllName}9{symBolName}9。前9字符的hash都已给出,只要枚举所有字符并将它们的hash值与给出hash比对就可以得出{prefix}的值。{dllName}是系统的dll,其名称长度为5,猜测应该是NTDLL。{symBolName}是{dllName}导出的api函数,它的长度为30(总长)-9({prefix})-3(分隔用的9)-5({dllName})=13。再结合key的hash为5728707748789076223i64,就可以暴力破解出key了。
代码如下:
#include
#include //ida的一些系统定义
#include
char test[0x100] = { 0 };
char ans[10] = { 0 };
//NTDLL导出的api函数
const char symbol[][0x100] = { "TppTimerpFree","TpReleaseWork","TpReleaseWait","RtlAreBitsSet","LdrpUnloadDll","LdrpSnapThunk","RtlUnlockHeap","RtlLoadString","TppTimerAlloc","EtwEventWrite","RtlStartRXact","RtlAbortRXact","TpWaitForWait","RealSuccessor","RebalanceNode","RtlGetVersion","RtlpNtOpenKey","RtlCopyString","RtlSetAllBits","RtlFreeHandle","ZwQueryObject","NtOpenProcess","NtOpenSection","ZwCreateEvent","ZwSetValueKey","NtCancelTimer","ZwAccessCheck","NtAlertThread","NtCompactKeys","NtCompressKey","ZwConnectPort","ZwCreateTimer","NtCreateToken","ZwFilterToken","ZwOpenSession","ZwQueryEaFile","ZwQueryMutant","NtRequestPort","NtSetUuidSeed","ZwStopProfile","ZwUnloadKeyEx","DbgBreakPoint","DebugService2","RtlFillMemory","RtlZeroMemory","StringCbCopyW","RtlRemoteCall","LdrpCreateKey","PfxFindPrefix","PfxInitialize","IsTimeExpired","WaitForWerSvc","WerpProcessId","DbgUiContinue","RtlpLockStack","RtlApplyRXact","TpReleasePool","TpWaitForWork","LdrReadMemory","ResCHitsFlush","RtlCreateHeap","RtlIdnToAscii","RtlpTpIoAlloc","LdrResRelease","Wow64LogPrint","NameToOrdinal","Wow64FreeHeap","whNtWriteFile","whNtReplyPort","whNtCreateKey","whNtOpenEvent","whNtDeleteKey","whNtLoadKeyEx","whNtOpenKeyEx","whNtOpenTimer","whNtRenameKey","whNtSaveKeyEx","whNtSetEaFile","whNtTestAlert","whNtUnloadKey","Wow64pLongJmp" };
//前9个字符hash值+9的hash值
signed __int64 keys[] = { -5808510693665524758i64,
-5808494200991101593i64,
-5808519489758550446i64,
-5808507395130640125i64,
-5808522788293435079i64,
-5808606351177179115i64,
0xAF63AD4C86019CAFi64,
0xAF63AC4C86019AFCi64,
0xAF63B54C8601AA47i64,
0xAF63B44C8601A894i64,0 };
//程序使用的hash函数
signed __int64 fnvHash(char* a1)
{
char v1; // dl
signed __int64 result; // rax
signed __int64 v3; // rax
v1 = *a1;
for (result = 0xCBF29CE484222325i64; *a1; result = 0x100000001B3i64 * v3)
{
++a1;
v3 = result ^ v1;
v1 = *a1;
}
return result;
}
int main() {
unsigned __int64 v183;
LOWORD(v183) = 0;
//求出前10个字符
for (char x = 1; x < 127; x++) {
LOBYTE(v183) = x;
signed __int64 hash = fnvHash((char*)&v183);
for (int i = 0; keys[i]; i++) {
if (keys[i] == hash) {
if (ans[i] == 0)ans[i] = x;
}
}
}
//遍历得出使用的api函数,并打印最终结果
for (int i = 0; symbol[i][0]; i++) {
if (strlen(symbol[i]) == 13) {
strcpy(test, ans);
strcat(test, "NTDLL9");
strcat(test, symbol[i]);
strcat(test, "9");
if (5728707748789076223i64 == fnvHash(test))
printf("Key: %s\n", test);
}
}
return 1;
}
运行结果如下:
故密钥为:KXCTF20189NTDLL9DbgUiContinue9。
Step4:验证
至此可以看到控制台显示:注册成功。
Step1:用upx给genprik.exe脱壳
Step2:使用IDA反汇编已脱壳程序
主函数的代码如上图所示。程序进行一个16次的循环,每次循环产生密钥的一个字节(用rand函数产生一随机数v5,然后对v5进行移位和&操作)。因为rand函数产生的随机数是和种子有关的,所以只要我们能猜出种子的值,就能破解出密钥。种子由系统当前时间和进程ID构造,我们需要破解出这两个值,即v3和PID。
Step3:确定v3和PID的范围
题目提示说,2019/04/11 22点11分之前运行的程序且时间离得不是很远。那我们猜测是在2019/04/11 21:00~22:11这个时间段运行的,调用_time32函数可得出v3的取值范围为1554987616~ 1554991872。PID是进程的id号,一般来说,它不会超过100000。
Step4:暴力破解密钥
已知密钥第1个字节是0x25第2个字节是0x61,把满足该条件所有可能的密钥输出。代码如下:
#include
#include
#include
int main() {
__time32_t v3;
DWORD Seed;
int v5;
signed int v7;
int flag1 = 0;
int flag2 = 0;
int pid;
for (pid = 0; pid < 100000; pid++) {
for (v3 = 1554987616; v3 <= 1554991872; v3++) {
Seed = pid ^ v3;
srand(Seed);
v7 = 0;
do
{
v5 = rand();
if (v7 == 0 && ((v5 >> 7) & 0xFF) == 0x25)
flag1 = 1;
if (v7 == 1 && ((v5 >> 7) & 0xFF) == 0x61)
flag2 = 1;
if (v7 == 1 && flag1 && flag2)
printf("25 ");
if (flag1 && flag2)
printf("%0X ", (v5 >> 7) & 0xFF);
++v7;
} while (v7 < 16);
if (flag1 && flag2)
printf("\n");
flag1 = 0;
flag2 = 0;
}
}
}
运行结果如下:
虽然跑出了很多结果,但实际上都是一样的。
所以完整的16字节密钥为:25 61 6C D5 1D D3 4B CB E7 34 97 93 A4 92 53 1F