WinAPI具有CreateRemoteThread函数,使您可以在另一个进程的地址空间中启动一个新线程。 它可以用于各种DLL注入,既可以用于不良目的(游戏作弊,密码盗窃等),也可以用于即时修复正在运行的程序中的错误,也可以将插件添加到非DLL注入的地方。提供。
通常,此功能具有可疑的应用程序实用程序,因此Linux没有现成的CreateRemoteThread类似物也就不足为奇了。 但是,我想知道如何实现它。 研究主题变成了一次很好的冒险。
我将详细讨论如何在ELF规范的帮助下,了解一些x86_64体系结构和Linux系统调用的知识,以及编写自己的小调试器,该调试器可以在已经运行且正在运行的过程中加载和执行任意代码。
理解本文需要具备有关Linux系统编程的基础知识:C语言,基于Java语言编写和调试程序,了解计算机中计算机代码和内存的作用,系统调用的概念,熟悉主要库以及阅读文档。
结果,我能够在Gnome控制中心“添加”预览密码的功能:

主要思想
如果要求中没有关于将代码加载到已经运行的进程中的子句,则解决方案将非常简单:LD_PRELOAD。 此环境变量允许在应用程序中加载任意库。 在共享库中,您可以定义在加载库时执行的构造函数 。
LD_PRELOAD和构造函数一起允许使用动态加载程序在任何进程中执行任意代码。 这是经常用于调试的相对知名的功能。 例如,您可以使用该应用程序下载自己的库,该库定义了函数malloc()和free(),这可以帮助捕获内存泄漏。
不幸的是,LD_PRELOAD仅在进程启动时起作用。 它不能用于将库加载到已经运行的进程中。 有一个dlopen()函数可以在进程运行时加载库,但是显然,进程本身必须调用它才能加载插件。
关于静态可执行文件
LD_PRELOAD仅适用于使用动态加载程序的程序。 如果程序是使用-static
开关构建的,则它将包含所有必需的库。 在这种情况下,库中依赖项的解析是在构建时执行的,程序通常尚未准备好,并且在运行时无法在汇编后动态加载库。
在静态组装的程序中,您可以在运行时嵌入代码,但这应该以稍微不同的方式完成。 而且这也不是完全安全的,因为程序可能还没有准备好迎接这种转变。
通常,没有现成的便捷解决方案,您必须编写自行车。 否则,您将不会阅读此文本:)
从概念上讲,要强制其他人的进程执行某种代码,您需要执行以下操作:
- 在目标过程中获得控制权。
- 将代码加载到目标进程的内存中。
- 准备下载的代码以在目标进程中执行。
- 通过目标进程组织执行下载的代码。
走吧...
在过程中获得控制
首先,我们需要使目标流程服从我们的意愿。 毕竟,通常进程仅执行自己的代码,已加载的库的代码或JIT编译的结果。 但是肯定不是我们的代码。
一种选择是在过程中使用某种漏洞,使您可以控制自己。 教程中的一个经典示例:缓冲区溢出,允许重写堆栈上的返回地址。 这很有趣,有时甚至可以,但是不适合一般情况。
我们将使用另一种诚实的方式来获得控制权: 调试系统调用 。 交互式调试器可以完美地停止第三方进程,评估表达式以及许多其他事情。 他们可以-我们可以。
在Linux上,主要的调试系统调用是ptrace() 。 它使您可以连接到流程,检查其状态并控制其执行进度。 ptrace()本身已被很好地记录了下来,但是其用法的详细信息只有在实践中才清楚。
将代码加载到进程存储器中
在缓冲区溢出的情况下,有效载荷( shell代码 )通常包含在使同一缓冲区溢出的内容中。 使用调试器时,可以将必要的代码直接写入过程存储器。 在WinAPI中,有一个特殊的功能WriteProcessMemory。 为此,Linux遵循UNIX方式:对于系统中的每个进程,都有一个/ proc / $ pid / mem文件显示该进程的内存。 可以使用通常的输入输出将某些内容写入过程存储器。
准备执行代码
仅将代码写入内存是不够的。 仍然需要将其写入可执行内存 。 在通过漏洞进行记录的情况下,这样做并不容易,但是由于我们可以完全控制目标进程,因此为我们自己找到或分配“正确的”内存将不是问题。
准备的另一个重要点是外壳代码本身。 在其中,我们可能希望使用库中的某些功能,例如输入输出,图形基元等。 但是,我们必须写下光秃秃的机器代码,它本身不知道库中所有这些很酷的函数的地址。 你从哪里得到的?
为了简化操作系统的寿命并使恶意代码的寿命复杂化,库通常不使用固定地址(并且包含所谓的位置无关代码 )。 因此,无法猜测地址。
当进程正常启动时,执行重定位的加载器负责确定库的确切地址。 但是,他一开始只完成一次。 如果该进程允许动态加载库,则其中有一个动态加载器 ,可以在进程运行时执行相同的操作。 但是,动态加载程序的地址也不固定。
通常,对于库,有四个选项:
- 完全不使用库,在干净的系统调用中执行所有操作
- 将所有必要库的副本放入外壳代码中
- 自己完成动态加载程序的工作
- 找到一个动态的引导程序并使其加载我们的库
我们将选择后者,因为我们需要这些库,并编写了很长时间的完整Bootloader。 这不是最秘密的方法,也不是最有趣的方法,而是最简单,最强大和最可靠的方法。
控制权转移到代码
ptrace()允许您更改处理器寄存器,因此将控制转移到已加载并准备好的代码应该没有问题:只需将我们的代码地址写到%rip寄存器中-瞧! 但是,实际上,一切并非如此简单。 困难与以下事实有关:调试过的进程实际上并没有消失,并且它还具有某种已经执行并且将继续执行的代码。
解决方案草图
总计,我们将在第三方流程中实现我们的流程,如下所示:
- 我们已连接到目标进程以进行调试。
- 我们在内存中找到了必要的库:
- libdl-加载新库
- libpthread-启动新线程
- 我们在库中找到必要的功能:
- libdl:dlopen(),dlsym()
- libpthread:pthread_create(),pthread_detach()
我们将shell代码引入目标进程的内存中:
void shellcode(void) { void *payload = dlopen("/path/to/payload.so", RTLD_LAZY); void *entry = dlsym(payload, "entry_point"); pthread_t thread; pthread_create(&thread, NULL, entry, NULL); pthread_detach(thread); }
- 我们给出要实现的shell代码。
结果,库将为我们做正确的事情:它们将向我们的库加载内存中所需的代码,并启动执行该代码的新线程。
局限性
上述方法施加了某些限制:
- 引导加载程序必须具有足够的特权才能调试目标进程。
- 该过程应使用libdl(已准备好动态加载模块)。
- 该进程应使用libpthread(可用于多线程)。
- 不支持静态应用程序。
此外,我个人也懒得为所有架构提供支持,因此我们将限于x86_64。 (即使是32位x86也会更复杂。)
如您所见,所有这些都结束了对恶意目标的秘密使用。 但是,该任务仍然保留了研究兴趣,甚至为工业应用留下了微弱的机会。
题外话:关于使用libdl和libpthread
经验丰富的读者可能会想:如果__libc_dlopen_mode()和__libc_dlsym()内部函数已经内置在glibc中,为什么libdl只是它们的包装,为什么需要libdl? 同样,如果可以使用clone()系统调用轻松创建新线程,为什么需要libpthread?
确实,在互联网上,如何使用它们的例子远不止一个:
在流行的黑客文献中甚至提到了它们:
那为什么不呢? 好吧,至少因为我们不是在编写适合解决方案的恶意代码,而该解决方案忽略了90%的检查,占用的空间减少了20倍,但在80%的情况下仍然有效。 另外,我想用自己的双手尝试一切。
实际上,对于glibc, 不需要 libdl来加载库。 进程使用它表明它已经准备好进行动态代码加载。 尽管如此,从原则上讲,您可以拒绝使用libdl(因为以后我们仍然需要寻找glibc)。
为什么要在glibc中使用dlopen()?
这本身就是一个有趣的问题。 简短答案:实施细节。
重点是名称服务开关 (NSS)-glibc的一部分,它提供各种名称的转换:机器,协议,用户,邮件服务器等的名称。由她负责诸如getaddrinfo()之类的功能,以通过以下方式获取IP地址:域名和getpwuid()即可通过其数字标识符获取有关用户的信息。
NSS具有模块化架构,并且模块可以动态加载。 实际上,为此,glibc还需要用于动态加载库的机制。 这就是为什么当您尝试在静态组装的应用程序中使用getaddrinfo()时,链接器会显示“难以理解”的警告:
/tmp/build/socket.o:在函数Socket :: bind中:
socket.o :(。text + 0x374):警告:在静态链接中使用'getaddrinfo'
应用程序在运行时需要glibc版本的共享库
用于链接
对于线程,线程通常不仅是堆栈和可执行代码,而且还是存储在线程本地存储 (TLS)中的全局数据。 正确初始化新线程需要OS内核,二进制代码加载器和编程语言运行时的协同操作。 因此,只需简单地调用clone()就可以创建一个流,该流可以写入“ Hello world!”文件,但是对于需要访问TLS和应用程序程序员所看不到的其他有趣内容的更复杂的代码,这可能不起作用。
与多线程有关的另一点是单线程进程。 如果我们在一个不被认为是多线程的进程中创建一个新线程会怎样? 对,含糊的行为。 实际上,在此过程中,线程之间没有工作同步,这迟早会导致数据损坏。 如果我们要求应用程序使用libpthread,则可以确保它已准备好在多线程环境中工作(至少应该已经准备好)。
步骤1.连接到流程
首先,我们需要连接到目标进程进行调试,然后再与它断开连接。 这是ptrace ()系统调用进入的地方。
首次接触ptrace()
在ptrace()的文档中,您可以找到几乎所有必要的信息:
装卸
可以使用调用将线程附加到跟踪器
ptrace(PTRACE_ATTACH,pid,0,0);
或
ptrace(PTRACE_SEIZE,PID,0,PTRACE_O_flags);
PTRACE_ATTACH将SIGSTOP发送到此线程。 如果追踪者想要这个
SIGSTOP无效,需要抑制它。 请注意,如果
其他信号在附加期间同时发送到该线程,
示踪剂可能会看到示踪剂与其他信号一起进入信号传递停止
nal首先! 通常的做法是重新注入这些信号,直到
看到SIGSTOP,然后抑制SIGSTOP注入。 设计错误
这是ptrace附加和并发传递的SIGSTOP可能
竞赛和并发的SIGSTOP可能会丢失。
因此,第一步是使用PTRACE_ATTACH:
int ptrace_attach(pid_t pid) { if (ptrace(PTRACE_ATTACH, pid, 0, 0) < 0) return -1; if (wait_for_process_stop(pid, SIGSTOP) < 0) return -1; return 0; }
在ptrace()之后,目标进程还没有准备好进行调试。 我们已经连接到它,但是为了交互式研究过程的状态,必须停止它。 ptrace()向进程发送SIGSTOP信号,但是我们仍然需要等待直到进程真正停止。
要等待,请使用waitpid ()系统调用。 同时,值得注意的是几种有趣的边界情况。 首先,该过程可能简单地结束或死掉而没有收到SIGSTOP。 在这种情况下,我们无能为力。 其次,某些其他信号可能会事先发送到该过程。 在这种情况下,我们应该让进程处理它(使用PTRACE_CONT),而我们自己,继续等待我们的SIGSTOP:
static int wait_for_process_stop(pid_t pid, int expected_signal) { for (;;) { int status = 0; if (waitpid(pid, &status, 0) < 0) return -1; if (WIFSIGNALED(status) || WIFEXITED(status)) return -1; if (WIFSTOPPED(status)) { int stop_signal = status >> 8; if (stop_signal == expected_signal) break; if (ptrace(PTRACE_CONT, pid, 0, stop_signal) < 0) return -1; continue; } return -1; } return 0; }
流程断开
停止调试过程非常简单:只需使用PTRACE_DETACH:
int ptrace_detach(pid_t pid) { if (ptrace(PTRACE_DETACH, pid, 0, 0) < 0) return -1; return 0; }
严格来说,并非总是必须明确禁用调试器。 调试器进程结束时,它将自动与所有调试的进程断开连接,并且如果进程被ptrace()停止,则该进程将恢复。 但是,如果调试器使用SIGSTOP信号而不使用ptrace()明确停止了调试的进程,则没有相应的SIGCONT或PTRACE_DETACH信号也不会唤醒它。 因此,最好从文化上脱离流程。
Ptrace_scope设置
调试器可以完全控制要调试的进程。 如果任何人都可以调试任何东西,那么恶意代码将是什么呢! 显然,交互式调试是一项相当具体的活动,通常仅对开发人员才必要。 在系统正常运行期间,大多数情况下无需调试过程。
由于这些原因,出于安全原因,系统通常默认情况下禁用调试任何进程的功能。 Yama安全模块对此负责,通过文件/ proc / sys / sys / kernel / yama / ptrace_scope进行管理。 它提供了四种行为:
- 0-用户可以调试他启动的任何进程
- 1-默认模式,仅调试器启动的进程可以被调试
- 2-只有root系统管理员才能调试进程
- 3-所有人都禁止调试,在系统重新启动之前,该模式不会关闭
显然,出于我们的目的,必须能够调试在调试器之前启动的进程,因此对于实验,您需要通过将0写入特殊的ptrace_scope文件(需要管理员权限)来将系统切换到开发模式:
$ sudo sh -c 'echo 0 > /proc/sys/kernel/yama/ptrace_scope'
或以管理员身份运行调试器:
$ sudo ./inject-thread ...
第一步结果
结果,在第一步中,我们能够作为调试器连接到目标进程,然后与它断开连接。
目标进程将停止,我们可以确保操作系统确实将我们视为调试器:
$ sudo ./inject-thread --target $(pgrep docker) $ cat /proc/$(pgrep docker)/status | head Name: docker State: t (tracing stop) <--- Tgid: 31330 Ngid: 0 Pid: 31330 PPid: 1 TracerPid: 2789 <--- PID Uid: 0 0 0 0 Gid: 0 0 0 0 FDSize: 64 $ ps a | grep [2]789 2789 pts/5 S+ 0:00 ./inject-thread --target 31330
步骤2.在内存中搜索库
下一步更简单:您需要在目标进程的内存中找到具有我们所需功能的库。 但是有很多内存,从哪里开始寻找以及究竟是什么?
文件/ proc / $ pid / maps
一个特殊的文件将帮助我们解决这个问题,内核通过该文件告知进程在内存中的位置和位置。 如您所知 ,在每个进程的/ proc目录中都有一个子目录。 其中有一个描述过程存储卡的文件 :
$ cat / proc / self / maps
00400000-0040c000 r-xp 00000000 fe:01 1044592 / bin / cat
0060b000-0060c000 r-p 0000b000 fe:01 1044592 / bin / cat
0060c000-0060d000 rw-p 0000c000 fe:01 1044592 / bin / cat
013d5000-013f6000 rw-p 00000000 00:00 0 [堆]
7f9920bd1000-7f9920d72000 r-xp 00000000 fe:01 920019 /lib/x86_64-linux-gnu/libc-2.19.so
7f9920d72000-7f9920f72000 --- p 001a1000 fe:01 920019 /lib/x86_64-linux-gnu/libc-2.19.so
7f9920f72000-7f9920f76000 r-p 001a1000 fe:01 920019 /lib/x86_64-linux-gnu/libc-2.19.so
7f9920f76000-7f9920f78000 rw-p 001a5000 fe:01 920019 /lib/x86_64-linux-gnu/libc-2.19.so
7fc3f8381000-7fc3f8385000 rw-p 00000000 00:00 0
7fc3f8385000-7fc3f83a6000 r-xp 00000000 fe:01 920012 /lib/x86_64-linux-gnu/ld-2.19.so
7fc3f83ec000-7fc3f840e000 rw-p 00000000 00:00 0
7fc3f840e000-7fc3f8597000 r-p 00000000 fe:01 657286 / usr / lib / locale / locale-archive
7fc3f8597000-7fc3f859a000 rw-p 00000000 00:00 0
7fc3f85a3000-7fc3f85a5000 rw-p 00000000 00:00 0
7fc3f85a5000-7fc3f85a6000 r-p 00020000 fe:01 920012 /lib/x86_64-linux-gnu/ld-2.19.so
7fc3f85a6000-7fc3f85a7000 rw-p 00021000 fe:01 920012 /lib/x86_64-linux-gnu/ld-2.19.so
7fc3f85a7000-7fc3f85a8000 rw-p 00000000 00:00 0
7ffdb6f0e000-7ffdb6f2f000 rw-p 00000000 00:00 0 [堆栈]
7ffdb6f7f000-7ffdb6f81000 r-xp 00000000 00:00 0 [vdso]
7ffdb6f81000-7ffdb6f83000 r-p 00000000 00:00 0 [vvar]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
该文件的内容由操作系统内核根据内部结构动态生成,这些内部结构描述了我们感兴趣的进程的内存区域,并包含以下信息:
- 分配给该区域的地址范围
- 对该区域的访问权
r/-
:读w/-
:写x/-
:执行p/s
:与其他进程共享内存
- 文件偏移量(如果有)
- 显示文件所在设备的代码
- 文件索引号(如果有)
- 显示文件的路径(如果有)
某些内存区域被映射到文件上:当进程读取此类内存时,实际上是从相应文件中以特定偏移量读取数据。 如果您可以写入区域,则内存更改仅对进程本身可见( 写时复制机制, p
模式为私有),或与磁盘同步( s
模式为共享)。
其他区域是匿名的 -此内存不对应任何文件。 操作系统只是为进程提供了它使用的一块物理内存。 例如,此类区域用于“常规”进程内存:堆栈和堆。 匿名区域可以是一个进程专用的,也可以在多个进程之间共享 ( 共享内存机制)。
此外,内存中还有几个特殊区域,它们用伪名[vdso]和[vsyscall]标记。 它们用于优化某些系统调用。
我们对显示库文件内容的区域感兴趣。 如果我们读取存储卡并按显示文件的名称过滤掉其中的条目,那么我们将找到所需的库占用的所有地址。 特别简化了存储卡的格式,以便于程序处理,并且使用scanf()系列的功能可以轻松理解存储卡的格式:
static bool read_proc_line(const char *line, const char *library, struct memory_region *region) { unsigned long vaddr_low = 0; unsigned long vaddr_high = 0; char read = 0; char write = 0; char execute = 0; int path_offset = 0; sscanf(line, "%lx-%lx %c%c%c%*c %*lx %*x:%*x %*d %n", &vaddr_low, &vaddr_high, &read, &write, &execute, &path_offset); if (!strstr(line + path_offset, library)) return false; if (region) { region->vaddr_low = vaddr_low; region->vaddr_high = vaddr_high; region->readable = (read == 'r'); region->writeable = (write == 'w'); region->executable = (execute == 'x'); region->content = NULL; } return true; }
, libc-2.19.so, :

2 - ? 51? ? ?
, , .
, , . , , , (, , ).
, ( 4 ). , .
, . — — . 2 — , ( x86_64 4 , 2 , 1 ). .
, :
- libdl: dlopen() dlsym()
- libpthread: pthread_create() pthread_detach()
, , . Linux ( address space layout randomization , ASLR). (- , ), — - .
, , , /proc/$pid/maps. , .
3. ELF-
, , , .
:
$ nm -D /lib/x86_64-linux-gnu/libdl-2.19.so | grep dlopen 0000000000001090 T dlopen
nm . .
- , nm , . , dlsym().
— ELF-, . procfs. UNIX way, /proc/$pid/mem , — ( /proc/$pid/maps).
Linux mmap(), ( , ). :
static int map_region(pid_t pid, struct memory_region *region) { size_t length = region->vaddr_high - region->vaddr_low; off_t offset = region->vaddr_low; char path[32] = {0}; snprintf(path, sizeof(path), "/proc/%d/mem", pid); int fd = open(path, O_RDONLY); if (fd < 0) goto error; void *buffer = malloc(length); if (!buffer) goto error_close_file; if (read_region(fd, offset, buffer, length) < 0) goto error_free_buffer; region->content = buffer; close(fd); return 0; error_free_buffer: free(buffer); error_close_file: close(fd); error: return -1; } static int read_region(int fd, off_t offset, void *buffer, size_t length) { if (lseek(fd, offset, SEEK_SET) < 0) return -1; size_t remaining = length; char *ptr = buffer; while (remaining > 0) { ssize_t count = read(fd, ptr, remaining); if (count < 0) return -1; remaining -= count; ptr += count; } return 0; }
ELF- . , -, , -, .
ELF
ELF — Linux. , , .
ELF . ELF . — , . , — . ELF-.
, libdl-2.19.so :

( readelf --headers
.)
, , (29 9). — , , . ELF — , . Linux, , LOAD, ( ).
ELF- , . , .
, . «» . .bss, , ( ).
, ELF — , . ...
?
() . , dlsym(), . - .
ELF (. 2-10). , .dynamic , DYNAMIC . .dynamic , :
- .dynsym — ;
- .dynstr — ;
- .hash — -, .
, , ELF:

ELF, (1), (2), (3), (4) , 。
ELF →
() ELF <elf.h>, , , . , ELF — . 32- 64- , , , . x86_64, ELF .
ELF- ( Elf64_Ehdr ). ( program headers ), e_phoff e_phnum :

— , , ELF- — , , , , .
e_phoff, , . e_phnum e_phentsize .
( ), ELF — 64 .
→ DYNAMIC
. — Elf64_Phdr ( 64- ELF-), . PT_DYNAMIC p_type :

:
.dynamic 0x2D88 ( ). DYNAMIC — 0x202D88. 0x210 (8448) . .
DYNAMIC → .dynsym, .dynstr, .hash
.dynamic, DYNAMIC, . Elf64_Dyn , :

8 d_val d_ptr , 8- d_tag , , . :
- DT_HASH (4) — .hash ( d_ptr)
- DT_STRTAB (5) — .dynstr ( d_ptr)
- DT_SYMTAB (6) — .dynsym ( d_ptr)
- DT_STRSZ (10) — .dynstr ( d_val)
- DT_NULL (0) —
. .dynamic : , , , .
, DYNAMIC , . , , - , .
.dynamic , . -, .dynstr , ? .
. , .dynsym , . ( «» .symtab, , , . .)
Elf64_Sym , ELF — , , , . dlopen
:

:
- st_name — ,
- st_info — ( )
- st_value —
( , nm , dlopen() .text, 0x1090 .)
, .
— - , . ( ). .dynstr , libdl-2.19.so :

, ( «dlopen», 0xA5) , . .
-
.hash - , . - — — ELF-, . , .dynsym, , . ( ) - .
- <elf.h>, (. 2-19). - , :

在哪里
- nbuckets — buckets
- nchains — chains ( )
- buckets —
- chains —
- :
- h .
- i
buckets[h % nbuckets]
, . - ( ) , .
- —
chains[i % nchains]
. - 3—4 , .
-, ELF:
static uint32_t elf_hash(const char *name) { uint32_t h = 0; uint32_t g; while (*name) { h = (h << 4) + *name++; g = h & 0xF0000000; if (g) h ^= g >> 24; h &= ~g; } return h; }
, "dlopen"
- 112420542 :

libdl — , 39 , . - .
, :
- dlopen() dlsym() libdl
- pthread_create() pthread_detach() libpthread
, .
. . , .
ELF- . , ( ). , . , , . .
4. -
, , - , : , . - .
-
, -:
void shellcode(void) { void *payload = dlopen("/path/to/payload.so", RTLD_LAZY); void (*entry)(void) = dlsym(payload, "entry_point"); pthread_t thread; pthread_create(&thread, NULL, entry, NULL); pthread_detach(thread); }
?
, — . , , , - — - ! .
— - . , , : .
.section .rodata .global shellcode_start .global shellcode_address_dlopen .global shellcode_address_dlsym .global shellcode_address_pthread_create .global shellcode_address_pthread_detach .global shellcode_address_payload .global shellcode_address_entry .global shellcode_end .set RTLD_LAZY, 1 .align 8 shellcode_start: lea shellcode_address_payload(%rip),%rdi mov $RTLD_LAZY,%rsi mov shellcode_address_dlopen(%rip),%rax callq *%rax mov %rax,%rdi lea shellcode_address_entry(%rip),%rsi mov shellcode_address_dlsym(%rip),%rax callq *%rax sub $8,%rsp mov %rsp,%rdi xor %rsi,%rsi mov %rax,%rdx xor %rcx,%rcx mov shellcode_address_pthread_create(%rip),%rax callq *%rax mov (%rsp),%rdi add $8,%rsp mov shellcode_address_pthread_detach(%rip),%rax callq *%rax int $3 .align 8 shellcode_address_dlopen: .space 8 shellcode_address_dlsym: .space 8 shellcode_address_pthread_create: .space 8 shellcode_address_pthread_detach: .space 8 shellcode_address_payload: .space 256 shellcode_address_entry: .space 256 shellcode_end: .end
, . :
$ as -o shellcode.o shellcode.S
, , , . : (procedure linkage table, PLT), .
- , (, ) . - .
-
- . , , , . ?
-
, . , . , . , .
(- ), : , , . , , JIT- , . ?
:
, . -, - , . -, . -, , - -, .
, . . x86_64 int $3
— 0xCC — . ptrace() PTRACE_POKETEXT — , 8 , . , , .
, , , : . - , .
?
, ! malloc()!
. , -, . . , mmap():
void inject_shellcode(const void *shellcode_src, size_t shellcode_size) { void *shellcode_dst = mmap(NULL, shellcode_size, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0); copy_shellcode(shellcode_dst, shellcode_src, shellcode_size); }
, ptrace() , .
, ? , . Linux x86_64 :
- %rax
- — — %rsi, %rdi, %rdx, %r10, %r8, %r9
- SYSCALL,
- %rax
ptrace() PTRACE_GETREGS PTRACE_SETREGS. , . - SYSCALL.
: , %rip. , , SYSCALL.
SYSCALL
SYSCALL? , . - , - . — libc. , , , :
unsigned long find_syscall_instruction(struct library *library) { for (size_t i = 0; i < library->region_count; i++) { struct memory_region *region = &library->regions[i]; if (!(region->readable && region->executable)) continue; const uint8_t *region_data = region->content; size_t region_size = region->vaddr_high - region->vaddr_low; if (region_size < 2) continue; for (size_t offset = 0; offset < region_size - 1; offset++) { if (region_data[offset + 0] == 0x0F && region_data[offset + 1] == 0x05) { return region->vaddr_low + offset; } } } return 0; }
, /proc/$pid/maps . x86_64 , - . , 0x0F 0x05. , , ARM, 0xDF 0x00 ( SVC #0), .
PTRACE_{GET,SET}REGS
:
int get_registers(pid_t pid, struct user_regs_struct *registers) { int err = 0; if (ptrace(PTRACE_GETREGS, pid, registers, registers) < 0) err = -errno; return err; }
struct user_regs_struct
, <sys/user.h>. . . , varargs :
static int set_regs_for_syscall(struct user_regs_struct *registers, unsigned long syscall_insn_vaddr, long syscall_number, int args_count, va_list args) { registers->rip = syscall_insn_vaddr; registers->rax = syscall_number; for (int i = 0; i < args_count; i++) { switch (i) { case 0: registers->rdi = va_arg(args, long); break; case 1: registers->rsi = va_arg(args, long); break; case 2: registers->rdx = va_arg(args, long); break; case 3: registers->r10 = va_arg(args, long); break; case 4: registers->r8 = va_arg(args, long); break; case 5: registers->r9 = va_arg(args, long); break; default: return -E2BIG; } } return 0; } static long perform_syscall(pid_t pid, unsigned long syscall_insn_vaddr, long syscall_number, int args_count, ...) { struct user_regs_struct old_registers; struct user_regs_struct new_registers; get_registers(pid, &old_registers); new_registers = old_registers; va_list args; va_start(args, args_count); set_regs_for_syscall(&new_registers, syscall_insn_vaddr, syscall_number, args_count, args); va_end(args); set_registers(pid, &new_registers); wait_for_syscall_completion(pid); get_registers(pid, &new_registers); long result = new_registers.rax; set_registers(pid, &old_registers); return result; }
PTRACE_SYSCALL
: , ?
PTRACE_SYSCALL. PTRACE_CONT, . , - : , .
PTRACE_SYSCALL SIGTRAP : ( ) ( ). , ptrace() , , .
, SIGTRAP:
static int wait_for_syscall_enter_exit_stop(pid_t pid) { if (ptrace(PTRACE_SYSCALL, pid, 0, 0) < 0) return -1; if (wait_for_process_stop(pid, SIGTRAP) < 0) return -1; return 0; } void wait_for_syscall_completion(pid_t pid) { wait_for_syscall_enter_exit_stop(pid); wait_for_syscall_enter_exit_stop(pid); }
— , — (wait_for_process_stop() ). . , .
PTRACE_O_TRACESYSGOOD
, PTRACE_SYSCALL : , , - . , SIGTRAP ( ).
SIGTRAP . PTRACE_O_TRACESYSGOOD, :
- SIGTRAP — -
- SIGTRAP | 0x80 —
int ptrace_attach(pid_t pid) { if (ptrace(PTRACE_ATTACH, pid, 0, 0) < 0) return -1; if (wait_for_process_stop(pid, SIGSTOP) < 0) return -1; + /* */ + unsigned long options = PTRACE_O_TRACESYSGOOD; + if (ptrace(PTRACE_SETOPTIONS, pid, 0, options) < 0) + return -1; return 0; } static int wait_for_syscall_enter_exit_stop(pid_t pid) { if (ptrace(PTRACE_SYSCALL, pid, 0, 0) < 0) return -1; - if (wait_for_process_stop(pid, SIGTRAP) < 0) + if (wait_for_process_stop(pid, SIGTRAP | 0x80) < 0) return -1; return 0; }
-
- :
void write_shellcode(void) { char shellcode_text[SHELLCODE_TEXT_SIZE]; size_t shellcode_size = shellcode_end - shellcode_start; prepare_shellcode(shellcode_text, shellcode_size); write_remote_memory(target, shellcode_text_vaddr, shellcode_text, shellcode_size); }
- : dlopen(), .
static inline void copy_shellcode(char *shellcode_text, const char *shellcode_addr, const void *data, size_t length) { ptrdiff_t offset = shellcode_addr - shellcode_start; memcpy(shellcode_text + offset, data, length); } static void prepare_shellcode(char *shellcode_text, size_t shellcode_size) { copy_shellcode(shellcode_text, shellcode_start, shellcode_start, shellcode_size); copy_shellcode(shellcode_text, shellcode_address_dlopen, &dlopen_vaddr, sizeof(dlopen_vaddr)); copy_shellcode(shellcode_text, shellcode_address_dlsym, &dlsym_vaddr, sizeof(dlsym_vaddr)); copy_shellcode(shellcode_text, shellcode_address_pthread_create, &pthread_create_vaddr, sizeof(pthread_create_vaddr)); copy_shellcode(shellcode_text, shellcode_address_pthread_detach, &pthread_detach_vaddr, sizeof(pthread_detach_vaddr)); copy_shellcode(shellcode_text, shellcode_address_payload, payload, sizeof(payload)); copy_shellcode(shellcode_text, shellcode_address_entry, entry, sizeof(entry)); }
, , -:
extern const char shellcode_start[]; extern const char shellcode_address_dlopen[]; extern const char shellcode_address_dlsym[]; extern const char shellcode_address_pthread_create[]; extern const char shellcode_address_pthread_detach[]; extern const char shellcode_address_payload[]; extern const char shellcode_address_entry[]; extern const char shellcode_end[];
, .
- . /proc/$pid/mem, :
int write_remote_memory(pid_t pid, unsigned long vaddr, const void *data, size_t size) { char path[32] = {0}; snprintf(path, sizeof(path), "/proc/%d/mem", pid); int fd = open(path, O_WRONLY); if (fd < 0) return -1; if (lseek(fd, vaddr, SEEK_SET) < 0) { close(fd); return -1; } int err = do_write_remote_memory(fd, data, size); close(fd); return err; } static int do_write_remote_memory(int fd, const void *data, size_t size) { size_t left = size; while (left > 0) { ssize_t wrote = write(fd, data, left); if (wrote < 0) return -1; data += wrote; left -= wrote; } return 0; }
, - — « » . . - , .
5.
- . , : %rip -, PTRACE_SETREGS, PTRACE_CONT . .
, , . -? ?
, . , « » . , . :
— . dlopen() pthread_create() . - dlopen(), dlopen() ?
-, , , . , pthread_create() . , ( ). clone().
pthread_create()?
, - , ?
: clone().
, (libc) (pthread). clone() (thread control block, TCB) (thread-local storage, TLS), , . . pthread_create() , .
«», clone() libc pthread. , .
clone() :
: -?
, - : , , , .
. , , . 怎么了 : exit(). , .
. exit() -:
+.set __NR_exit, 60 .set RTLD_LAZY, 1 @@ - /* - * . - */ - int $3 + /* + * exit(0); + */ + xor %rdi,%rdi + mov $__NR_exit,%rax + syscall
: exit() — exit() . exit() , exit() — . Linux exit_group().
. . , , PROT_EXEC:
shellcode_stack_vaddr = remote_mmap(target, syscall_vaddr, 0, SHELLCODE_STACK_SIZE, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE | MAP_STACK | MAP_GROWSDOWN, -1, 0);
, Linux x86_64 — «» , . mmap() , clone() . , mmap() MAP_GROWSDOWN, , .
PTRACE_O_TRACECLONE
. , - . waitpid(), : , .
— PTRACE_O_TRACECLONE. . , . , , , . , PTRACE_ATTACH , .
-, :
- unsigned long options = PTRACE_O_TRACESYSGOOD; + unsigned long options = PTRACE_O_TRACESYSGOOD | PTRACE_O_TRACECLONE; if (ptrace(PTRACE_SETOPTIONS, pid, 0, options) < 0) return -1;
-, clone(), PTRACE_EVENT_CLONE, , PTRACE_SYSCALL. :
-void wait_for_syscall_completion(pid_t pid) +void wait_for_syscall_completion(pid_t pid, long syscall) { wait_for_syscall_enter_exit_stop(pid); + + /* clone() PTRACE_EVENT_CLONE */ + if (syscall == __NR_clone) + wait_for_clone_event(pid); wait_for_syscall_enter_exit_stop(pid); }
:
static int wait_for_clone_event(pid_t pid) { if (ptrace(PTRACE_CONT, pid, 0, 0) < 0) return -1; int event = SIGTRAP | (PTRACE_EVENT_CLONE << 8); if (wait_for_process_stop(pid, event) < 0) return -1; return 0; }
clone() PID , . :
void clear_ptrace_options(pid_t pid) { ptrace(PTRACE_SETOPTIONS, pid, 0, 0); }
, clone() ptrace(), PTRACE_O_TRACECLONE. , , - .
, - . clone() :
static int spawn_shell_thread() { shell_tid = remote_clone(target, syscall_ret_vaddr, CLONE_FILES | CLONE_FS | CLONE_IO | CLONE_SIGHAND | CLONE_SYSVSEM | CLONE_THREAD | CLONE_VM, shellcode_stack_vaddr + SHELLCODE_STACK_SIZE); if (!shell_tid) return -1; return 0; }
clone() : , , , . , .
CLONE_FILES, CLONE_FS, CLONE_IO, CLONE_SIGHAND, CLONE_SYSVSEM, CLONE_VM — . , CLONE_FILES , ( fork()). — — , . . , CLONE_VM , , .
CLONE_THREAD : Linux — « », . , , getpid() , kill() — - , execve() — , .
, clone() fork(): , . clone() : , — . . ( , , .)
, pthread_create() , , . ?
fork() :
pid_t child = fork(); if (child < 0) { } if (child == 0) { }
, . clone() . .
. , clone() , . syscall ret, , . .
SYSCALL + RET
, . , syscall ret:
-if (region_size < 2) +if (region_size < 3) continue; /* * 0F 05 syscall + * C3 retq */ -for (size_t offset = 0; offset < region_size - 1; offset++) { +for (size_t offset = 0; offset < region_size - 2; offset++) { if (region_data[offset + 0] == 0x0F && - region_data[offset + 1] == 0x05) + region_data[offset + 1] == 0x05 && + region_data[offset + 2] == 0xC3) { return region->vaddr_low + offset; } }
, .
. prepare_shellcode() , , :
void write_shellcode(void) { char shellcode_text[SHELLCODE_TEXT_SIZE]; size_t shellcode_size = shellcode_end - shellcode_start; /* , , . . */ prepare_shellcode(shellcode_text, shellcode_size); /* - */ write_remote_memory(target, shellcode_text_vaddr, shellcode_text, shellcode_size); + /* «» */ + unsigned long retaddr_vaddr = + shellcode_stack_vaddr + SHELLCODE_STACK_SIZE - 8; + write_remote_memory(target, retaddr_vaddr, + &shellcode_text_vaddr, sizeof(shellcode_text_vaddr)); }
, , .
, , . System V ABI , ( %rsp) 16 . shellcode_stack_vaddr + SHELLCODE_STACK_SIZE
: ( 4096 ), 1 . 8 , , retq, - . - :
- sub $8,%rsp + sub $16,%rsp /* */ mov %rsp,%rdi xor %rsi,%rsi mov %rax,%rdx xor %rcx,%rcx mov shellcode_address_pthread_create(%rip),%rax callq *%rax
, %rsp 16 pthread_create(). SIGSEGV, — pthread_create() , .
, - , clone():
static int spawn_shell_thread() { shell_tid = remote_clone(target, syscall_ret_vaddr, CLONE_FILES | CLONE_FS | CLONE_IO | CLONE_SIGHAND | CLONE_SYSVSEM | CLONE_THREAD | CLONE_VM, /* ** */ - shellcode_stack_vaddr + SHELLCODE_STACK_SIZE); + shellcode_stack_vaddr + SHELLCODE_STACK_SIZE - 8); if (!shell_tid) return -1; return 0; }
ptrace() SIGSTOP, :
int ignore_thread_stop(pid_t pid) { return wait_for_process_stop(pid, SIGSTOP); }
. ptrace():
void resume_thread(pid_t pid) { ptrace(PTRACE_CONT, pid, 0, 0); }
, , , exit(). waitpid(). — CLONE_THREAD wait() ,— PTRACE_O_TRACECLONE, :
int wait_for_process_exit(pid_t pid) { int status = 0; if (waitpid(pid, &status, 0) < 0) return -1; if (!WIFEXITED(status)) return -1; return WEXITSTATUS(status); }
pthread , , pthread_join() pthread , . , — . , , .
可用内存
, - . , - , munmap():
void remote_munmap(pid_t pid, unsigned long syscall_insn_vaddr, unsigned long addr, size_t len) { perform_syscall(pid, syscall_insn_vaddr, __NR_munmap, 2, (long) addr, (long) len); } static void unmap_shellcode() { remote_munmap(target, syscall_ret_vaddr, shellcode_text_vaddr, SHELLCODE_TEXT_SIZE); remote_munmap(target, syscall_ret_vaddr, shellcode_stack_vaddr, SHELLCODE_STACK_SIZE); }
, , , — ptrace() . (, SIGSTOP), , ( ):
int stop_thread(pid_t pid) { if (kill(pid, SIGSTOP) < 0) return -1; if (wait_for_process_stop(pid, SIGSTOP) < 0) return -1; return 0; }
, , . PTRACE_DETACH:
int ptrace_detach(pid_t pid) { if (ptrace(PTRACE_DETACH, pid, 0, 0) < 0) return -1; return 0; }
, . , . , .
结论
? . , , .

Linux . GTK+ . , make:
libpayload.so: payload.c $(CC) $(CFLAGS) $(shell pkg-config --cflags --libs gtk+-3.0) -shared -o $@ $<
entry() GTK- — GTK UI , :
#include <glib.h> #include <gtk/gtk.h> static gboolean actual_entry(gpointer _arg) { /* : */ hook_gtk_entry_constructor(); /* FALSE, */ return FALSE; } void entry(void) { /* -, */ g_idle_add_full(G_PRIORITY_DEFAULT_IDLE, actual_entry, NULL, NULL); }
, GTK , GtkEntry . "input-purpose" . «», , .
GTK glib — — GtkEntry . constructed(), . :
static void (*old_gtk_entry_constructed)(GObject *object); static void new_gtk_entry_constructed(GObject *object) { GtkEntry *entry = GTK_ENTRY(object); old_gtk_entry_constructed(object); } static void hook_gtk_entry_constructor(void) { GTypeClass *entry_type_class = g_type_class_peek(GTK_TYPE_ENTRY); GObjectClass *entry_object_class = G_OBJECT_CLASS(entry_type_class); old_gtk_entry_constructed = entry_object_class->constructed; entry_object_class->constructed = new_gtk_entry_constructed; }
GtkEntry :
, GtkEntry , , . , :
static void new_gtk_entry_constructed(GObject *object) { GtkEntry *entry = GTK_ENTRY(object); old_gtk_entry_constructed(object); g_signal_connect(entry, "notify::input-purpose", G_CALLBACK(input_purpose_changed), NULL); g_signal_connect(entry, "icon-press", G_CALLBACK(icon_pressed), NULL); g_signal_connect(entry, "icon-release", G_CALLBACK(icon_released), NULL); }
. , . .
static void input_purpose_changed(GtkEntry *entry) { GtkInputPurpose purpose = gtk_entry_get_input_purpose(entry); if (purpose == GTK_INPUT_PURPOSE_PASSWORD) { gtk_entry_set_icon_activatable(entry, GTK_ENTRY_ICON_PRIMARY, TRUE); gtk_entry_set_icon_from_icon_name(entry, GTK_ENTRY_ICON_PRIMARY, "list-remove"); } else { gtk_entry_set_icon_activatable(entry, GTK_ENTRY_ICON_PRIMARY, FALSE); gtk_entry_set_icon_from_icon_name(entry, GTK_ENTRY_ICON_PRIMARY, NULL); } }
: , , , - , :
static void icon_pressed(GtkEntry *entry, GtkEntryIconPosition position) { if (position != GTK_ENTRY_ICON_PRIMARY) return; gtk_entry_set_visibility(entry, TRUE); gtk_entry_set_icon_from_icon_name(entry, GTK_ENTRY_ICON_PRIMARY, "list-add"); } static void icon_released(GtkEntry *entry, GtkEntryIconPosition position) { if (position != GTK_ENTRY_ICON_PRIMARY) return; gtk_entry_set_visibility(entry, FALSE); gtk_entry_set_icon_from_icon_name(entry, GTK_ENTRY_ICON_PRIMARY, "list-remove"); }
仅此而已。
GitHub (GPLv2).
, . gdb :
$ gdb --pid $(pgrep target) \ --batch \ -ex 'compile file -raw shell-code.c'