主要针对https://github.com/250wuyifan/ChangmenEdr/blob/main/ChangmenDriver/SyscallTrace.c,我的edr使用了这个技术进行hook敏感函数,从https://github.com/Xacone/BestEdrOfTheMarket抄过来的技术,可以去他博客看看他的描述
所以这个意思就是数组注册了一个函数,然后修改ethread的某些成员即可调用这个函数,作为一个hook,我们根据他的说明自己实际分析一波。
正常系统调用 ida打开测试机的ntdll.dll来分析正常的系统调用,比如WriteProcessMemory函数最终就调用ntdll里的NtWriteVirtualMemory。如下图,mov eax, 3Ah,就说明这个系统调用号是NtWriteVirtualMemory,然后使用syscall进行调用这个函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 用户程序 │ ▼ KERNEL32!WriteProcessMemory ← jmp 转发给 KERNELBASE │ ▼ KERNELBASE!WriteProcessMemory ← 真正实现,组装参数 │ ▼ ntdll!NtWriteVirtualMemory ← syscall stub │ mov r10, rcx │ mov eax, 0x3A ← 系统调用号 │ test [SharedUserData+308], 1 │ syscall ← 进内核 ▼ nt!KiSystemCall64 ← 内核统一入口 │ ▼ nt!NtWriteVirtualMemory ← 内核实现
win10 22h2 syscall分析 上面我们知道是要使用syscall进行调用函数的,我们随便写个函数进行调用NtWriteVirtualMemory,然后在这个程序下断点。
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 #include <windows.h> #include <stdio.h> int main () { DWORD target_pid = GetCurrentProcessId(); LPVOID addr = VirtualAlloc(NULL , 0x1000 , MEM_COMMIT, PAGE_READWRITE); char buf[] = "hello" ; SIZE_T size = sizeof (buf); HANDLE h = OpenProcess(PROCESS_ALL_ACCESS, FALSE, target_pid); if (h == NULL ) return 1 ; printf ("PID: %d, addr: %p\nPress Enter...\n" , target_pid, addr); HMODULE ntdll = GetModuleHandleA("ntdll.dll" ); printf ("ntdll base: %p\n" , ntdll); getchar(); WriteProcessMemory(h, addr, buf, size, NULL ); printf ("done, press Enter to exit\n" ); getchar(); VirtualFree(addr, 0 , MEM_RELEASE); CloseHandle(h); return 0 ; }
windbg找进程
1 !process 0 0 <进程名> ; 找 EPROCESS 地址
断点该进程的函数
1 bp /p <EPROCESS> <地址> ; 进程绑定断点
继续执行,然后断点成功,k 看一下栈的情况,顺便反编译一下这个函数。
KiSystemCall64 → KiSystemServiceUser KiSystemCall64 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 // KiSystemCall64 入口 swapgs mov [gs:0x10], rsp // 保存用户态 RSP mov rsp, [gs:0x1a8] // 切换到内核栈 // 构建 TrapFrame push 0x2b // 用户态 SS push [gs:0x10] // 用户态 RSP push r11 // 用户态 RFLAGS push 0x33 // 用户态 CS push rcx // 用户态 RIP(syscall 约定:返回地址在 rcx) // 保存用户态参数寄存器 mov [rbp-0x50], rax // TrapFrame->Rax = 系统调用号 mov [rbp-0x48], rcx // TrapFrame->Rcx = 第1个参数 mov [rbp-0x40], rdx // TrapFrame->Rdx = 第2个参数 // 取当前线程 mov rbx, [gs:0x188] // GS:0x188 → 当前 KTHREAD*
1 2 3 4 5 6 7 8 ; windbg看完整函数 uf nt!KiSystemCall64 ; 关键行 fffff805`7121126e 807b0300 cmp byte ptr [rbx+3], 0 fffff805`71211294 f6430324 test byte ptr [rbx+3], 24h fffff805`712112be call nt!PsAltSystemCallDispatch fffff805`712112c3 cmp al, 1
Alt-Syscall 触发判断(核心) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 // KiSystemServiceUser 里的关键判断 mov rbx, [gs:0x188] // rbx = 当前 KTHREAD* cmp byte [rbx+0x3], 0x0 // KTHREAD+3 标志字节是否为0? je KiSystemServiceStart // 为0:跳过,走正常分发 test byte [rbx+0x3], 0x3 // bit0/1:调试相关 je → 跳过 debug 寄存器保存 test byte [rbx+0x3], 0x24 // bit2(0x04) | bit5(0x20) je KiSystemServiceStart // 没有 alt 标志:跳过 // 有 alt 标志,调用分发器 mov rcx, rsp // rcx = TrapFrame 指针(参数) call PsAltSystemCallDispatch cmp al, 0x1 // 返回值 == 1? je KiSystemServiceStart // 是:继续正常 syscall // 否:拦截,不执行实际 syscall
PsAltSystemCallDispatch 分发逻辑
WinDbg 反汇编 1 uf nt!PsAltSystemCallDispatch
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 1: kd> uf nt!PsAltSystemCallDispatch nt!PsAltSystemCallDispatch: fffff804`6fd82a70 4883ec38 sub rsp,38h fffff804`6fd82a74 65488b042588010000 mov rax,qword ptr gs:[188h] fffff804`6fd82a7d 8a5003 mov dl,byte ptr [rax+3] fffff804`6fd82a80 f6c204 test dl,4 fffff804`6fd82a83 7409 je nt!PsAltSystemCallDispatch+0x1e (fffff804`6fd82a8e) Branch nt!PsAltSystemCallDispatch+0x15: fffff804`6fd82a85 488b050c9d7700 mov rax,qword ptr [nt!PsAltSystemCallHandlers (fffff804`704fc798)] fffff804`6fd82a8c eb0c jmp nt!PsAltSystemCallDispatch+0x2a (fffff804`6fd82a9a) Branch nt!PsAltSystemCallDispatch+0x1e: fffff804`6fd82a8e f6c220 test dl,20h fffff804`6fd82a91 7418 je nt!PsAltSystemCallDispatch+0x3b (fffff804`6fd82aab) Branch nt!PsAltSystemCallDispatch+0x23: fffff804`6fd82a93 488b05069d7700 mov rax,qword ptr [nt!PsAltSystemCallHandlers+0x8 (fffff804`704fc7a0)] nt!PsAltSystemCallDispatch+0x2a: fffff804`6fd82a9a 4883f802 cmp rax,2 fffff804`6fd82a9e 720b jb nt!PsAltSystemCallDispatch+0x3b (fffff804`6fd82aab) Branch nt!PsAltSystemCallDispatch+0x30: fffff804`6fd82aa0 e8db51e8ff call nt!guard_dispatch_icall (fffff804`6fc07c80) fffff804`6fd82aa5 4883c438 add rsp,38h fffff804`6fd82aa9 c3 ret nt!PsAltSystemCallDispatch+0x3b: fffff804`6fd82aab 488364242000 and qword ptr [rsp+20h],0 fffff804`6fd82ab1 4533c9 xor r9d,r9d fffff804`6fd82ab4 4533c0 xor r8d,r8d fffff804`6fd82ab7 b9e0010000 mov ecx,1E0h fffff804`6fd82abc 418d5104 lea edx,[r9+4] fffff804`6fd82ac0 e8fbaae7ff call nt!KeBugCheckEx (fffff804`6fbfd5c0) fffff804`6fd82ac5 cc int 3 fffff804`6fd82ac6 cc int 3 fffff804`6fd82ac7 cc int 3 fffff804`6fd82ac8 cc int 3 fffff804`6fd82ac9 cc int 3 fffff804`6fd82aca cc int 3 fffff804`6fd82acb cc int 3 fffff804`6fd82acc cc int 3 fffff804`6fd82acd cc int 3 fffff804`6fd82ace cc int 3 fffff804`6fd82acf cc int 3 fffff804`6fd82ad0 4883ec28 sub rsp,28h fffff804`6fd82ad4 e8538e3800 call nt!PsPicoSystemCallDispatch (fffff804`7010b92c) fffff804`6fd82ad9 33c0 xor eax,eax fffff804`6fd82adb 4883c428 add rsp,28h fffff804`6fd82adf c3 ret
研究发现 在上面我们研究了这个调用链,我们Alt-Syscall 触发判断(核心)这个最关键的,系统拿到KTHREAD结构进行一系列的判断,过了判断之后最终调用分发PsAltSystemCallDispatch,然后分发函数中就会调用[PsAltSystemCallHandlers+0x8] ,所以把前面的判断都过了,然后在这里面填入我们函数即可。
加载驱动
运行测试程序
断两个点,大家觉得会先断哪个,前提是之前的判断我都绕过了,这个其实我是复现不出来,只要断点就卡哈哈哈。
这是我加了一些输出日志,可以看到监控能力没毛病的,就是太多噪音了,那些系统进程的东西,也监控了。
感想 去年学这个的时候,完全看不懂,当时就想着自己开发edr,看了很多博客等等,慢慢也是能看懂一点点,但是写不出来,今年AI发展太快了,这种东西写的也是没毛病,有问题,问ai,一步一步调试,也能做出来,比如这个alt syscall,我之前使用的不是现在22h2版本,然后是不能做到的,我调试了半天都是注册不成功,问的claude才说出来是版本的问题,还有很多调试的地方,比如各种偏移啥的,ai也能做到,就是他告诉我们命令需要自己windbg然后给他输出就行了。有没有大手子给点token,我身上好痒。