在上一篇文章《Linux x86内核终止D状态的进程》(http://www.linuxdiyf.com/linux/26883.html)中,我展示了32位x86系统中如何编码杀死D进程。本文我将展示一种64位x86系统上的方法。
说实话,64位系统上做这样的事是比较难的,因为你无法通过修改p->thread.ip来到达将进程拽出死循环的目的。要想知道64位系统上到底该怎么把进程执行绪引出,我们得先看看”标准“的做法是什么。
标准的做法就是fork时的行为,一个新进程刚刚被创建,它第一次进入运行状态之前,并不是通过switch_to切出的,为了让它”看起来像“是被切出而后被切入,就需要ret_from_fork来制造现场。问题是既然无法修改p->thread.ip,那又如何把执行绪引导到ret_from_fork里。
答案在于,64位(这里特指x86_64)系统是在switch_to中直接通过标志位判断跳转的,其过程如下:
1.在copy_thread中设置TIF_FORK标志
2.在switch_to中判断TIF_FORK标志是否存在,若存在则直接跳转到ret_from_fork
因此ret_from_fork在64位系统中是硬编码到switch_to中的,不像32位系统中那样是可以随意修改的。
到这里,想通过修改堆栈上保存的PC寄存器来达到跳出循环的这条心也该死了。一个进程被切入,要么循着被切出之前的路径走,要么进入ret_from_fork,只有这两条路。如果循着之前的路,那还是在死循环里面,那么只能给D进程设置TIF_FORK标记,引导它进入ret_from_fork!
然而我们并不是真的希望它return from fork,而是因为这是不得已的办法,它只能到ret_from_fork里面。接下来怎么办?
接下来的技术涉及到inline hook,我们希望hook掉ret_from_fork这个entry!具体如何inline hook,本文不讲,不然本文又要很长很长了。本文仅仅给出ret_from_fork被hook后的样子:
ENTRY(ret_from_fork)
DEFAULT_FRAME
LOCK ; btr $TIF_FORK,TI_flags(%r8)
push kernel_eflags(%rip)
CFI_ADJUST_CFA_OFFSET 8
popf# reset kernel eflags
CFI_ADJUST_CFA_OFFSET -8
call schedule_tail # rdi: 'prev' task parameter
GET_THREAD_INFO(%rcx)
testl $_TIF_D, TI_flags(%rcx) # 这里判断是不是有新增的TIF_D标识,如果有,就直接do_exit
jnz do_exit
RESTORE_REST
testl $3, CS-ARGOFFSET(%rsp) # from kernel_thread?
je int_ret_from_sys_call
testl $_TIF_IA32, TI_flags(%rcx) # 32-bit compat task needs IRET
jnz int_ret_from_sys_call
RESTORE_TOP_OF_STACK %rdi, -ARGOFFSET
jmp ret_from_sys_call # go to the SYSRET fastpath
CFI_ENDPROC
END(ret_from_fork)
然后,模块里非常简单的设置TIF_FORK和TIF_D即可:
if (pid > 0) {
for_each_process(p) {
if (task_pid_vnr(p) == pid) {
set_task_state(p, TASK_INTERRUPTIBLE);
// 设置TIF_FORK,目的是执行流导入ret_from_fork
set_tsk_thread_flag(p, TIF_FORK);
// 设置TIF_D,目的是将执行流在hook后的ret_from_fork里进行区分
set_tsk_thread_flag(p, TIF_D);
wake_up_process(p);
break;
}
}
}
和32位系统的实验方法一样,D进程将被拉出死循环,然后死掉!
注意,用kprobe/jprobe技术进行hook事实上就是一种inline hook的应用,然而我们不方便用它来hook ret_from_fork,因为你既不能在ret_from_fork之前执行hook,也不能之后执行hook,而必须在其中间,即调用完schedule_tail之后去执行do_exit,因此,正确的做法是hook别的函数而不是hook ret_from_fork这个函数。仔细观察ret_from_fork的汇编码,就会发现在int_ret_from_sys_call,ret_from_sys_call的pre handler中进行TIF_D的判断并且执行do_exit应该是正确的做法!