腾讯游戏安全2024决赛-PC-WriteUp
题目解读后,可以分为两个块,一个是写注册机,一个是分析内核shellcode的位置和干了什么
分析USER和KEY
在进行这个题目的时候,发现双机调试不能使用,分析的ark
工具也不能使用,基本上这种方式不太行了,但是仍然比不了自己手速!哈哈哈哈!在没有蓝屏的时候用ark dump
了一份load.dump.bin
,哈哈哈哈
因为题目要求将card.txt
放置到C
盘根目录:这个时候发现一个现象,如果我不放到根目录下,或者如果我card.txt
里面的值被我随便修改的情况下可以进行双机调试不会被检测,这个时候自己有了一点想法,(但是还是先做这个问为主),因为这个时候可以进行双机调试所以自己用双机调试做的USER
和KEY
分析的东西
load.dump.bin
中,可以看到路径的名称为:\\??\\C:\\card.txt
,但是回溯不了很烦
还根据ZwCreateFile
入手
NTSYSAPI NTSTATUS ZwCreateFile(
[out] PHANDLE FileHandle,
[in] ACCESS_MASK DesiredAccess,
[in] POBJECT_ATTRIBUTES ObjectAttributes,
[out] PIO_STATUS_BLOCK IoStatusBlock,
[in, optional] PLARGE_INTEGER AllocationSize,
[in] ULONG FileAttributes,
[in] ULONG ShareAccess,
[in] ULONG CreateDisposition,
[in] ULONG CreateOptions,
[in, optional] PVOID EaBuffer,
[in] ULONG EaLength
);
直接看一下ObjectAttributes
里面的ObjectName
的名字
一般来说如果读取文件来说,用的是ZwCreateFile
,进行栈回溯一直跟一直跟,因为在vm
中一定要回到text
段,这个时候我认为的方式是:参数写入后,进行call
lea reg,xxxxxx
call vm
但是感觉栈回溯太多了不太行,然后想了一下看了一下这个地址的address
模块在这个loader.sys
中,计算一下offset = 0x675e20
,然后交叉引用回去即可
这个的路径是xor 0xace
做到的,我这边直接解密,也可以看到code References
的地址
直接windbg
分析后,这个位置不难,很好的可以分析user
算法的位置:
key
的算法位置:
这两个位置我是动态调试出来的,大概算法的表示为-
进行分割后,user
计算后的值转化为10
进制,就是key
的值
注册机:
#include <iostream>
using namespace std;
uint64_t getkey(string user)
{
uint32_t encuser = 0;
for(int i : user)
{
encuser = (i + encuser)*0x1003f;
}
return encuser;
}
int main()
{
string user;
cout << "input your user: " << endl;
cin >> user;
cout << "your key : " << getkey(user) << endl;
return 0;
}
第一问:administrator-4007951923
第二问:如上
第三问:编写一个exp,在exp程序运行后,对于任意的用户名-key,Loader.sys均能正确启动
只需要把jne
-> nop
即可,因为我们不需要对al
进行处理了
mov byte [rsp+0x20 {var_b8}], 0x1
mov al, byte [rsp+0x20 {var_b8}] {0x1}
分析内核内存空间中的shellcode
目前是前三问都OK了,这个时候会出现双机调试的问题,根本调试不了,因为在没有双机调试的环境下进行分析的时候发现不会蓝屏,但是当驱动正常加载后,进行打开ark
这种工具,几秒后还是会蓝屏
想了一下还是在内核当中起了线程(因为第四问也说了:在Load驱动运行后找到内核内存空间中的shellcode),因为一定是线程在做的检测的事情导致的蓝屏,尝试断PsCreateSystemThread
没有发现什么重要的信息,但是一定是有线程跑不然为什么会蓝屏?
所以注册进程回调看有什么异常的,打印ProcessId
和ThreadId
#include <ntddk.h>
VOID ThreadNotifyRoutine(
HANDLE ProcessId,
HANDLE ThreadId,
BOOLEAN Create
)
{
// 打印线程创建或退出的信息
if (Create) {
DbgPrint("Created: Process ID=%p, Thread ID=%p\n", ProcessId, ThreadId);
} else {
DbgPrint("Exited: Process ID=%p, Thread ID=%p\n", ProcessId, ThreadId);
}
}
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
NTSTATUS status;
// 注册线程创建/退出通知回调
status = PsSetCreateThreadNotifyRoutine(ThreadNotifyRoutine);
if (!NT_SUCCESS(status)) {
DbgPrint("Failed to register thread notify routine: %08x\n", status);
return status;
}
DbgPrint("Successfully registered thread notify routine.\n");
DriverObject->DriverUnload = DriverUnload;
return STATUS_SUCCESS;
}
void DriverUnload(PDRIVER_OBJECT DriverObject)
{
// 注销回调
PsRemoveCreateThreadNotifyRoutine(ThreadNotifyRoutine);
DbgPrint("Thread notify routine unregistered.\n");
}
主要观察processid = 4
的,因为初赛也是这样,所以主要观察点在这个位置,发现确实创建了两条很可疑的线程,但是自己在观察这些线程地址的时候,确实没有很奇怪的地址存在,那么很有可能,在线程地址那里做了个跳板,之后进行了恢复所以看到的所有的线程都是很正常的地址
直接使用PsLookupThreadByThreadId
通过threadId
,找到startAddress
然后直接dump
,看一下里面做了什么
发现这里就是一个push mov ret
返回真正的地址位置,这两个都是这种形式
现在可以确定线程的入口是伪造的,当线程启动后会还原入口,所以这就是为什么看的线程的这些都没有啥问题的原因,这个时候猜测的这两个线程肯定是有一个做了检测的工作,另一个做了真正的事情,不然没必要创建两条
所以直接修改入口为ret
,这个时候可以发现可以双机调试了,也可以打开ark
工具,其中的另一个很奇怪的shellcode
如下
这里做了很多xmm
的操作,大概看了一下这个xmm
操作都是混淆,分析的时候直接跳过了这些分析的
现在可以确定的是百分百分确定了线程会被创建,一定会用到PsCreateSystemThread
,但是bp
不到,直接去看一下函数的实现
直接下断PsCreateSystemThreadEx
这里可以获取函数的入口点
但是按照道理来说如果直接ret
的话,不应该会做shellcode
的事情,但是现象是:断下后,瞬间入口还原了,并且蓝屏了,说明有个地方启动了,这里就猜测是插了APC
插入APC:使用KeInsertQueueApc
函数来插入APC到目标线程的APC队列中,直接断KeInsertQueueApc
查看一下KPAC
结构
可以查看到APC
的内容在loader.sys
中
可以看到这里进行了cmp
,直接修改了je -> jmp
就不会蓝屏了
第二次APC
的位置
第二次的时候发现都是xmm
的操作,很难去分析,所以没有管第二次的问题,但是由于题目上说:分析shellcode
反复在读取哪个内存地址,可以确定这里一定是一个while 1
一直在循环操作,为了降低CPU的利用率,猜测会用延时函数,最有可能的就是KeDelayExecutionThread
可以看到找到了这个shellcode
的位置,可以看到是根据rax
来的,往上跟一下
可以看到表的解密方式是xor acacacacacacac
解密出的函数列表:
nt!KeWaitForSingleObject
nt!KeDelayExecutionThread
nt!KeQueryTimeIncrement
nt!KeInitializeApc
nt!KeInsertQueueApc
nt!KeBugCheckEx
nt!ExAllocatePoolWithTag
nt!ExFreePool
nt!RtlInitUnicodeString
nt!RtlUnicodeStringToAnsiString
nt!RtlAnsiStringToUnicodeString
nt!RtlFreeAnsiString
nt!RtlCharToInteger
nt!HalPutDmaAdapter
nt!ZwQueryInformationFile
nt!ZwCreateFile
nt!ZwReadFile
nt!ZwQuerySystemInformation
nt!ZwClose
nt!PsGetThreadId
nt!PsLookupThreadByThreadId
nt!PsCreateSystemThread
nt!PsTerminateSystemThread
nt!PsGetProcessPeb
nt!PsGetProcessWow64Process
nt!PsGetProcessExitStatus
nt!PsGetCurrentThreadId
nt!PsLookupProcessByProcessId
nt!PsGetProcessImageFileName
nt!MmCopyVirtualMemory
nt!MmCopyMemory
nt!MmGetPhysicalAddress
nt!MmMapIoSpace
nt!MmUnmapIoSpace
nt!MmIsAddressValid
nt!PsGetCurrentProcess
nt!SeLocateProcessImageName
nt!DbgPrint
nt!DbgPrintEx
nt!memset
nt!memcpy
nt!memcpy
nt!memcmp
nt!strlen
nt!sqrtf
nt!vsnprintf
nt!vsnwprintf
这样就比较好看了,直接hook
所有的函数,发现一直在循环的遍历进程,感觉有点问题存在,直接过去分析看看做了什么,如果是一直循环遍历进程,自己猜测肯定是在准备比对什么东西,但是没有比对到,直接去分析vm
中的东西
下读断点,断下后,进行分析,发现时逐字节比对,比对是不是GameSec.exe
这个GameSec.exe
需要自己创建这个进程,然后继续分析,直接写了一个helloworld
改个名字丢进去看看做啥了,这个时候不难想到是在读R3
进程的东西
这个时候可以看到了MmCopyVirtualMemory
和PsGetProcessPeb
在一直做事情
直接跟PsGetProcessPeb
回溯
直接对rsp+40h
下访问断点
所以第四问:PsGetProcessPeb + 0xACE
第五问:编写一个search程序,在Load驱动运行后找到内核内存空间中的shellcode,输出shellcode范围内的任意地址 (NMI即可)