环境配置
软硬件环境
- 物理机
- 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 ni
2次,直到调用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
,这样input
和password
两个变量就相等了,于是验证成功。
改变程序控制流(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时),我们再看一下堆栈内容。
上图中,溢出的内容从0xffffd010
到0xffffd024
(闭区间)。显然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 %edx
,edx
的值来自leal .LC0@GOTOFF(%eax), %edx
而标号.LC0
保存了.string "/bin/sh"
。因此在32位下system
是通过栈传参的,在调用system
前,将参数压栈就行了。
现在回过头来看shellcode
的内容,8个8
是用来填充变量buf
的,12个A
依旧用来填充,\xb0\x7c\xc4\xf7
是system
函数的地址,\xc0\xa1\xc3\xf7
是exit
函数的地址,\x40\xc0\x04\x08
是变量c
(内容为/bin/sh
)的地址。shellcode
共32字节,对应下图中0xffffd008
到0xffffd0208
(闭区间)。
关于64位下栈溢出的讨论
在32位环境下许多函数通过栈传参(已经过我验证的有printf
、system
等)。而在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