RunPE

RunPE

RunPE顾名思义,就是将dll或者exe等PE文件直接从内存中加载到木马的内存中去执行,而不需要LoadLibrary等函数,因此可以避免某些杀软的检测。

在学习之前一定要学习一遍文件的PE结构,我们接下来要做的事也是差不多按照PE结构来解析文件,然后执行。

0x01 原理和思路

我们的大体思路如下

内存加载PE文件
读取PE文件,进行解析
申请内存 ImageBase作为内存基地址,sizeofimage作为长度
pe复制到内存
解析Section的地址并将Section复制到内存中
基于重定位表修改内存
解析导入表,加载所需的Dll
跳转到入口地址AddressOfEntryPoint,执行PE文件

解析导入表,加载所需的Dll此时我们可以遍历导出表,避免GetProAddress的使用,避免某些杀软的检测。

另外,dll和exe文件加载过程中还有一点不同,在于入口函数,exe不需要dllmain函数,直接根据PE结构然后计算按获取入口地址即可。

0x02 代码实现

代码可以参考windows_hack_teach/用户层/4/内存直接加载运行 at master · Gilfoylex/windows_hack_teach (github.com)

0x03 项目推荐

c/c++的项目可以看aaaddress1/RunPE-In-Memory: Run a Exe File (PE Module) in memory (like an Application Loader) (github.com)

nim的项目可以看S3cur3Th1sSh1t/Nim-RunPE:从内存中反射式PE加载的Nim实现 (github.com)

详细分析Nim-RunPE

我们一起来分析一下nim的项目,因为之前对nim并没有太了解过,所以写的会相对比较详细。

import winim
import ptr_math

首先是导入库,作者在项目主页也说了需要安装以下依赖项:nimble install ptr_math winim

var success: BOOL

接下来定义success变量,被声明为BOOL类型,在Nim语言中,BOOL类型实际上是一个枚举类型,它有两个可能的值:TRUE和FALSE。这种类型可以用于表示逻辑真假值。

when defined(args):
  const toLoadfromMem = slurp"C:\\windows\\system32\\cmd.exe"
else:
  const toLoadfromMem = slurp"C:\\windows\\system32\\calc.exe"

这段代码使用了Nim语言中的条件编译指令,它会根据args是否定义来决定加载哪个文件到内存中。

其中,defined(args)是一个条件编译器指令,它判断args是否已经定义。如果已经定义,则执行when defined(args)后面的代码块;否则,执行else后面的代码块。

具体解释如下:

  1. defined(args):这是一个条件编译指令,它用于判断args是否已经定义。如果已经定义,则条件成立,执行when defined(args)后面的代码块;否则,条件不成立,执行else后面的代码块。
  2. const toLoadfromMem = slurp"C:\\windows\\system32\\cmd.exe":在条件成立的情况下,即args已经定义,会将C:\windows\system32\cmd.exe文件中的内容读取到内存中,并赋值给常量toLoadfromMem。这里使用了slurp过程来读取文件中的内容。
  3. const toLoadfromMem = slurp"C:\\windows\\system32\\calc.exe":在条件不成立的情况下,即args没有定义,会将C:\windows\system32\calc.exe文件中的内容读取到内存中,并赋值给常量toLoadfromMem。

在项目处,我们以可以看到作者对两个不同的编译方式进行了解释。

when defined(args):
    const exeArgs = "/c whoami"
else:
    const exeArgs = ""

这句话是用来在定义参数时执行cmd命令时的参数。

func toByteSeq*(str: string): seq[byte] {.inline.} =
  ## Converts a string to the corresponding byte sequence.
  @(str.toOpenArrayByte(0, str.high))

这里定义函数toByteSeq,用于将字符串转换为相应的字节序列。

when defined(args):
    proc patchMemory*(targetAddr: pointer, data: openArray[byte]): void =
        var oldProtect: DWORD
        VirtualProtect(targetAddr, cast[SIZE_T](len(data)), PAGE_READWRITE, cast[PDWORD](addr(oldProtect)))
        copyMem(targetAddr, unsafeAddr data[0], len(data))
        VirtualProtect(targetAddr, cast[SIZE_T](len(data)), oldProtect, cast[PDWORD](addr(oldProtect)))
when defined(args):
    proc patchArgFunctionMemory*(funcAddr: pointer, pNewCommandLine: pointer): void =
        when defined x86:
            var shellcode: seq[byte] = @[byte(0xb8)] # movabs rax, new_cmd
        else:
            var shellcode: seq[byte] = @[byte(0x48), byte(0xb8)] # movabs rax, new_cmd
        # add new_cmd addr to shellcode
        for t in cast[array[sizeOf(pointer), byte]](pNewCommandLine):
            shellcode.add t        
        shellcode.add(byte(0xc3)) # ret
        patchMemory(funcAddr, shellcode)

这段代码是Nim语言中的一些过程定义。这些过程用于在内存中修改数据,特别是用于修改目标地址的内存和修改函数地址的内存。

第一个过程patchMemory用于修改目标地址的内存。它使用VirtualProtect函数修改目标地址的内存保护属性,然后使用copyMem函数将数据复制到目标地址,最后再恢复原始的内存保护属性。

第二个过程patchArgFunctionMemory用于修改函数地址的内存,以实现参数传递功能。它根据不同的架构生成不同的shellcode,并将新的命令行地址添加到shellcode中。最后,它调用了patchMemory过程来修改函数地址的内存。

这些过程都被包含在defined(args)条件编译块中,这意味着它们只会在代码中定义了args宏时才会被编译和执行。

var memloadBytes = toByteSeq(toLoadfromMem)

var shellcodePtr: ptr = memloadBytes[0].addr

还记得最初定义的toLoadfromMem吗,它根据我们有无参数的值分别是calc和cmd。在这里我们将字符串转成了字符数组,并且获取地址。

proc getNtHdrs*(pe_buffer: ptr BYTE): ptr BYTE =
  if pe_buffer == nil:
    return nil
  var idh: ptr IMAGE_DOS_HEADER = cast[ptr IMAGE_DOS_HEADER](pe_buffer)
  if idh.e_magic != IMAGE_DOS_SIGNATURE:
    return nil
  let kMaxOffset: LONG = 1024
  var pe_offset: LONG = idh.e_lfanew
  if pe_offset > kMaxOffset:
    return nil
  var inh: ptr IMAGE_NT_HEADERS32 = cast[ptr IMAGE_NT_HEADERS32]((
      cast[ptr BYTE](pe_buffer) + pe_offset))
  if inh.Signature != IMAGE_NT_SIGNATURE:
    return nil
  return cast[ptr BYTE](inh)

接下来又是一个过程,是一个获取IMAGE_NT_HEADERS 结构体指针的过程。

proc getPeDir*(pe_buffer: PVOID; dir_id: csize_t): ptr IMAGE_DATA_DIRECTORY =
  if dir_id >= IMAGE_NUMBEROF_DIRECTORY_ENTRIES:
    return nil
  var nt_headers: ptr BYTE = getNtHdrs(cast[ptr BYTE](pe_buffer))
  if nt_headers == nil:
    return nil
  var peDir: ptr IMAGE_DATA_DIRECTORY = nil
  var nt_header: ptr IMAGE_NT_HEADERS = cast[ptr IMAGE_NT_HEADERS](nt_headers)
  peDir = addr((nt_header.OptionalHeader.DataDirectory[dir_id]))
  if peDir.VirtualAddress == 0:
    return nil
  return peDir

还是过程,获取 PE 文件中指定目录项的 IMAGE_DATA_DIRECTORY 结构体指针。

type
  BASE_RELOCATION_ENTRY* {.bycopy.} = object
    Offset* {.bitsize: 12.}: WORD
    Type* {.bitsize: 4.}: WORD

接下来定义一个数据结构,代表着我们的重定位表,两个参数分别是偏移和类型。

proc applyReloc*(newBase: ULONGLONG; oldBase: ULONGLONG; modulePtr: PVOID;
                moduleSize: SIZE_T): bool =
  echo "    [!] Applying Reloc "
  var relocDir: ptr IMAGE_DATA_DIRECTORY = getPeDir(modulePtr,
      IMAGE_DIRECTORY_ENTRY_BASERELOC)
  if relocDir == nil:
    return false
  var maxSize: csize_t = csize_t(relocDir.Size)
  var relocAddr: csize_t = csize_t(relocDir.VirtualAddress)
  var reloc: ptr IMAGE_BASE_RELOCATION = nil
  var parsedSize: csize_t = 0
  while parsedSize < maxSize:
    reloc = cast[ptr IMAGE_BASE_RELOCATION]((
        size_t(relocAddr) + size_t(parsedSize) + cast[size_t](modulePtr)))
    if reloc.VirtualAddress == 0 or reloc.SizeOfBlock == 0:
      break
    var entriesNum: csize_t = csize_t((reloc.SizeOfBlock - sizeof((IMAGE_BASE_RELOCATION)))) div
        csize_t(sizeof((BASE_RELOCATION_ENTRY)))
    var page: csize_t = csize_t(reloc.VirtualAddress)
    var entry: ptr BASE_RELOCATION_ENTRY = cast[ptr BASE_RELOCATION_ENTRY]((
        cast[size_t](reloc) + sizeof((IMAGE_BASE_RELOCATION))))
    var i: csize_t = 0
    while i < entriesNum:
      var offset: csize_t = entry.Offset
      var entryType: csize_t = entry.Type
      var reloc_field: csize_t = page + offset
      if entry == nil or entryType == 0:
        break
      if entryType != RELOC_32BIT_FIELD:
        echo "    [!] Not supported relocations format at ", cast[cint](i), " ", cast[cint](entryType)
        return false
      if size_t(reloc_field) >= moduleSize:
        echo "    [-] Out of Bound Field: ", reloc_field
        return false
      var relocateAddr: ptr csize_t = cast[ptr csize_t]((
          cast[size_t](modulePtr) + size_t(reloc_field)))
      echo "    [V] Apply Reloc Field at ", repr(relocateAddr)
      (relocateAddr[]) = ((relocateAddr[]) - csize_t(oldBase) + csize_t(newBase))
      entry = cast[ptr BASE_RELOCATION_ENTRY]((
          cast[size_t](entry) + sizeof((BASE_RELOCATION_ENTRY))))
      inc(i)
    inc(parsedSize, reloc.SizeOfBlock)
  return parsedSize != 0

这段 Nim 代码定义了一个名为 applyReloc 的过程,用于应用基址重定位(Base Relocation)。

该过程接受以下参数:

  • newBase: ULONGLONG:新的基地址,用于替换原始模块的基地址。
  • oldBase: ULONGLONG:旧的基地址,需要被替换的原始模块的基地址。
  • modulePtr: PVOID:指向模块内存的指针。
  • moduleSize: SIZE_T:模块的大小。

在过程中,通过 getPeDir 过程获取基址重定位表所在的数据目录项,并检查是否成功获取。然后,通过循环遍历基址重定位表,解析每个重定位项并进行相应的修正操作。

具体步骤如下:

  1. 获取基址重定位表的大小和虚拟地址。
  2. 创建一个指针 reloc,指向当前重定位项的内存地址。
  3. 检查当前重定位项的虚拟地址和块大小是否为零,如果是,则退出循环。
  4. 计算当前重定位项中包含的重定位项数量。
  5. 记录当前重定位项的页地址。
  6. 创建指针 entry,指向当前重定位项中的第一个重定位条目。
  7. 循环遍历重定位项中的所有重定位条目。
  8. 获取重定位条目的偏移量和类型。
  9. 根据重定位条目的类型进行判断,如果不是 32 位字段类型,则输出错误信息并返回失败。
  10. 检查要修正的字段是否超出了模块的大小范围,如果是,则输出错误信息并返回失败。
  11. 将要修正的字段的地址转换为指针类型 relocateAddr。
  12. 输出修正前的字段地址,并应用基址重定位操作,将其修正为新的基地址。
  13. 更新重定位条目的指针,指向下一个重定位条目。
  14. 增加计数器 i。
  15. 增加已解析的字节数 parsedSize。
  16. 如果全部重定位项都已处理完毕,则退出循环。
  17. 返回是否成功解析了重定位项。

总之,这个过程用于遍历基址重定位表中的每个重定位项,并根据重定位类型对相应的字段进行修正,以实现模块的基址重定位。

proc fixIAT*(modulePtr: PVOID, exeArgs: Stringable): bool =
  echo "[+] Fix Import Address Table\n"
  var importsDir: ptr IMAGE_DATA_DIRECTORY = getPeDir(modulePtr,
      IMAGE_DIRECTORY_ENTRY_IMPORT)
  if importsDir == nil:
    return false
  var maxSize: csize_t = cast[csize_t](importsDir.Size)
  var impAddr: csize_t = cast[csize_t](importsDir.VirtualAddress)
  var lib_desc: ptr IMAGE_IMPORT_DESCRIPTOR
  var parsedSize: csize_t = 0
  while parsedSize < maxSize:
    lib_desc = cast[ptr IMAGE_IMPORT_DESCRIPTOR]((
        impAddr + parsedSize + cast[uint64](modulePtr)))
    
    if (lib_desc.OriginalFirstThunk == 0) and (lib_desc.FirstThunk == 0):
      break
    var libname: LPSTR = cast[LPSTR](cast[ULONGLONG](modulePtr) + lib_desc.Name)
    echo "    [+] Import DLL: ", $libname
    var call_via: csize_t = cast[csize_t](lib_desc.FirstThunk)
    var thunk_addr: csize_t = cast[csize_t](lib_desc.OriginalFirstThunk)
    if thunk_addr == 0:
      thunk_addr = csize_t(lib_desc.FirstThunk)
    var offsetField: csize_t = 0
    var offsetThunk: csize_t = 0
    var hmodule: HMODULE = LoadLibraryA(libname)

    when defined(args):
        var commandStr: string
        var exeArgsPassed = false
        if len(exeArgs) > 0: 
            commandStr = " " & exeArgs # in case commands are passed we have to prepend at least a space so that argv[1] is the first part of exeArgs
            exeArgsPassed = true
        if exeArgsPassed:
            # patch _wcmdln and _acmdln if they are present in the import to make exeArgs working for some C++ binaries
            var wcmdlenaddr = GetProcAddress(hmodule,"_wcmdln") 
            if wcmdlenaddr != NULL:
                echo "        Found _wcmdln -> patching with exeArgs"
                var newCmd = newWideCString(commandStr) # we have to prepend 
                patchMemory(wcmdlenaddr, cast[array[sizeOf(pointer), byte]](newCmd))
            var acmdlenaddr = GetProcAddress(hmodule,"_acmdln") 
            if acmdlenaddr != NULL:
                echo "        Found _wcmdln -> patching with exeArgs"
                var newCmd = &(commandStr)
                patchMemory(acmdlenaddr, cast[array[sizeOf(pointer), byte]](newCmd))
        
    while true:
      var fieldThunk: PIMAGE_THUNK_DATA = cast[PIMAGE_THUNK_DATA]((
          cast[csize_t](modulePtr) + offsetField + call_via))
      var orginThunk: PIMAGE_THUNK_DATA = cast[PIMAGE_THUNK_DATA]((
          cast[csize_t](modulePtr) + offsetThunk + thunk_addr))
      var boolvar: bool
      if ((orginThunk.u1.Ordinal and IMAGE_ORDINAL_FLAG32) != 0):
        boolvar = true
      elif((orginThunk.u1.Ordinal and IMAGE_ORDINAL_FLAG64) != 0):
        boolvar = true
      if (boolvar):
        var libaddr: size_t = cast[size_t](GetProcAddress(LoadLibraryA(libname),cast[LPSTR]((orginThunk.u1.Ordinal and 0xFFFF))))
        fieldThunk.u1.Function = ULONGLONG(libaddr)
        echo "        [V] API ord: ", (orginThunk.u1.Ordinal and 0xFFFF)
      if fieldThunk.u1.Function == 0:
        break
      if fieldThunk.u1.Function == orginThunk.u1.Function:
        var nameData: PIMAGE_IMPORT_BY_NAME = cast[PIMAGE_IMPORT_BY_NAME](orginThunk.u1.AddressOfData)
        var byname: PIMAGE_IMPORT_BY_NAME = cast[PIMAGE_IMPORT_BY_NAME](cast[ULONGLONG](modulePtr) + cast[DWORD](nameData))
      
        var func_name: LPCSTR = cast[LPCSTR](addr byname.Name)
        var libaddr: csize_t = cast[csize_t](GetProcAddress(hmodule,func_name))
        echo "        [V] API: ", func_name
        fieldThunk.u1.Function = ULONGLONG(libaddr)

        when defined(args):
            # patch common Win32 functions to get the command line
            if exeArgsPassed and "GetCommandLineW" == $$func_name:
                echo "           [>] Patching function to pass exeArgs: ", func_name
                patchArgFunctionMemory(cast[pointer](libaddr), cast[pointer](newWideCString(commandStr)))
            if exeArgsPassed and $$"GetCommandLineA" == func_name:
                echo "           [>] Patching function to pass exeArgs: ", func_name
                patchArgFunctionMemory(cast[pointer](libaddr), cast[pointer](&commandStr))

      inc(offsetField, sizeof((IMAGE_THUNK_DATA)))
      inc(offsetThunk, sizeof((IMAGE_THUNK_DATA)))
    inc(parsedSize, sizeof((IMAGE_IMPORT_DESCRIPTOR)))
  return true

  1. 获取PE文件的导入目录表(IMAGE_DATA_DIRECTORY)。
  2. 遍历导入目录表中的每个导入描述符(IMAGE_IMPORT_DESCRIPTOR)。
  3. 对于每个导入描述符,获取导入的DLL名称,并加载该DLL。
  4. 遍历导入描述符中的每个函数地址表项(IMAGE_THUNK_DATA)。
  5. 判断函数地址表项是否为序号导入(通过判断Ordinal标志位)。
  6. 如果是序号导入,则通过GetProcAddress函数获取函数地址,并将修复后的地址写入函数地址表项。
  7. 如果是名称导入,则通过GetProcAddress函数获取函数地址,并将修复后的地址写入函数地址表项。
  8. 如果传入了参数exeArgs,则对特定的函数进行修复,以支持传递参数给函数。
  9. 逐步增加偏移量,继续遍历下一个函数地址表项,直到遍历完所有函数地址表项。
  10. 增加偏移量,继续遍历下一个导入描述符,直到遍历完所有导入描述符。
  11. 返回修复结果。

总之,这段代码的作用是遍历模块的导入表,加载每个导入的 DLL 并修正 IAT 条目,以确保正确的函数调用。

下面是代码的总体思路:

首先,代码使用Winim库中的VirtualAlloc函数在进程的虚拟地址空间中申请一段连续的内存空间,大小为要加载的PE文件的大小。然后,代码使用Winim库中的ReadFile函数从指定路径中读取PE文件的内容,将其写入到刚刚申请的内存空间中。

接着,代码解析PE头部信息,获取程序入口点和需要进行重定位的地址列表等重要信息。如果需要进行重定位,则通过ptr_math库中提供的指针加法运算,将加载的基址与重定位表中保存的偏移量相加,获得需要修复的地址。然后,代码会在内存中修改这些地址的内容,使得它们指向正确的位置。

之后,代码会根据PE头部信息,遍历导入表中的函数列表,获取对应的函数地址,并将其写入到内存中。最后,代码调用TLS回调函数和程序入口点,开始执行PE文件中的程序代码。

(还是c++看着舒服.......)

posted @ 2023-11-22 16:05  fdx_xdf  阅读(524)  评论(0)    收藏  举报