欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

****实验Lab6

程序员文章站 2022-04-26 09:39:21
...

赞赏码 & 联系方式 & 个人闲话

****前言

Lab6

1、Yet another crackme (or rather keygenme).

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://*.com/questions/5475790/how-to-disassemble-the-main-function-of-a-stripped-application

 

Task1:by patching, turn on/off all 5 features.

Step1简单运行exe文件

先尝试一下题目所给的两个***,可以看到5个功能各自状态:

****实验Lab6

其中功能E在使用第1个***时被关闭,第2个***时被打开。

Step2使用IDA反编译源程序

使用IDA反编译后查看其伪码,不难找到其进行功能判断的地方:

****实验Lab6

在汇编码中我们也可以轻松找到其对应的部分:

****实验Lab6

Step3修改跳转部分的十六进制码

对于其中的JNZ,查找资料可知:

JNZ/JNE   不为零/不等于   对应16进制码:75

段内直接短转移Jmp short   (IP)←(IP)+8位位移量   对应16进制码:EB

这里我们想无条件地执行或跳过判断语句中的操作,就必须要用到无条件跳转指令JMP,并且借助其后的跳转地址来控制功能的ON/OFF。若想实现所有功能全部打开(ON),那么地址偏移应为:1400012B2- 1400012AB=7。

修改前:

****实验Lab6

修改后

****实验Lab6

Step4验证程序修改后的运行结果

****实验Lab6

再次执行第1个***,原本4个ON、1个OFF的状态已经变成全ON。

Step5:再次修改程序使所有功能关闭

若想实现所有功能全部关闭(OFF),那么地址偏移应为:0。

再次修改:

****实验Lab6

运行结果:

****实验Lab6

从运行结果可以看出,所有功能都已关闭(OFF)。

 

Task2:generate a serial number with arbitrary features turned on/off.(未做出)

要想生成打开/关闭任意功能的***则必须对源码的验证流程有充分的理解,仔细分析源程序,发现其在3个函数中都在不断地对***进行反复验证,要想完整理解透彻非常困难。具体的代码分析注释如下几幅图:

1、主函数main:

****实验Lab6

2、函数sub_140001480(验证2部分):

****实验Lab6

****实验Lab6

3、函数sub_140001150(验证3部分):

****实验Lab6

****实验Lab6

4、函数sub_1400010B0(在函数sub_140001150中):

****实验Lab6

 

****实验Lab6

最主要的是整个***都会在sub_1400010B0函数进行遍历,这一块觉得是最难的,很遗憾并没有研究清楚。所以虽然代码基本看懂,并如上图注释出来了,但是***背后的生成机理,特别是sub_1400010B0函数中的移位操作的实际意义并不是很理解。而由于位数太长,仅靠确定的几处去暴力**剩下部分也基本是不可行的,很可惜第1题暂时只能到此结束,缺少继续下去的思路。

 

2、CTFx6412.exe

Step1:寻找校验函数

虽然IDA反汇编源程序后,没有看到主函数。但经过动态调试,可以找到主函数的位置。可以发现,0x140002E00这个函数被主函数调用且多达2400行,所以猜测这是校验函数。

Step2:分析校验函数

第一处校验是长度校验,要求整个输入的长度必须为30:

****实验Lab6

第二处校验是整个输入中‘9’的个数必须大于等于3。程序对每个输入计算FNVHASH(vc-x64标准库中std::hash的算法),将得到的哈希值和‘9’的哈希值进行比较:

****实验Lab6

第三处校验规定了前9个字符的取值。程序给出了前9个字符的FNV值,通过暴力**可以得出这9个字符:

****实验Lab6

第四处校验规定整个输入的FNV值为5728707748789076223:

****实验Lab6

第五处校验规定输入中第一个9往后5个字符为系统dll的名字,第二个9和第三个9之间为该dll调用的api名称。

分析如下:

a.调用findch函数搜索输入(从第10位开始)中9所在的位置,从而以9为分界符对输入进行分割:

****实验Lab6

b.使用FNV算法对kernel32.dll(通过动态调试可知)的api函数进行逐个哈希。将kernel32.dll的所有api进行枚举找到匹配哈希值的api,为LoadLibraryA:

****实验Lab6

c.对从输入中读取的dll名字调用LoadLibraryA:

****实验Lab6

d.调用GetProcAddress从对应dll基址处加载从输入读取的api名字,并调用该api函数。如果返回值是负数,则注册成功:

****实验Lab6

****实验Lab6

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 <stdio.h>
#include<defs.h>   //ida的一些系统定义
#include<string.h>

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;
}

 运行结果如下:

****实验Lab6

故**为:KXCTF20189NTDLL9DbgUiContinue9。

Step4验证

****实验Lab6

至此可以看到控制台显示:注册成功。

 

3、某文件被加密了,最后修改时间是2019/04/11 22:10:34。猜测大概2019/04/11    22点11分之前,某一刻用户运行了genprik,生成了**加密文件。加密算法已知,通过分析密文,只能得出**第1个字节是0x25第2个字节是0x61。求完整的16个字节的**?

Step1用upx给genprik.exe脱壳

****实验Lab6

Step2使用IDA反汇编已脱壳程序

****实验Lab6

主函数的代码如上图所示。程序进行一个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<stdio.h>
#include <Windows.h>
#include <time.h>  

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;
		}
	}
}

运行结果如下:

****实验Lab6

虽然跑出了很多结果,但实际上都是一样的。

所以完整的16字节**为:25 61 6C D5 1D D3 4B CB E7 34 97 93 A4 92 53 1F

 

相关标签: ****