PWN(3)
ret2stack
在/proc/sys/kernel/randomize_va_space
中可以控制ASLR的关闭,其值为0表示关闭,为2表示完全打开。
假如我们有一个ret2stack文件
1 | #include <stdio.h> |
接下来编译。编译需要关掉各种各样的保护:
1 | #!/bin/sh |
这样我们就可以对应的写shellcode了:
1 | from pwm import * |
然而这样攻击是失败的。这是因为gdb中基地址不可靠。我们发现,gdb中地址是e060,而直接打印出来是e0e0。所以要改成实际的基地址。
ret2syscall
系统调用是操作系统给用户的接口,可以被链接库链接成一个函数。能接触系统的代码都位于For Kernel,并且存在一些内核函数。
考虑下面一个C代码:
1 | #include <stdio.h> |
在C语言中的write被动态链接库所连接,并自动调用。
用命令ldd a.out
可以查看所有动态链接库,常见的漏洞出现在libc.so.6。动态链接库文件是在不断更新的,而编译器在编译时候会将链接库写入程序中,是用软链接实现的,这一点类似一个快捷方式,其目录位于/lib/x86_64-linux-gnu
中。
可以反编译动态链接库,有一个system函数。那我们可以返回到这一函数,实现系统调用。
另一个函数是execve('/bin/sh')
,也是一个很常用的函数。
汇编中int指令用来中断,int 80调用的是系统调用函数。那么假如我们有这样一段代码:
1 | mov eax, 0xb |
就可以实现系统调用,然而并没有这样一段代码。所以我们需要构造一个gadget
:汇编中的某些固有代码片段。
利用ROPgatdet
可以查看一些特定的代码片段:ROPgatget --binary xxx --only "[pop|ret]"
。例如
1 | 0x000000000040125c : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret |
我们在代码的基础上继续进行溢出并存放在栈里,可以认为这些gadget是一些积木。我们用下面这个例子来说明。
其过程如何执行呢?
- stack overflow之后ret,ESP增加字长到下一个位置,EIP跳转到下一条指令
- pop %edx ,ESP继续向前移动
- ret,ESP增加字长到下一个位置,EIP跳转到下一条指令
- xor %eax, %eax,相当于清空eax的值,然后ret,ESP增加字长到下一个位置,EIP跳转到下一条指令
- mov %eax, (%edx),将edx的值赋给eax,然后ret,ESP增加字长到下一个位置,EIP跳转到下一条指令
现在我们就可以用下面的流程实现:
- pop_eax_ret address
- 0xb
- pop_ebx_ret address
- “/bin/sh” address
- pop_ecx_ret address
- 0
- pop_edx_ret address
- 0
- int 0x80 address
这样的效果就是,我们把eax赋值为0xb,ebx赋值为/bin/sh,ecx赋值为0,edx赋值为0.
接下来看看例题。
在IDA反编译,可以找到有一个”/bin/sh”的ao东西。
先找gadget,发现有两个可能可以用:
- 0x080bb196 : pop eax ; ret
- 0x0806eb90 : pop edx ; pop ecx ; pop ebx ; ret
那么我们构造的应该是
- 0x080bb196
- eax的值0xb,表示execve函数
- 0x0806eb90
- edx的值0
- ecx的值0
- ebx的值0x80BE408
- int 80的位置0x8049421
接下来解题。先用gdb发现距离是108,那么要填充112字节。就可以写脚本了。
1 | from pwn import * |
找/bin/sh可以使用pwntools的如下函数:
1 | elf = ELF("./ret2syscall") |
动态链接
刚刚的一切都建立在我们能找到gadget的前提下,然而往往这不是能一蹴而就的。因此,我们试图用其他方法来找到,而这就与动态链接有关。
1 | gcc -fno-pie --static -o sttest test.c |
这样我们输出的是一个静态程序。如果删去了--static
参数,得到的则是一个动态链接。
二者的大小差别是很大的。例如puts函数,动态链接库几乎没有具体代码,而静态链接库则涉及到了非常庞大的函数。那么二者究竟有甚么区别呢?
总的来说,程序的变化经历了这些过程:编译 汇编 链接 装载 执行。链接将目标文件转化为可执行文件,而装载将可执行文件载入内存、转化为可执行文件。而动态链接在函数装载的时候执行,静态链接则在链接的时候就载入。
动态链接的特点是,我们并不知道运行时候的真实地址是多少。其结构如下:
Sections:
- .dynamic 提供动态链接相关信息,比如表的位置
- .got, global opposite table,表示所有变量的偏移量也就是地址
- .got.plt, 表示所有函数的地址
- .data
假设有这样一份C代码
1 | #include <stdio.h> |
这里的foo是我们假设存在于stdio库里的函数。
在text段,我们调用了call foo@plt。那么如何找到foo呢?我们就去plt节里找。每一个动态链接库里的函数都会在plt里创建一个表象。
plt表里的形式大概是这样:read, printf, system, foo, …,在call的时候就找到了这个表象。由于.got.plt存了全局地址,plt开始会向.got.plt表里寻找foo这个函数的真实地址。然而在初次调用的时候,我们还没有装载.got.plt进去,所以.got.plt就指向了plt的对应位置,跳回了plt表中。
接下来,plt就会找到foo的真实地址,然后将他存在.got.plt中。我们可以认为,plt就是B细胞,.got.plt就是记忆细胞。在发挥作用之前,B细胞会先检查有没有存在的记忆细胞,如果有就取过来分化。如果没有,就自己去找。
所以我们跳到了PLT0,在*(GOT+8)找到了。我们进入dl_resolve中,开始执行解析任务。结果会传到.got.plt中,此时存储的是这个函数在表中的真实地址。
而如果我们再一次的调用这一函数,我们就直接在.got.plt中取到其位置,完成了相关操作。