一、题目
二、分析
决赛题目只有一个驱动,pe工具查看加了vmp,直接加载后dump进行分析。
1、注册机 由于题目前置条件为防止card.txt
到C盘根目录,因此猜测驱动内部为写死路径,因此搜索字符串,找到关键线索。
交叉引用到关键位置后发现无法F5,发现存在vmp,但思考后认为题目考察点不会以分析VMP为主,因此猜测,对应函数为读取card.txt
内容。
因此继续往下分析,具体分析就不贴出了,这里直接给出分析结论;card.txt
给定的内容为ACE-4051574845
(试做题目时给的固定内容)。题目首先将将文本分割为两份,ACE
和 4051574845
。
然后将两串字符串传入对应的计算函数中,得到两串校验值A和B。
最后进行对比,若两串校验值相等,则符合题目要求(user-key正确),否则为不正确
。
因此根据题目要求,可自行编写sys程序对jnz位置进行nop,即可完成(3) 。 进一步对calc_key_14000A368
和calc_user_1400098CC
进行分析,其中calc_user的内容比较少。
反观calc_key的部分就比较多,毕竟且加了混淆(贴图为手动去除混淆之后)。
将代码转换为C++后,对其中部分代码进行分析&调试后,猜测可能为进制转换相关。
于是对字符串4051574845
手动转换后发现与实际得到的校验值一致。
因此可得到<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) { uint32_t key_hex = calc_user (name.c_str (), name.length ()); char strHex[256 ]{}; sprintf_s (strHex, "%x" , key_hex); return hex2dec (strHex); } 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,即验证通过。
将生成的key写入cart.txt
,后调试验证,结果也相同。
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 , 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); }
嗯?eca,ace?进一步过滤一下。
done!
2.2 读内存 题目说反复读取,那就是个死循环咯!首先考虑线程;进一步分析,发现0x1000
的内存页实际上是调用api,只不过是push ret
方式调用。
好办,写个驱动给他遍历出来,gogogo
配合livekd
还原符号(检测了kd.exe
,但没检测windbg
) !!!=。=。
1 2 cd C:\Users\krnl\Desktop\dbg\LiveKDcmd .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+0x5
、nt!MmCopyVirtualMemory+0x2
这两个敏感函数,一眼丁真,大概率跨内存读内存,去下断(ida+gdb)。nt!MmCopyVirtualMemory+0x2
没断,但是nt!KeDelayExecutionThread+0x5
断下了。
由于之前分析,发现整体存在魔改的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 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 ) 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
于是脚本里加了条件进行过滤,遇到首个非PsLookupProcessByProcessId
时停下。
脚本跑了N年。。。。。。终于中断了(说到底也是一直单步跑,效率低)。
这个时候断在了PsGetProcessExitStatus
,这是找到了目标进程,判断进程是否还存活?把过滤PsLookup
那部分的代码注释掉,继续跑脚本看看情况。
继续跑。
OK,又在PsGetProcessExitStatus
断下了,有点思路了,再跑看看。
基本上逻辑已经大概猜到了。通过遍历进程,寻找符合固定条件
的进程名,根据输出的日志,应该是优先判断进程名长度;符合条件后,会暂停遍历,执行PsGetProcessExitStatus
获取进程状态,进行一系列操作后再继续遍历 or 结束遍历。
但有一个疑点就是MmCopyVirtualMemory
一直没被调用,根据目前得到的信息,题目应该就是跨进程读写;日志最后一次与字符串相关的函数为PsGetProcessImageFileName
紧接着就是继续遍历了,因此最有可能进行compare process name
的操作只能是这个函数之后,手动下断后跟踪一下。
直接查看堆栈数据。
不出意外的话,应该就是找这个GameSec.exe
了,但是这个进程貌似不存在进程列表中?让虚拟机跑起来,然后随便把一个进程改成这个名字启动,保险起见,找个64位进程改。。
刚刚修改后,虚拟机直接断了下,返回IDA一看,开头下断的MmCopyVirtualMemory
成功断下!
直接看一下参数吧。
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
使其再断下一次后,返回到调用上层进行参数分析。
继续追[rsp+0x40]来源,最终发现来自一个call。
直接从在0xFFFF900CAF370348
下断后,使用脚本执行。输出调用PsGetProcessPeb
。
手动单步后,刚好就是0x3F0000
。
因此可解答第四问,反复读取进程GameSec.exe
的0x3F0000+0xACE=0x3F0ACE
地址。