长亭百川云 - 文章详情

蓝军和elf loader

leveryd

48

2024-07-13

背景

在linux系统上执行二进制文件一般会用到execve系统调用,比如下面的执行sleep 1000

[root@instance-h9w7mlyv ~]# strace sleep 1000 2>&1|grep execveexecve("/usr/bin/sleep", ["sleep", "1000"], 0x7ffdb98242f8 /* 40 vars */) = 0

其中/usr/bin/sleep文件因为在本地存储,所以可能被主机上的安全产品做静态分析,如果是恶意样本就有可能暴露攻击行为。

为了对抗静态分析,蓝军可以让攻击样本不落盘,比如用如下memfd_create的方式

fdm = syscall(__NR_memfd_create, "elf", MFD_CLOEXEC);write(fdm, elfbuf, filesize);sprintf(cmd, "/proc/self/fd/%d", fdm);execve(cmd, argv, NULL);

完整代码可以见 https://github.com/QAX-A-Team/ptrace/blob/master/anonyexec.c

但是这种攻击行为会产生memfd_create和execve两个系统调用,特征很明显,于是又有蓝军提到在用户态加载elf并执行,这样既可以样本不落盘,又可以避免用到execve被安全产品采集到进程数据。

https://github.com/anvilsecure/ulexecve/blob/main/ulexecve.py 这个开源项目就实现了用户态的elf装载。

elf装载的原理不复杂,基本步骤是通过mmap、mprotect系统调用申请到"可读可写可执行"的内存,然后将PT_LOAD类型的segment映射到内存中,最后根据e_entry跳转到映射到内存的代码段中执行。

有两个疑问促使我研究,第一个问题是elf装载时内存地址空间不会和装载前的内存地址空间冲突吗,第二个问题是怎么处理动态链接库。

本文记录在我研究过程中学到的"散装知识点",希望对你有点帮助。

elf装载时内存地址空间不会和装载前的ulexecve程序内存地址空间冲突吗

python ulexecve.py加载elf时有可能破坏原来的python程序指令,导致程序崩溃?

实际上不会,ulexecve有一个"jump buffer"的概念,ulexecve.py会先生成"elf loader"指令,然后申请一个"jump buffer"内存,最后跳转到内存执行。

def prepare_jumpbuf(buf):    dst = mmap(0, PAGE_CEIL(len(buf)), PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0)    src = ctypes.create_string_buffer(buf)    logging.debug("Memmove(0x%.8x, 0x%.8x, 0x%.8x)" % (dst, ctypes.addressof(src), len(buf)))    memmove(dst, src, len(buf))    ret = mprotect(PAGE_FLOOR(dst), PAGE_CEIL(len(buf)), PROT_READ | PROT_EXEC)    ...    return ctypes.cast(dst, ctypes.CFUNCTYPE(c_void_p))cfunction = prepare_jumpbuf(jumpbuf)cfunction()

怎么处理动态链接库

处理动态库是"动态链接器"的工作,而不是"程序装载器"的工作。"程序装载器"设置好栈环境、辅助向量(auxilliary vector),就可以把程序控制权交给"动态链接器"。如下

def generate(self, stack, jump_delay=None):    # generate jump buffer with the CPU instructions which copy all    # segments to the right locations in memory, set the correct protection    # flags on those memory segments and then prepare for the actual jump    # into hail mary land.    # generate ELF loading code for the executable as well as the    # interpreter if necessary    ret = []    code = self.generate_elf_loader(self.exe) # 1.拷贝elf segment到虚拟内存    ret.append(code)    # fix up the auxv vector with the proper relative addresses too    code = self.generate_auxv_fixup(stack, Stack.OFFSET_AT_PHDR, self.exe.e_phoff)  2.设置辅助向量    ret.append(code)    # fix up the auxv vector with the proper relative addresses too    code = self.generate_auxv_fixup(stack, Stack.OFFSET_AT_ENTRY, self.exe.e_entry, self.exe.is_pie)  3.设置辅助向量    ret.append(code)    if self.interp: # 4.如果有动态链接器,就从动态链接器的入口执行        code = self.generate_elf_loader(self.interp)  # 4.1.拷贝动态链接器 segment到虚拟内存        ret.append(code)        code = self.generate_auxv_fixup(stack, Stack.OFFSET_AT_BASE, 0) # 4.2.设置辅助向量        ret.append(code)        entry_point = self.interp.e_entry    else: # 4.如果没有动态链接器,就从elf入口执行        entry_point = self.exe.e_entry        if not self.exe.is_pie:            entry_point -= self.exe.ph_entries[0]["vaddr"]    self.log("Generating jumpcode with entry_point=0x%.8x and stack=0x%.8x" % (entry_point, stack.base))    code = self.generate_jumpcode(stack.base, entry_point, jump_delay)  5.生成"从入口执行"的指令    ret.append(code)    return b"".join(ret)

上面代码中可以看到self.exe.is_pie影响程序入口地址,这个pie是什么呢?

pie和aslr

pie和aslr一样都可以实现地址随机化,防御漏洞利用。区别在于aslr不负责代码段以及数据段的随机化工作,这项工作由pie负责。但是只有在开启aslr之后,pie才会生效。

下面我们可以结合ulexecve代码和动手实践,看一下pie到底是怎么工作的。

如果elf文件有pie机制,mmap第一个地址参数就是0。此时如果开启了aslr,mmap系统调用返回的地址就会一个随机化的地址。

def generate_elf_loader(self, elf):    ...    addr = 0x0 if elf.is_pie else elf.ph_entries[0]["vaddr"]    ...    code = self.mmap(addr, map_sz, prot, flags)    ret.append(code)

怎么判断elf程序是否开启pie机制呢?从下面代码可以看到,第一个PT_LOAD类型的segment虚拟地址是0时,就说明开启了pie。

def parse_pentry(self):    ...    # first PT_LOAD section we use to identifie PIE status    if len(self.ph_entries) == 0:        if p_vaddr != 0x0:            self.log("Identified as a non-PIE executable")            self.is_pie = False        else:            self.log("Identified as a PIE executable")            self.is_pie = True

当你用gcc --pie参数编译时,文件的第一个PT_LOAD类型的segment虚拟地址就会是0。

[root@instance-h9w7mlyv tmp]# gcc -fPIC --pie z.c[root@instance-h9w7mlyv tmp]# readelf -l ./a.out...Program Headers:  Type           Offset             VirtAddr           PhysAddr                 FileSiz            MemSiz              Flags  Align  ...  LOAD           0x0000000000000000 0x0000000000000000 0x0000000000000000   # 参数带--pie时,VirtAddr为0                 0x0000000000000898 0x0000000000000898  R E    0x200000  LOAD           0x0000000000000de0 0x0000000000200de0 0x0000000000200de0                 0x0000000000000254 0x0000000000000258  RW     0x200000[root@instance-h9w7mlyv tmp]# gcc -fPIC z.c[root@instance-h9w7mlyv tmp]# readelf -l ./a.out...Program Headers: Type           Offset             VirtAddr           PhysAddr                FileSiz            MemSiz              Flags  Align ... LOAD           0x0000000000000000 0x0000000000400000 0x0000000000400000    # 非--pie时,VirtAddr不为0                0x0000000000000808 0x0000000000000808  R E    0x200000 LOAD           0x0000000000000e00 0x0000000000600e00 0x0000000000600e00                0x000000000000022c 0x0000000000000230  RW     0x200000

总结

文中有一些概念我并没有解释,比如elf文件格式、segment是什么,这一块你可以参考《程序员的自我修养—链接、装载与库》、ELF 格式解析[1],辅助向量的知识你可以参考 https://lwn.net/Articles/519085/

ulexecve代码中的注释非常清晰,原作者还写了一篇博客 Userland Execution of Binaries Directly from Python[2]

感觉"动态链接器"要比"程序装载器"要复杂,以后有场景了再研究。

留一个思考问题:怎么检测elf loader呢,以及作为蓝军可以怎么优化elf loader来避免检测呢?

参考资料

[1]

ELF 格式解析: https://paper.seebug.org/papers/Archive/refs/elf/Understanding\_ELF.pdf

[2]

Userland Execution of Binaries Directly from Python: https://www.anvilsecure.com/blog/userland-execution-of-binaries-directly-from-python.html

相关推荐
关注或联系我们
添加百川云公众号,移动管理云安全产品
咨询热线:
4000-327-707
百川公众号
百川公众号
百川云客服
百川云客服

Copyright ©2024 北京长亭科技有限公司
icon
京ICP备 2024055124号-2