2024鹅厂安全竞赛题解-决赛

一、题目

image-20240326155648514

二、分析

决赛题目只有一个驱动,pe工具查看加了vmp,直接加载后dump进行分析。

1、注册机

由于题目前置条件为防止card.txt到C盘根目录,因此猜测驱动内部为写死路径,因此搜索字符串,找到关键线索。

image-20240326160150169

交叉引用到关键位置后发现无法F5,发现存在vmp,但思考后认为题目考察点不会以分析VMP为主,因此猜测,对应函数为读取card.txt内容。

image-20240326160322841

因此继续往下分析,具体分析就不贴出了,这里直接给出分析结论;card.txt给定的内容为ACE-4051574845(试做题目时给的固定内容)。题目首先将将文本分割为两份,ACE 4051574845

image-20240326160830814

然后将两串字符串传入对应的计算函数中,得到两串校验值A和B。

image-20240326161022266

最后进行对比,若两串校验值相等,则符合题目要求(user-key正确),否则为不正确

image-20240326161331270

因此根据题目要求,可自行编写sys程序对jnz位置进行nop,即可完成(3)。 进一步对calc_key_14000A368calc_user_1400098CC进行分析,其中calc_user的内容比较少。

image-20240326163006020

反观calc_key的部分就比较多,毕竟且加了混淆(贴图为手动去除混淆之后)。

image-20240326163021636

将代码转换为C++后,对其中部分代码进行分析&调试后,猜测可能为进制转换相关。

image-20240326163056437

于是对字符串4051574845手动转换后发现与实际得到的校验值一致。

image-20240326163232788

因此可得到<user,key>对应的关系。

1
key = hex2dec(calc_user("xxxxxxx"));

故(1)、(2)答案如下:

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
uint32_t hex2dec(std::string hex)
{
std::stringstream ss2;
uint32_t d2;
ss2 << std::hex << hex; //选用十六进制输出
ss2 >> d2;
return d2;
}

uint32_t start_register(std::string name)
{
// 获取十六进制的key
uint32_t key_hex = calc_user(name.c_str(), name.length());

// 格式化
char strHex[256]{};
sprintf_s(strHex, "%x", key_hex);

return hex2dec(strHex);
}

// 函数calc_user和calc_key实现均为ida拷贝粘贴。具体代码见附件代码
int main()
{
std::string username = "administrator";

// 注册
uint32_t admin_key = start_register(username);

std::cout << "username:" << username << "\t register key:" << admin_key<<std::endl;

char strKey[256]{};
sprintf_s(strKey, "%u", admin_key);

// 验证
char* out = (char*)malloc(256);
uint32_t v1 = calc_user(username.c_str(), username.size());
uint32_t v2 = calc_key(strKey, &out, 10, 1, 0);
std::cout << "v1:" << v1 << "\tv2:" << v2 << std::endl;
}

运行效果,v1==v2,即验证通过。

image-20240326165233048

将生成的key写入cart.txt,后调试验证,结果也相同。

image-20240326170459950

2、shellcode

2.1 search程序实现

​ 题目告知运行时会在内核注入shellcode,并且要求编写search程序输出shellcode地址。这里有个疑惑点,题目指的是程序,没指是R0还是R3?那题目会不会意思是R3就可以完成的。首先就是想到查询大页(类似国际服val的Guard);写个demo测试一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
using fnNtQuerySystemInformation = DWORD(WINAPI*) (UINT systemInformation, PVOID SystemInformation, ULONG SystemInformationLength,PULONG ReturnLength);

fnNtQuerySystemInformation lpNtQuerySystemInformation = (fnNtQuerySystemInformation)GetProcAddress(LoadLibrary(L"ntdll.dll"), "NtQuerySystemInformation");
if (!lpNtQuerySystemInformation)
return;

ULONG size = 1 << 22;
SYSTEM_BIGPOOL_INFORMATION* m_BigPools = static_cast<SYSTEM_BIGPOOL_INFORMATION*>(VirtualAlloc(nullptr, size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE));;

ULONG len;
auto status = lpNtQuerySystemInformation(66/*SystemBigPoolInformation*/, m_BigPools, size, &len);
if (status) {
printf("lpNtQuerySystemInformation error\n");
return;
}

for (int i = 0; i < m_BigPools->Count; i++) {
const auto& info = m_BigPools->AllocateInfo[i];
printf("tag = %s base = 0x%p\n", info.Tag, info.VirtualAddress);
}

image-20240326235145806

嗯?eca,ace?进一步过滤一下。

image-20240326235645865

done!

2.2 读内存

​ 题目说反复读取,那就是个死循环咯!首先考虑线程;进一步分析,发现0x1000的内存页实际上是调用api,只不过是push ret方式调用。

image-20240401224408840

​ 好办,写个驱动给他遍历出来,gogogo

image-20240401224532222

​ 配合livekd还原符号(检测了kd.exe,但没检测windbg) !!!=。=。

1
2
cd C:\Users\krnl\Desktop\dbg\LiveKD
cmd.exe /K "livekd64.exe -vsym -w -k "C:\Users\krnl\Desktop\dbg\x64\windbg.exe""

​ 有那么多。

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
0: kd> dqs FFFFF8036B033090 l100
fffff803`6b033090 fffff803`643b6265 nt!MmUnmapIoSpace+0x5
fffff803`6b033098 fffff803`64460263 nt!strlen+0x3
fffff803`6b0330a0 fffff803`643c85d7 nt!PsGetProcessWow64Process+0x7
fffff803`6b0330a8 fffff803`648e1eb2 nt!MmCopyVirtualMemory+0x2
fffff803`6b0330b0 fffff803`643d74d7 nt!PsGetThreadId+0x7
fffff803`6b0330b8 fffff803`642ff657 nt!RtlInitUnicodeString+0x7
fffff803`6b0330c0 fffff803`6445ddc4 nt!vsnwprintf+0x4
fffff803`6b0330c8 fffff803`643669c5 nt!KeInsertQueueApc+0x5
fffff803`6b0330d0 fffff803`64494e43 nt!memcpy+0x3
fffff803`6b0330d8 fffff803`649aafe3 nt!PsCreateSystemThread+0x3
fffff803`6b0330e0 fffff803`643b7864 nt!MmMapIoSpace+0x4
fffff803`6b0330e8 fffff803`648edcd2 nt!RtlFreeAnsiString+0x2
fffff803`6b0330f0 fffff803`642fbb55 nt!KeWaitForSingleObject+0x5
fffff803`6b0330f8 fffff803`643deae3 nt!DbgPrintEx+0x3
fffff803`6b033100 fffff803`6462f0a4 nt!ExFreePool+0x4
fffff803`6b033108 fffff803`64586144 nt!MmIsAddressValid+0x4
fffff803`6b033110 fffff803`6445def4 nt!vsnprintf+0x4
fffff803`6b033118 fffff803`643d0479 nt!PsGetCurrentThreadId+0x9
fffff803`6b033120 fffff803`643deb23 nt!DbgPrint+0x3
fffff803`6b033128 fffff803`6445f493 nt!memcmp+0x3
fffff803`6b033130 fffff803`649a5144 nt!PsTerminateSystemThread+0x4
fffff803`6b033138 fffff803`643b6265 nt!MmUnmapIoSpace+0x5
fffff803`6b033140 fffff803`6447e853 nt!ZwClose+0x3
fffff803`6b033148 fffff803`648e1eb2 nt!MmCopyVirtualMemory+0x2
fffff803`6b033150 fffff803`642ff657 nt!RtlInitUnicodeString+0x7
fffff803`6b033158 fffff803`6445ddc4 nt!vsnwprintf+0x4
fffff803`6b033160 fffff803`64328fc9 nt!PsGetCurrentProcess+0x9
fffff803`6b033168 fffff803`6447e893 nt!ZwQueryInformationFile+0x3
fffff803`6b033170 fffff803`64b87956 nt!PsGetProcessExitStatus+0x6
fffff803`6b033178 fffff803`643669c5 nt!KeInsertQueueApc+0x5
fffff803`6b033180 fffff803`64494e43 nt!memcpy+0x3
fffff803`6b033188 fffff803`643dcdc4 nt!MmGetPhysicalAddress+0x4
fffff803`6b033190 fffff803`649aafe3 nt!PsCreateSystemThread+0x3
fffff803`6b033198 fffff803`642fbb55 nt!KeWaitForSingleObject+0x5
fffff803`6b0331a0 fffff803`648dc215 nt!PsLookupProcessByProcessId+0x5
fffff803`6b0331a8 fffff803`6447ed33 nt!ZwQuerySystemInformation+0x3
fffff803`6b0331b0 fffff803`64460263 nt!strlen+0x3
fffff803`6b0331b8 fffff803`64586144 nt!MmIsAddressValid+0x4
fffff803`6b0331c0 fffff803`643c85d7 nt!PsGetProcessWow64Process+0x7
fffff803`6b0331c8 fffff803`64349fa6 nt!KeQueryTimeIncrement+0x6
fffff803`6b0331d0 fffff803`642f9fe5 nt!ObfDereferenceObject+0x5
fffff803`6b0331d8 fffff803`64495183 nt!memset+0x3
fffff803`6b0331e0 fffff803`643f7a15 nt!MmCopyMemory+0x5
fffff803`6b0331e8 fffff803`648c24b4 nt!SeLocateProcessImageName+0x4
fffff803`6b0331f0 fffff803`643d0479 nt!PsGetCurrentThreadId+0x9
fffff803`6b0331f8 fffff803`642f6f05 nt!KeDelayExecutionThread+0x5
fffff803`6b033200 fffff803`643e3937 nt!PsGetProcessImageFileName+0x7
fffff803`6b033208 fffff803`6462f0a4 nt!ExFreePool+0x4
fffff803`6b033210 fffff803`6447e853 nt!ZwClose+0x3
fffff803`6b033218 fffff803`6445def4 nt!vsnprintf+0x4
fffff803`6b033220 fffff803`643e0b67 nt!PsGetProcessPeb+0x7
fffff803`6b033228 fffff803`64328fc9 nt!PsGetCurrentProcess+0x9
fffff803`6b033230 fffff803`6438c2c3 nt!KeInitializeApc+0x3
fffff803`6b033238 fffff803`6447e893 nt!ZwQueryInformationFile+0x3
fffff803`6b033240 fffff803`64494e43 nt!memcpy+0x3
fffff803`6b033248 fffff803`648dc4a3 nt!PsLookupThreadByThreadId+0x3
fffff803`6b033250 fffff803`643dcdc4 nt!MmGetPhysicalAddress+0x4
fffff803`6b033258 fffff803`6462f015 nt!ExAllocatePoolWithTag+0x5
fffff803`6b033260 fffff803`64944865 nt!RtlAnsiStringToUnicodeString+0x5
fffff803`6b033268 fffff803`643deb23 nt!DbgPrint+0x3
fffff803`6b033270 fffff803`648dc215 nt!PsLookupProcessByProcessId+0x5
fffff803`6b033278 fffff803`6447e733 nt!ZwReadFile+0x3

​ 看到nt!KeDelayExecutionThread+0x5nt!MmCopyVirtualMemory+0x2这两个敏感函数,一眼丁真,大概率跨内存读内存,去下断(ida+gdb)。nt!MmCopyVirtualMemory+0x2没断,但是nt!KeDelayExecutionThread+0x5断下了。

image-20240401225455303

​ 由于之前分析,发现整体存在魔改的llvm,去对shellcode分析,有点麻烦,但题目说是反复读取,常规思路就是while循环+sleep作为线程跑。领域展开!上脚本。

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
#
# 自动F7,记录调用的shellcode,然后对shellcode里进行特征码搜索匹配地址后识别符号
#

def get_api_addr(addr):
ret = idc.find_binary(addr,SEARCH_DOWN,'68 ?? ?? ?? ?? C7 44 24 04 ?? ?? ?? ?? C3')
if ret != idc.BADADDR:
low = idc.get_wide_dword(ret+1)
high = idc.get_wide_dword(ret+9)
return (high<<32) +low
return 0


def auto_detect():
while True:
ida_dbg.step_into()
ida_dbg.wait_for_next_event(WFNE_ANY,-1)

curt_addr = idc.get_screen_ea()
mnen = idc.print_insn_mnem(curt_addr)
if mnen == 'call':
optype = idc.get_operand_type(curt_addr,0)
# call reg
if optype == o_reg:
reg_name = idc.print_operand(curt_addr,0)
reg_value = ida_dbg.get_reg_val(reg_name)
api_addr = get_api_addr(reg_value)
args=[]
if api_addr > 0:
api_name = idc.get_func_name(api_addr)
if 'PsLookupProcessByProcessId' in api_name:
# 打印参数
args.append(hex(ida_dbg.get_reg_val('rcx')))
elif 'RtlUnicodeStringToAnsiString' in api_name:
src_addr = ida_dbg.get_reg_val('rdx')
buffer_len = idc.get_wide_word(src_addr)
buffer_ptr = idc.get_qword(src_addr+8)
datas = idc.get_bytes(buffer_ptr, buffer_len)
args.append(datas.decode('utf-16-le'))
elif 'RtlFreeAnsiString' in api_name:
src_addr = ida_dbg.get_reg_val('rdx')
buffer_len = idc.get_wide_word(src_addr)
buffer_ptr = idc.get_qword(src_addr+8)
datas = idc.get_bytes(buffer_ptr, buffer_len)
args.append(datas.decode('gbk'))
print('oh~ yeh baby!!! -> addr:{0} call reg | {1}:{2} args:{3}'.format(hex(curt_addr),hex(reg_value),api_name,','.join(args)))
else:
print('err ',hex(curt_addr))

break

auto_detect()

​ 直接从Delay函数的返回地址开始跑。执行发现一直在跑PsLookupProcessByProcessId

QQ图片20240401230010

​ 于是脚本里加了条件进行过滤,遇到首个非PsLookupProcessByProcessId时停下。

image-20240401235258800

​ 脚本跑了N年。。。。。。终于中断了(说到底也是一直单步跑,效率低)。

image-20240402000807785

​ 这个时候断在了PsGetProcessExitStatus,这是找到了目标进程,判断进程是否还存活?把过滤PsLookup那部分的代码注释掉,继续跑脚本看看情况。

image-20240402003145758

​ 继续跑。

image-20240402003559051

​ OK,又在PsGetProcessExitStatus断下了,有点思路了,再跑看看。

image-20240402004821470

​ 基本上逻辑已经大概猜到了。通过遍历进程,寻找符合固定条件的进程名,根据输出的日志,应该是优先判断进程名长度;符合条件后,会暂停遍历,执行PsGetProcessExitStatus获取进程状态,进行一系列操作后再继续遍历 or 结束遍历。

​ 但有一个疑点就是MmCopyVirtualMemory一直没被调用,根据目前得到的信息,题目应该就是跨进程读写;日志最后一次与字符串相关的函数为PsGetProcessImageFileName紧接着就是继续遍历了,因此最有可能进行compare process name的操作只能是这个函数之后,手动下断后跟踪一下。

image-20240402005709687

​ 直接查看堆栈数据。

image-20240402005824758

​ 不出意外的话,应该就是找这个GameSec.exe了,但是这个进程貌似不存在进程列表中?让虚拟机跑起来,然后随便把一个进程改成这个名字启动,保险起见,找个64位进程改。。

image-20240402010202989

​ 刚刚修改后,虚拟机直接断了下,返回IDA一看,开头下断的MmCopyVirtualMemory成功断下!

image-20240402010244262

​ 直接看一下参数吧。

image-20240402010349753

1
2
3
4
5
6
7
rcx = 0x0xFFFF900CB396A080
rdx = 0x00000000003F0ACE ; ACE? 很熟悉的东西,初赛就有
r8 = 0xFFFF900CAD894300
r9 = 0xFFFFFB8DE3C798AF
[rsp+0x20] = 0x00000001
[rsp+0x28] = 0x00000000
[rsp+0x30] = 0xFFFFFB8DE3C79848

​ 根据初赛经验,0xACE应该是偏移,因此实际读取的地址应该是0x3F0000,重新回到MmCopyVirtualMemory使其再断下一次后,返回到调用上层进行参数分析。

image-20240402013644573

​ 继续追[rsp+0x40]来源,最终发现来自一个call。

image-20240402014349648

​ 直接从在0xFFFF900CAF370348下断后,使用脚本执行。输出调用PsGetProcessPeb

image-20240402014747663

​ 手动单步后,刚好就是0x3F0000

image-20240402014856725

​ 因此可解答第四问,反复读取进程GameSec.exe0x3F0000+0xACE=0x3F0ACE地址。