目录

RDI 反射dll加载(KaynLdr)

RDI是无文件落地,直接内存加载执行PE的技术,C2中经常使用。

背景

最初的rdi技术在很早就已经出现,地址ReflectiveDLLInjection,里面用到了的API包括HeapAllocVirtualAllocExWriteProcessMemory等,随着现在杀软技术的提升,这种方式已经不能够满足现在的免杀要求,现在需要通过Syscalls、函数hash等方式来提升免杀能力。

下面就通过KaynLdr这个项目来简单分析一下目前在代码方面可以优化的地方。

项目介绍

该项目代码使用了C语言和汇编,分配内存时设置内存属性为RW,等到将dll代码写入到分配的内存以后将内存属性改为RW,这可以有效避免杀软对敏感内存的动态扫描。最后在执行dll代码时,又清空了dll的DOS和NT头,让这段内存在杀软看起来不那么可疑。

编译及运行

编译使用的系统为Parrot Linux,git clone项目后使用make来进行编译,此时会提示x86_64-w64-mingw32-gcc: not found,此时需要使用命令sudo apt-get install gcc-mingw-w64-x86-64来安装mingw编译器。目前该项目不支持x86编译,所以需要修改KaynInject目录下的makefile文件,去掉x86编译的命令即可正常编译。最后的结果如下图

/images/rdiMessagebox.png
运行结果

重点代码分析

在dll中获取文件头位置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
   KaynCaller:
       call pop                 ; Calling the next instruction puts RIP address on the top of our stack
       pop:
       pop rcx                  ; 前面两条指令用来获取当前的代码执行的地址
   loop:
       xor rbx, rbx             ; rbx = 0
       mov ebx, 0x5A4D          ; MZ bytes for comparing if we are at the start of our reflective DLL
       dec rcx
       cmp bx,  word ds:[rcx]   ; Compare the first 2 bytes of the page to MZ
       jne loop					; rcx值不断减一,向低地址处遍历,然后比较内存数据是否与MZ(PE文件头)相同
       xor rax, rax             ; eax = 0
       mov ax,  [rcx+0x3C]      ; ax = PIMAGE_DOS_HEADER->e_lfanew
       add rax, rcx             ; DLL base + RVA new exe header = 0x00004550 PE00 Signature
       xor rbx, rbx             ; rbx = 0
       add bx,  0x4550          ; bx = IMAGE_NT_SIGNATURE
       cmp bx,  word ds:[rax]   ; eax == IMAGE_NT_SIGNATURE
       jne loop					; 当找到MZ标志后,再获取NT Headers中的Signature,比较是否与0x4550相同
       mov rax, rcx             ; Saves the address to our reflective Dll
   ret                          ; return KaynLdrAddr

可以优化的地方:此时dll已经加载到目标进程的内存中,MZ这个标志对杀软来说比较明显,可以修改为其他字符。

获取函数地址的方式

  1. 获取函数所在dll的地址

    1
    2
    
    hKernel32          = KGetModuleByHash(KERNEL32_HASH);
    hNtDLL             = KGetModuleByHash(NTDLL_HASH);
    

    KaynLoader函数运行完以前,输入表还没有初始化,所以此时获取dll地址是通过PEB获取的,通过代码__readgsqword( 0x60 )来获取到PEB的地址,然后读取Ldr结构中的InMemoryOrderModuleList链表,使用dll名字的hash去对比,然后获取到dll地址。(此处使用hash可以有效的免杀)

  2. 获得dll的地址以后,通过读取dll的导出表,同样比对函数名字的hash来查找函数地址,可以获得必要的函数包括NtAllocateVirtualMemoryNtProtectVirtualMemory,此处是从本身的ntdll中获取函数地址。此处写内存时使用的__movsb函数

可以优化的地方:此处获取函数地址是从本身的ntdll中获取,这篇文章里面说过ntdll里面敏感函数可能被修改,所以尽可能使用其他方法来获取ntdll中干净的函数地址,可以使用文章里面已经讲解过那几种方法。

项目中Syscall的代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
   ;; set syscall value in r11 register
   SyscallPrepare:
       nop             ; extra nop to "obfuscate"
       xor r11, r11    ; zero out r11
       nop             ; extra nop to "obfuscate"
       nop             ; extra nop to "obfuscate"
       mov r11d,ecx   ; save 32-bit value from ecx to r11 as 32-bit
   ret                 ; return

   ;; Invoke Syscall and pass given arguments
   SyscallInvoke:
       nop             ; extra nop to "obfuscate"
       xor eax, eax    ; zero out rax
       mov r10, rcx    ; syscall arguments
       nop             ; extra nop to "obfuscate"
       mov eax, r11d   ; move value from r11 to eax which is the syscall id to invoke the syscall
       nop             ; extra nop to "obfuscate"
       syscall         ; invoke the syscall
       nop             ; extra nop to "obfuscate"
   ret                 ; return NTSTATUS

这个项目中的syscall分为两个部分。

  1. SyscallPrepare部分将函数的调用号传入r11寄存器的低32位,中间还使用了许多的nop混淆了代码,可以干扰杀软的判断

  2. SyscallInvoke部分将上面获取的函数调用号传入eax中,同上面一样中间加了nop来混淆代码

可以改进的地方:此处杀软可能会检测syscall指令来源地址,上面的这种写法syscall的来源是当前进程中的某个地址,不合常理。syscall应该来源于ntdll的内存领域中,关于这一点可以参考这个代码,这份代码的功能是创建挂起的指令然后获取到syscall的调用号,编写汇编指令模拟syscall过程,将dll中原本syscall指令的地址传入R11寄存器,然后jmp r11,这种方式在目前有效。