This page looks best with JavaScript enabled

NepCTF2022 Qriver2.0出题思路

 ·  ☕ 7 min read · 👀... views

关于怎么出这道题其实想了很久,如何将我最近学习的东西和CTF联系起来,并出一道难度适中,考察面较广,且不恶心人的题。我个人比较反感CTF题中卷一堆乱七八糟很复杂的加密算法,所以这次题目的算法是非常简单并且是加解密同函数的,只要分析完程序逻辑就能马上解出该题而不用花大量时间在那里扣算法。但是又不希望题目被静态直接秒了,(比如去年NepCTF的Qriver是个Windows驱动+VM,直接有大师傅静态秒了),希望做题者能够有一定的驱动调试能力和反调试绕过能力,所以配合混淆、反调试出了这么一道题。

个人最近一年花了较多时间在混淆对抗上,同时也会写写Windows驱动。所以这次的题目选定为Windows驱动+LLVM混淆。接下来以 混淆分析、反调试和逻辑分析 三部分来分解这道题。

End:看了比赛结果,很遗憾没有师傅做出来,而且似乎也没有师傅挂双机调试去调试,也就是我设计的很多思路都没用用到,略遗憾。

混淆分析

这次题目主要使用了三种混淆:字符串混淆宏,平坦化,全局变量间接跳转。同时为了考虑题目难度,并没有无差别添加混淆,只对部分字符串、函数、全局变量添加了混淆

  1. 字符串混淆宏:C++的常量表达式真的是最恶心的东西,天生用于写混淆的利器。字符串混淆我主要用于DriverEntry中的DbgPrint调试、错误信息输出上,通过对这些信息加混淆可以直接干碎F5的伪代码。比如可以看到IDA CFG汇编是非常清晰的逐层判定并注册驱动
    DriverEntry_1
    但是F5后的伪代码很恶心
    DriverEntry_2
    对抗这种字符串混淆宏最好的办法其实就是调试,通过脚本模式匹配并修复的成本较高,静态看汇编也可以但是这题我使用了C++,会有大量的构造函数,静态会受到较大的干扰(其实就是想方设法让做题者不能静态秒

  2. 平坦化:这题我是用LLVM Pass对几个关键算法函数进行了平坦化,并对平坦化做了一定程度的修改。
    OLLVM 之控制流平坦化源码学习与魔改思路
    虽然但是,这种轻度的小魔改还是可以被一把梭的,比如IDA的D810插件,我们可以看一下效果图
    fla

  3. 全局变量间接跳转:同样也是用LLVM Pass实现。这题没有加太多全局变量间接跳转,因为全局变量很少而且大部分关键字符串都已经加上字符串混淆宏了,仅仅只有几个类对象的传递位置加了混淆。比如这里是用D810去平坦化后看到的效果,就是由全局变量间接跳转混淆造成的
    IndGV
    具体关于这种混淆是实现原理可以看 OLLVM 之全局变量间接访问源码学习

以上就是这题加的全部混淆了,剩下的看到很多函数调用和空函数,均是C++ stl生成的,主要目的还是希望做题者能以动态调试的方法解得该题。

反调试

如果做题者用双机调试来调试这道题,就会发现进入DriverEntry不久后调试器就会未响应,这是由于反调试造成的。以下贴出反调试的关键代码:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
class AntiDebug
{
public:
	
	~AntiDebug() { UninstallAntiDebugThread(); };
	static AntiDebug& Instance();

	NTSTATUS Init();
	BOOL DetectKernelDebug();
	BOOL DetectProcessDebug(HANDLE pid);
	BOOL AntiKernelDebug();
	BOOL AntiProcessDebug(HANDLE pid);

	NTSTATUS InstallAntiDebugThread();
	NTSTATUS UninstallAntiDebugThread();
	NTSTATUS InstallAntiKernelDebug();
	NTSTATUS UninstallAntiKernelDebug();
	NTSTATUS InstallAntiProcessDebug(HANDLE pid);
	NTSTATUS UninstallAntiProcessDebug(HANDLE pid);

	BOOL bAntiDebugThread;						
	BOOL bAntiKernelDebug;				

private:
	AntiDebug() {};
	static VOID AntiDebugRoutine(PVOID pContext);	

	ULONG mKdDebuggerEnabledOffset;
	ULONG mKdDebuggerNotPresentOffset;
	PVOID pThreadObj;		
	KSPIN_LOCK spin_lock;
	std::vector<HANDLE> anti_debug_process_list;	
};

AntiDebug& AntiDebug::Instance()
{
	static AntiDebug inst;
	return inst;
}

BOOL AntiDebug::AntiKernelDebug() {
	BOOL bRet = TRUE;
	KdDisableDebugger();
	return bRet;
}

NTSTATUS AntiDebug::InstallAntiKernelDebug() {
	bAntiKernelDebug = TRUE;
	return STATUS_SUCCESS;
}

NTSTATUS AntiDebug::InstallAntiDebugThread() {
	HANDLE hThread;
	NTSTATUS nStatus = STATUS_SUCCESS;

	if (bAntiDebugThread == TRUE)
		return nStatus;

	bAntiDebugThread = TRUE;

	//创建线程
	nStatus = PsCreateSystemThread(&hThread,
		THREAD_ALL_ACCESS,
		NULL,
		(HANDLE)0,
		NULL,
		AntiDebugRoutine,
		this);
	if (!NT_SUCCESS(nStatus)) {
		return nStatus;
	}
		
	if (KeGetCurrentIrql() != PASSIVE_LEVEL)
		nStatus = KfRaiseIrql(PASSIVE_LEVEL);

	// 获得线程对象
	nStatus = ObReferenceObjectByHandle(hThread,
		THREAD_ALL_ACCESS,
		NULL,
		KernelMode,
		&pThreadObj,
		NULL);
	if (!NT_SUCCESS(nStatus)) {
		ZwClose(hThread);			
		bAntiDebugThread = FALSE;		
		return nStatus;
	}
		
	ZwClose(hThread);
	return nStatus;
}

VOID AntiDebug::AntiDebugRoutine(PVOID pContext) {

	AntiDebug* instance = reinterpret_cast<AntiDebug*>(pContext);
	
	while (instance->bAntiDebugThread == TRUE)
	{
		// 内核反调试
		if (instance->bAntiKernelDebug == TRUE)
			instance->AntiKernelDebug();

		// 三环反调试
		KIRQL Irql;
		KeAcquireSpinLock( &(instance->spin_lock), &Irql );
		for (auto pid : instance->anti_debug_process_list)
			instance->AntiProcessDebug(pid);
		KeReleaseSpinLock(&(instance->spin_lock), Irql);

		Utils::Sleep(2000);
	}
	
	PsTerminateSystemThread(STATUS_SUCCESS);
}

在DriverEntry中,会先初始化反调试,并拉起反调试线程,在Check函数前会在检测反调试线程与内核反调试标志是否处于开启状态(防止做题者直接patch掉反调试线程启动call),如果未开启则不会进入Check函数去判断。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
if (!NT_SUCCESS(AntiDebug::Instance().Init())) {
    kprintf("[Qriver] : Init Error!\n");
    return STATUS_FAILED_DRIVER_ENTRY;
}

// 这里创建设备并建立符号链接

AntiDebug::Instance().InstallAntiKernelDebug();
AntiDebug::Instance().InstallAntiDebugThread();

// 格式检查并取花括号内反转

if (AntiDebug::Instance().bAntiDebugThread == FALSE	|| 
    AntiDebug::Instance().bAntiKernelDebug == FALSE	||
    CheckInput( const_cast<char*>(serials.c_str()), serials.length() ) == FALSE)
{
    // False
}

// Success

对于反调试的处理,方法主要有三种:

  1. patch InstallAntiKernelDebug或InstallAntiDebugThread,在调试时手动过掉对bAntiDebugThread和bAntiKernelDebug两个标志位的判断。
  2. 写一个驱动Demo,Hook KdDisableDebugger。Windows驱动反调试也就那么几种方法,不像三环群魔乱舞。IDA看导入表,交叉引用KdDisableDebugger,可以马上找到这个位置调用了该API实现反调试,回溯可以看到这个函数在循环体内并是由PsCreateSystemThread创建的线程函数。(其实分析到这里了直接patch这条 call KdDisableDebugger 也可以,本来想在这个位置加个CRC防止被patch
    KdDisableDebugger
  3. VT调试器。这没什么好说的,会用这玩意做这个应该是乱秒。

逻辑分析

其实,把混淆、反调试都解决掉,挂个调试器看,这题就是非常简单的一题。这里我将逻辑部分核心代码贴出。

驱动外壳部分主要去获得输入,并做格式检查,然后对花括号内容做了一次翻转(专门设计起来卡做题者静态分析出程序的

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
std::wstring GetInput() {
	Registry reg;
	if (reg.Open(Registry::HKEY_LOCAL_MACHINE, xorstr(L"SOFTWARE\\Nepnep\\Qriver2.0").crypt_get()) == TRUE)
	{
		wchar_t szBuf[MAX_PATH] = { 0 };
		size_t BufferLen = MAX_PATH * 2;
		if (reg.GetValue(xorstr(L"flag").crypt_get(), REG_SZ, szBuf, BufferLen) == TRUE)
		{
			return std::wstring(szBuf);
		}
	}
	return L"";
}

extern "C" NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
	// 初始化
	if (!NT_SUCCESS(KernelOffsets::init())) {
		kprintf("[Qriver] : Windows Version Too High Or Too Low!\n");
		return STATUS_FAILED_DRIVER_ENTRY;
	}
	if (!NT_SUCCESS(AntiDebug::Instance().Init())) {
		kprintf("[Qriver] : Init Error!\n");
		return STATUS_FAILED_DRIVER_ENTRY;
	}

	// 获取输入
	std::string input = Utils::WstringToAnsi(  (wchar_t*)(GetInput().c_str()) );

	// 格式检查
	if (input == "" || input.find("NepCTF{") != 0 || input.c_str()[input.length() - 1] != '}') {
		IoDeleteSymbolicLink(&DeviceLink);
		IoDeleteDevice(DriverObject->DeviceObject);
		AntiDebug::Instance().~AntiDebug();
		return STATUS_ACCESS_DENIED;
	}
	// kprintf("[Qriver] : Input:%s\n", input.c_str());

	// 取花括号内并反转
	std::string serials = input.substr(7, input.length() -7 - 1);
	serials = std::string(serials.rbegin(), serials.rend());
	// kprintf("[Qriver] : Serials:%s\n", serials.c_str());


	if (AntiDebug::Instance().bAntiDebugThread == FALSE	|| 
		AntiDebug::Instance().bAntiKernelDebug == FALSE	||
		CheckInput( const_cast<char*>(serials.c_str()), serials.length() ) == FALSE)
	{
		IoDeleteSymbolicLink(&DeviceLink);
		IoDeleteDevice(DriverObject->DeviceObject);
		AntiDebug::Instance().~AntiDebug();
		return STATUS_ACCOUNT_DISABLED;
	}

	auto driver_exit = std::experimental::make_scope_exit([&]() { kprintf("[Qriver] : Success\n"); });
	return STATUS_SUCCESS;
}

算法部分

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
class Crypto {
private:
    int length;
    BYTE buffer[0x100];
    void decrypto(DWORD* a1, DWORD a2);
public:
    Crypto() {};
    ~Crypto() {};
    BYTE cipher[0x100] = {};

    BOOL set_input(char input[], int length);
    BYTE* get_buffer() { return (BYTE*)buffer; };
    void call_decrypt();
    BOOL check();
}inst;

BYTE cipher[] = {0xC9, 0x5A, 0x05, 0xF3, 0x5C, 0x0C, 0xE4, 0x54, 0x0A, 0xE6, 0x55, 0x53, 0xE2, 0x52, 0x55, 0x54, 0xCA, 0x5D, 0x02, 0xF1, 0x5B, 0x0C, 0xE6, 0x53, 0x0C, 0xE5, 0x54, 0x55, 0xE1, 0x54, 0x53, 0x51};

// 复制输入到类  
BOOL Crypto::set_input(char input[], int length)
{
    this->length = length;
    if(this->length >= 0x100)
        return FALSE;

    // memcpy
    for(int i = 0; i < this->length; i++)
        buffer[i] = input[i];
    
    return TRUE;
}

void  Crypto::decrypto(DWORD* a1, DWORD a2)
{
    int i; 
    int* v4; 
    int j; 
    char v6; 
    DWORD result; 
    int v8; 
    char buf[16]; 

    buf[1] = a2 ^ 0x24;
    buf[0] = a2 ^ 0x6A;
    buf[8] = a2 ^ 0x1A;
    buf[2] = a2 ^ 0x35;
    buf[9] = a2 ^ 0x8D;
    buf[3] = a2 ^ 0x23;
    buf[11] = a2 ^ 0x9A;
    buf[4] = a2 ^ 0xCA;
    buf[5] = a2 ^ 0x4B;
    buf[6] = a2 ^ 0x21;
    buf[7] = a2 ^ 0x35;
    buf[10] = a2 ^ 0x91;
    buf[12] = a2 ^ 0x2C;
    buf[13] = a2 ^ 0xC2;
    buf[14] = a2 ^ 0x92;
    buf[15] = a2 ^ 0x7;
    for (i = 0; i < 4; ++i)
    {
        v4 = &v8;
        v8 = a2 ^ *a1;
        for (j = 0; j < 4; ++j)
        {
            v4 = (int*)((char*)v4 + 1);
            v6 = j | ((BYTE)j << j) | buf[15 - (((BYTE)j + (BYTE)i) & 0xF)];
            *((BYTE*)v4 - 1) ^= v6 | 4;
        }
        result = v8 ^ (DWORD)~a2;
        *a1++ = result;
    }
}


void Crypto::call_decrypt() {
    BYTE* p = this->buffer;
    int i = 0;
    do
    {
        decrypto((DWORD*)p, i++);
        p = p + 0x10;
    } while (i < (this->length >> 4));
}

BOOL Crypto::check() {

    for(int i = 0; i < 0x100; ++i) {
        if( this->buffer[i] != this->cipher[i] )
            return FALSE;
    }
    return TRUE;
}

BOOL CheckInput(char input[], int length) {

    // 将cipher复制到类实例成员中
    for(int i=0;i<32;++i)
        inst.cipher[i] = cipher[i];

    if( inst.set_input(input, length) == FALSE )
        return FALSE;
    inst.call_decrypt();

    return inst.check();
}

其中,Crypto::decrypto也就是唯一的一个加密函数,这个加密算法是从某商业软件中抠出来的,其加密与解密逻辑是相同的,意思是,只要把该函数的逻辑扣出来(D810可以一把梭),连逆都不用逆,直接将cipher传入再反转一下就可以得到flag了。

EXP

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
#include <stdio.h>
#include <windows.h>

BYTE cipher[] = {0xC9, 0x5A, 0x05, 0xF3, 0x5C, 0x0C, 0xE4, 0x54, 0x0A, 0xE6, 0x55, 0x53, 0xE2, 0x52, 0x55, 0x54, 0xCA, 0x5D, 0x02, 0xF1, 0x5B, 0x0C, 0xE6, 0x53, 0x0C, 0xE5, 0x54, 0x55, 0xE1, 0x54, 0x53, 0x51};

void  decrypto(DWORD* a1, DWORD a2)
{
    int i; 
    int* v4; 
    int j; 
    char v6; 
    DWORD result; 
    int v8; 
    char buf[16]; 

    buf[1] = a2 ^ 0x24;
    buf[0] = a2 ^ 0x6A;
    buf[8] = a2 ^ 0x1A;
    buf[2] = a2 ^ 0x35;
    buf[9] = a2 ^ 0x8D;
    buf[3] = a2 ^ 0x23;
    buf[11] = a2 ^ 0x9A;
    buf[4] = a2 ^ 0xCA;
    buf[5] = a2 ^ 0x4B;
    buf[6] = a2 ^ 0x21;
    buf[7] = a2 ^ 0x35;
    buf[10] = a2 ^ 0x91;
    buf[12] = a2 ^ 0x2C;
    buf[13] = a2 ^ 0xC2;
    buf[14] = a2 ^ 0x92;
    buf[15] = a2 ^ 0x7;
    for (i = 0; i < 4; ++i)
    {
        v4 = &v8;
        v8 = a2 ^ *a1;
        for (j = 0; j < 4; ++j)
        {
            v4 = (int*)((char*)v4 + 1);
            v6 = j | ((BYTE)j << j) | buf[15 - (((BYTE)j + (BYTE)i) & 0xF)];
            *((BYTE*)v4 - 1) ^= v6 | 4;
        }
        result = v8 ^ (DWORD)~a2;
        *a1++ = result;
    }

}

int main() {

    BYTE* p = cipher;
    int i = 0;
    do
    {
        decrypto((DWORD*)p, i++);
        p = p + 0x10;
    } while (i < (0x20 >> 4));

    // 反转
    for(int i=0x20-1;i>=0;--i)
        printf("%c", cipher[i]);

    return 0;
}
Share on

Qfrost
WRITTEN BY
Qfrost
CTFer, Anti-Cheater, LLVM Committer