четверг, 2 мая 2024 г.

yet another linux process injection

As you my know there are two methods

1) using LD_PRELOAD, don`t work if you want to inject into already running (and perhaps even many days) process

2) ptrace. Has the following inherent disadvantages

  • target process can be ptraced by somebody else
  • victim program can detect ptrace
  • you just want to avoid in logs something like ptrace attach of "./a.out"[PID] was attempted by "XXX"

So I developed very rough analogs of famous VirtualAllocEx/VirtualProtectEx + simple hook to hijack execution onto assembly written shellcode to call dlopen/dlsym. Currently only x86_64 supported bcs I am too lazy to rewrite this asm stub

Prerequisites
You must have root privileges and be able to build and load kernel modules. I tested code on kernel 6.8, 5.15 and probably it also can work on 4.x, not sure about more old versions

Lets start lighting the dirty details in reverse order

Asm code in target process

Due to the fact that my goal was to load arbitrary shared library target process must at least have ld.so, so statically linked processes are immune to this injection (greetings to Go binaries). Asm code is pretty straightforward - it just call dlopen, then dlsym("inject") and call what it returned

 

execution hijacking

Now we need somehow intercept normal execution flow and force asm stub to run. As usually there are several methods
  • good old malloc_hook trick. Actually I used free_hook too in my implementation
  • patch PLT/GOT. Presence of RELRO makes this method slightly harder but don`t forget that you have code in kernel - couple of additional mprotect calls can fix this
  • for stdc++ there is also set_new_handler function
     

run kernel code in context of right process

As far as I understand the main reason that linux kernel never had analogs of VirtualAllocEx/VirtualProtectEx is that functions do_mmap/do_mprotect_pkey always operating with current process. Ok, not big deal - we just must find method to execute our code in context of right process. As usually there are plenty ways to do this

kprobe

probabilistic method - if you know which kernel functions most often called by target process - you can register kprobe on one of them. Obvious drawback is that your kprobe will be fired for all processes and this can lead to performance degradation

task_struct.restart_block

Unfortunately it doesn't work. I've made for my lkmem -p option to show task details, for normal processes it looks like

PID 557580 at 0xffff90e2507b99c0
 thread.flags: 0
 flags: 400000
 sched_class: 0xffffffff976f4818 - kernel!fair_sched_class
 restart_block.fn: 0xffffffff960cfbc0 - kernel!do_no_restart_syscall

With patched restart_block.fn: 

PID 557580 at 0xffff90e2507b99c0
 thread.flags: 0
 flags: 400000
 sched_class: 0xffffffff976f4818 - kernel!fair_sched_class
 restart_block.fn: 0xffffffffc12d9a80 - lkcd!main_horror

for unknown reason restart_block.fn was never called

preempt_notifier_register

Don`t ask me why but registered notifier was never called

task_works

I`ve choose it for my implementation. Just couple of functions task_work_add & task_work_cancel. lkmem -p shows for such processes something like
PID 582439 at 0xffff90e151b8b380
 thread.flags: 0
 flags: 400000
 works_count: 1
  work[0] 0xffffffffc12d9a80 - lkcd!main_horror

process death notification

and finally the last piece - we should be able to cancel our injection in case of process suddenly die (for many reasons - we are buggy, process voluntarily decided to leave this cruel world or meet OOM killer etc). Otherwise we can wait for result forever. 
Actually this was hardest part - until kernel 5.17 the grass was greener and the world was simpler. You could just call profile_event_register(PROFILE_TASK_EXIT). Then this evil clowns calling themselves "maintainers" killed it and instead ask to use trace_sched_process_exit which is even cannot be found at elixir.bootlin or other piece of dead code register_trace_prio_sched_process_free/register_trace_prio_sched_process_exit (and they still can`t decide which one is more trve). Anyway I've solved this problem and very proud of it
 

Putting all together

Lets inject some shared library to test process
./test -t
pid 593502
 
on other console load driver and run
/a.out -p 593502 -i `pwd`/test.so
dlopen 0x7f8747ea1e48 0x7f8747ea1e48
/usr/lib/x86_64-linux-gnu/libc-2.31.so base 7F34E1643000
wait
...
injected at 0x7f34e1ad2000
 
Now first console shows
[+] greeting from injected, addr 0x7f34e1ad2000
 
And yet another couple of proofs:
dmesg | tail
patch 7F34E182FB70 to 7F34E1AD2000 and 7F34E1831E48 to 7F34E1AD2009
 
grep 7f34e1ad2000 /proc/593502/maps
7f34e1aca000-7f34e1ad2000 r--p 00024000 103:02 20187524                  /usr/lib/x86_64-linux-gnu/ld-2.31.so
7f34e1ad2000-7f34e1ad3000 r-xp 00000000 00:00 0 
 
grep test.so /proc/593502/maps
7f34e1875000-7f34e1876000 r--p 00000000 08:02 4456719                  /home/redp/lkcd/inject/test.so
7f34e1876000-7f34e1877000 r-xp 00001000 08:02 4456719                  /home/redp/lkcd/inject/test.so
7f34e1877000-7f34e1878000 r--p 00002000 08:02 4456719                  /home/redp/lkcd/inject/test.so
7f34e1878000-7f34e1879000 r--p 00002000 08:02 4456719                  /home/redp/lkcd/inject/test.so
7f34e1879000-7f34e187a000 rw-p 00003000 08:02 4456719                  /home/redp/lkcd/inject/test.so 

Комментариев нет:

Отправить комментарий