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类型实际上是一个枚举类型,它有两个可能的值:TRUEFALSE。这种类型可以用于表示逻辑真假值。

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  阅读(123)  评论(0编辑  收藏  举报