Featured image of post Hgame2025_week1 剩余题解

Hgame2025_week1 剩余题解

解出总计10题

hgame week1 wp

misc

Hakuya Want A Girl Friend

下载得到一个hky.txt 打开一看有一大堆的十六进制文本,放到010editor中后导出文件,然后放到随波逐流


提取出一个压缩包,刚打开时显示已损坏但是还可以继续查看,那么压缩文件是没问题的

然后我在010editor中疯狂找密码找不出来,还傻乎乎地去锤出题人 后来发现结尾是0A 1A 0A 0D 47 4E 50 89,反过来就是png文件的头,把压缩包部分删去后反转一下 附上反转脚本:

反转脚本

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def reverse_hex_file(input_file, output_file):
    with open(input_file, 'rb') as f:
        data = f.read()
    
    reversed_data = data[::-1]
    
    with open(output_file, 'wb') as f:
        f.write(reversed_data)

# 使用示例
reverse_hex_file(r"E:\CTF\Hgame\misc\hakuya\1", 'fixed_png.png')

得到一张png图片,看了看没什么其他信息后应该是改宽高了,放随波逐流

alt text

alt text
我刚开始还以为解压密码是hakuya的QQ号,但是不对,那么应该就是里面的这个密码 解压,得到flag hgame{h4kyu4_w4nt_gir1f3nd_+q_931290928}

Level 314 线性走廊中的双生实体

呃呃下载得到一个entity.pt文件,不知道是什么东西, 010editor看一下

alt text

发现一大堆pk,放到随波逐流里解压试试
alt text

提取出来一个model?还是不知道是什么东西,上网搜一下
在这篇文章 找到了类似的东西 so——.pt文件就是PyTorch框架的模型保存格式,其中会保存PyTorch模型的结构和参数,以便后续加载并使用这些模型来推理和训练
那么按照题目意思来说的话,我们首先要加载模型

1
2
model_path = r'E:\CTF\Hgame\misc\level_314\entity.pt'  # 请替换为实际路径
model = torch.jit.load(model_path)

然后我们要给模型输入一组张量,尝试激活得到输出消息,那么张量的值要怎么确定?,考虑到题目中的确保稳定态,我们到调用的__torch__.py中看一看

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
  def forward(self: __torch__.SecurityLayer,
    x: Tensor) -> Tensor:
    _0 = torch.allclose(torch.mean(x), torch.tensor(0.31415000000000004), 1.0000000000000001e-05, 0.0001)
    if _0:
      _1 = annotate(List[str], [])
      flag = self.flag
      for _2 in range(torch.len(flag)):
        b = flag[_2]
        _3 = torch.append(_1, torch.chr(torch.__xor__(b, 85)))
      decoded = torch.join("", _1)
      print("Hidden:", decoded)
    else:
      pass
    if bool(torch.gt(torch.mean(x), 0.5)):
      _4 = annotate(List[str], [])
      fake_flag = self.fake_flag
      for _5 in range(torch.len(fake_flag)):
        c = fake_flag[_5]
        _6 = torch.append(_4, torch.chr(torch.sub(c, 3)))
      decoded0 = torch.join("", _4)
      print("Decoy:", decoded0)
    else:
      pass
    return x

发现这么一个函数,那么我们就可以确定张量的值了,只要使张量的均值限制在0.31415,同时要注意 输入张量的现实稳定系数(atol)必须≤1e-4 系统才会给我们flag的值

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
import torch

# 加载TorchScript模型
model_path = r'E:\CTF\Hgame\misc\level_314\entity.pt'  # 请替换为实际路径
model = torch.jit.load(model_path)
model.eval()  # 设置为评估模式


input_tensor = torch.randn(1, 10)

# 计算当前均值
current_mean = torch.mean(input_tensor)
print(f"当前均值: {current_mean.item()}")


target_mean = 0.31415000000000004

# 计算需要添加到每个元素上的值
adjustment = target_mean - current_mean

# 调整输入张量,使其均值接近0.31415000000000004
adjusted_input_tensor = input_tensor + adjustment
print(f"调整后的均值: {torch.mean(adjusted_input_tensor).item()}")

atol = 1e-5
output = model(adjusted_input_tensor)

# 输出结果
print("模型输出:", output)

按道理来说应该一次就可以得到了,但是误差控制可能有点小问题,多试了几下出了

1
2
3
4
当前均值: 0.41629067063331604
调整后的均值: 0.31415003538131714
Hidden: flag{s0_th1s_1s_r3al_s3cr3t}
模型输出: tensor([[0.5456]])

flag{s0_th1s_1s_r3al_s3cr3t} 后继:其实题目中周率三分隐玉衡,十方镜界启玄晶诗句的意思就在提醒我们0.314这个参数就是$\Pi$/10

computer cleaner

题目要求有三个:

  1. 找到攻击者的webshell连接密码
  2. 对攻击者进行简单溯源
  3. 排查攻击者目的

先进入虚拟机,简单翻找一下

alt text
flag part3
alt text
flag part2 攻击目的就是利用webshell获取flag3
alt text
溯源找到ip ip:121.41.34.25

然后我在这卡了半天,我一直以为本地会有记录或者中间部分就是flag,结果。。

alt text

直接访问就有中间的flag了

hgame{y0u_hav3_cleaned_th3_c0mput3r!}

web

Level 24 Pacman

送分题

alt text

只有前端,肯定不是玩游戏玩到10000分 失败一次试试
alt text

base64解码+2栏栅栏密码,得到hgame{practice_makes_perfet},答案不是这个,提交会显示
alt text
再观察一下
alt text

有两串base64字符串,上面这串就是

hgame{u_4re_pacman_m4ster}

Level 47 BandBomb

解析

下载得到附件app.js,后端代码中翻找

 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
app.post('/upload', (req, res) => {
  upload.single('file')(req, res, (err) => {
    if (err) {
      return res.status(400).json({ error: err.message });
    }
    if (!req.file) {
      return res.status(400).json({ error: '没有选择文件' });
    }
    res.json({ 
      message: '文件上传成功',
      filename: req.file.filename 
    });
  });
});

app.post('/rename', (req, res) => {
  const { oldName, newName } = req.body;
  const oldPath = path.join(__dirname, 'uploads', oldName);
  const newPath = path.join(__dirname, 'uploads', newName);

  if (!oldName || !newName) {
    return res.status(400).json({ error: ' ' });
  }

  fs.rename(oldPath, newPath, (err) => {
    if (err) {
      return res.status(500).json({ error: ' ' + err.message });
    }
    res.json({ message: ' ' });
  });
});

一个文件上传一个文件重命名函数,没怎么做过web不是很懂,不过可以通过nodejs布置本地,加上const port=3000,本地node app.js运行后可以在本地先测试一下

打开靶机看看

alt text
难绷ave mujica
这里可以选择本地文件和上传文件,问了一下ai这题使用的是express框架 (之前杭助学后端的时候学了一点nodejs)
再回去看一下,/rename并没有对newName进行过滤,所以是不是可以包含路径? 并且multerfileFilter并不会过滤文件类型,上传 可执行脚本的话就会直接解析!! 那么就有尝试的思路了
但是我不会写web脚本将上面的思路喂给deepseek后概括一下

  1. 在文件上传的时候,是否可以上传一个带有恶意代码的js文件,并让服务器执行?例如,如果上传的文件被当作Node.js模块加载,但需要特定的条件。或者,通过模板注入,因为应用使用ejs模板引擎。如果攻击者可以控制模板的内容,那么可能进行RCE。
  2. 由于文件会渲染mortis模板,所以我们覆盖为mortis.ejs并插入恶意代码执行
  3. 渲染后访问首页,触发模板渲染并执行恶意代码 OK思路完全畅通,开始动手

解题

在本地创建一个mortis.ejs文件,由于不知道flag会在哪,于是

1
<%= global.process.mainModule.require('child_process').execSync('cat /flag') %>

先这样尝试一下,然后上传文件之后在apifox中发送请求

alt text

目录似乎不对,调整一下
alt text

ok成功,但是没有flag,所以应该是藏在了其他地方,或许是环境变量?(pwn题也有类似的题目过) 修改一下

1
<%= global.process.mainModule.require('child_process').execSync('cat /proc/self/environ') %>

alt text

找到flag

hgame{ave_mUjIC4_h4S-bROKEN-uP-but-we-hAv3-UMlTakI12}

crypto

sieve

解析

两种不同口径的筛子才能筛干净 题目主要部分

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
def trick(k):
    if k > 1:
        mul = prod(range(1,k)) 
        if k - mul % k - 1 == 0:
            return euler_phi(k) + trick(k-1) + 1
        else:
            return euler_phi(k) + trick(k-1)
    else:
        return 1

e = 65537
p = q = nextprime(trick(e^2//6)<<128)
n = p * q
enc = pow(m,e,n)
print(f'{enc=}')

打开sage文件可以看到使用了euler_phi(k)和trick(k),一个是欧拉函数,一个是递归函数, e^2//6=715,849,728 n=7e8
考虑两种筛法可能就是要对大数的筛选进行优化 这里要考虑如何优化

  1. 使用可以同时计算欧拉函数和素数个数的筛法,搜索得到例如线性筛法就可以在o(n)内计算出结果n=7e8时应该不会再很长时间后才能跑出来
  2. 分段筛法
  3. 寻找能将sum_phi和prime_count结合的高效计算方式
\[ \text{trick}(k) = \sum_{i=1}^{k} \varphi(i) + \pi(k) \]

我一开始的解法

 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
from sympy import nextprime
from Crypto.Util.number import inverse, long_to_bytes

def compute_trick(k):
    if k == 0:
        return 0
    # 线性筛法计算欧拉函数和素数
    phi = list(range(k + 1))
    is_prime = [True] * (k + 1)
    primes = []
    sum_phi = 0
    prime_count = 0
    for i in range(2, k + 1):
        if is_prime[i]:
            primes.append(i)
            phi[i] = i - 1
            prime_count += 1
        for p in primes:
            if i * p > k:
                break
            is_prime[i * p] = False
            if i % p == 0:
                phi[i * p] = phi[i] * p
                break
            else:
                phi[i * p] = phi[i] * (p - 1)
    sum_phi = sum(phi[1:]) + 1  # phi[1]=1
    return sum_phi + prime_count

e = 65537
k = (e ** 2) // 6  # k = 715849728

trick_k = compute_trick(k)
shifted_val = trick_k << 128

p = nextprime(shifted_val)
n = p * p  

# 私钥d
phi_n = p * (p - 1)
d = inverse(e, phi_n)

enc = 2449294097474714136530140099784592732766444481665278038069484466665506153967851063209402336025065476172617376546

m = pow(enc, d, n)

print("FLAG =", long_to_bytes(m).decode())

遗憾的是在跑了半个小时之后宣告失败了,实际上跑出来是一堆乱码,compute_trick函数计算错误了,线性筛法存储整个数组效率野果低 然后思考了一下,想起之前ACM课教的位压缩和优化,顺手问了一下deepseek,优化了一下脚本。 思路:

  1. 解析题目中的“两种不同孔径的筛子”指的是线性筛法和埃拉托斯特尼筛法,或者欧拉筛法和另一种筛法,用于计算欧拉函数和素数个数。
  2. 根据trick函数的逻辑,正确计算trick(k)=sum_phi + prime_count,使用高效的筛法实现。
  3. 优化compute_trick函数,使其在Python中能够处理k=7e8,可能使用numpy或其他优化库。
  4. 生成正确的p和q,解密得到flag。 考虑到时间要求,我们不可能跑所有的trick(k),并计算p=nextprime(trick(k)«128)去解密。

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
from sympy import nextprime, primepi
from Crypto.Util.number import inverse, long_to_bytes

def compute_trick(k):
    # 计算欧拉函数前缀和与素数个数
    sum_phi = 1  # phi(1)=1
    phi = [0] * (k + 1)
    phi[1] = 1
    primes = []
    min_prime = [0] * (k + 1)
    
    for i in range(2, k + 1):
        if min_prime[i] == 0:
            primes.append(i)
            min_prime[i] = i
            phi[i] = i - 1
        else:
            p = min_prime[i]
            j = i // p
            if min_prime[j] == p:
                phi[i] = phi[j] * p
            else:
                phi[i] = phi[j] * (p - 1)
        sum_phi += phi[i]
        for p in primes:
            if p > min_prime[i] or i * p > k:
                break
            min_prime[i * p] = p
    
    prime_count = primepi(k)  # 使用 sympy 的高效素数计数
    return sum_phi + prime_count

# 参数设置
e = 65537
k = (e ** 2) // 6  

# 计算 trick(k)
print("Calculating trick(k)...")
trick_k = compute_trick(k)
print(f"trick(k) = {trick_k}")

# 生成 p 和 q
shifted_val = trick_k << 128
p = nextprime(shifted_val)
n = p * p  


phi_n = p * (p - 1)
d = inverse(e, phi_n)


enc = 2449294097474714136530140099784592732766444481665278038069484466665506153967851063209402336025065476172617376546
m = pow(enc, d, n)

flag = long_to_bytes(m).decode()
if flag.startswith("hgame{"):
    print(f"FLAG = {flag}")
else:
    print("Decrypted message is not in flag format.")

alt text

hgame{sieve_is_n0t_that_HArd}

re

COMPRESS_DOT_NEW

下载解压得到两个文件,enc_txt一大堆数据文本看不懂 还有一个compress.nu文件,也不知道是什么,搜索一下找到nushell这样一个终端

  1. NUSHELL解决 compress.nu脚本定义了一系列函数,用于构建Huffman树、生成编码表、以及进行数据压缩。
  • into b:将输入转换为字节数组。

  • gss 和 gw:用于从树节点中提取字符和权重。

  • oi:插入节点到有序列表中。

  • h:构建Huffman树。

  • gc:生成字符到编码的映射表。

  • sk:简化树结构。

  • bf:计算字符频率。

  • enc:使用编码表对输入数据进行编码。 下载完之后

1
2
source compress.nu
open enc.txt --raw | into json | each {|x| $x.a} | compress

就可以了(一开始nu命令用不了就做不出来)

2.脚本解决 enc.txt中的前半部分是一个JSON树结构,后半部分是二进制数据 先把前半部分提取出来 例如

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
{
  "a": {
    "a": {
      "a": {
        "s": 125
      },
      "b": {
        "s": 119
      }
    },
    "b": {
      "s": 123
    }
  },
  "b": {
    "s": 104
  }
}

两个根a和b 那么各个路径为:

  • 字符125的路径是 a -> a -> a,编码为 000。

  • 字符119的路径是 a -> a -> b,编码为 001。

  • 字符123的路径是 a -> b,编码为 01。

  • 字符104的路径是 b,编码为 1。

问了下AI如何将这个数和二进制数据对应起来 答案是生成和遍历一个Huffman树(死去的算法记忆开始攻击我) 找了半天找到了一个可以改了改可的解析脚本

 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
import json
# 从enc.txt中解析出Huffman树
def parse_tree(node, code='', code_table={}):
    if 's' in node:
        code_table[node['s']] = code
    else:
        if 'a' in node:
            parse_tree(node['a'], code + '0', code_table)
        if 'b' in node:
            parse_tree(node['b'], code + '1', code_table)
    return code_table

# 解码二进制数据
def decode_binary(binary_str, code_table):
    reverse_code_table = {v: k for k, v in code_table.items()}
    decoded_text = ''
    current_code = ''
    for bit in binary_str:
        current_code += bit
        if current_code in reverse_code_table:
            decoded_text += chr(reverse_code_table[current_code])
            current_code = ''
    return decoded_text

def main():
    # 读取enc.txt文件
    with open(r"E:\CTF\Hgame\re\COMPRESS_DOT_NEW\enc.txt", 'r') as file:
        content = file.read().split('\n') #分割成两组数据
        json_tree = json.loads(content[0])
        binary_str = content[1]

    # 构建编码表
    code_table = parse_tree(json_tree)

    # 解码二进制数据
    decoded_text = decode_binary(binary_str, code_table)

    # 输出解码结果
    print(decoded_text)

if __name__ == "__main__":
    main()

hgame{Nu-Shell-scr1pts-ar3-1nt3r3st1ng-t0-wr1te-&-use!}

turtle

解析

下载得到附件,

alt text

exeinfo看有upx魔改头
alt text

010editor看一下,魔改upx头了而且改了很多,所以学了一下用x64dbg手动脱壳

x64dbg打开,f9继续到程序开始处

alt text

这里可以看到许多寄存器进行入栈,那么在pop的时候会一并执行,而出栈顺序是反过来的,所以我们ctrl+f搜索pop rbp,找到真正进入点
alt text

f7单步进入,然后scylla手动脱壳后dump后
alt text

可以正常IDA打开了
alt text

分析以后 上面要注意一点v7和v8数组其实是用于连续存储的数据,改为v7[7]字节数组应该为:0xCD, 0x8F, 0x25, 0x3D, 0xE1, 0x51, 0x4A,对应v7[0]-v7[7],后面进行了RC4加密,密钥是yekyek

最后根据加密部分逆向写解密脚本即可

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
def decrypt_flag(ciphertext_flag, key):
    # KSA初始化S盒
    S = list(range(256))
    j = 0
    for i in range(256):
        j = (j + S[i] + key[i % len(key)]) % 256
        S[i], S[j] = S[j], S[i]
    
    # PRGA生成密钥流并解密(这里要反向使用加法)
    i = j = 0
    keystream = []
    for _ in range(len(ciphertext_flag)):
        i = (i + 1) % 256
        j = (j + S[i]) % 256
        S[i], S[j] = S[j], S[i]
        k = S[(S[i] + S[j]) % 256]
        keystream.append(k)
    
    # 加法解密Flag
    plaintext_flag = bytes([(c + k) % 256 for c, k in zip(ciphertext_flag, keystream)])
    return plaintext_flag


decrypted_key = b'ecg4ab6'
print(f"Decrypted Key: {decrypted_key}")

# 解密Flag
flag_cipher = bytes([
    0xF8, 0xD5, 0x62, 0xCF, 0x43, 0xBA, 0xC2, 0x23, 0x15, 0x4A,
    0x51, 0x10, 0x27, 0x10, 0xB1, 0xCF, 0xC4, 0x09, 0xFE, 0xE3,
    0x9F, 0x49, 0x87, 0xEA, 0x59, 0xC2, 0x07, 0x3B, 0xA9, 0x11,
    0xC1, 0xBC, 0xFD, 0x4B, 0x57, 0xC4, 0x7E, 0xD0, 0xAA, 0x0A
])
print(f"Flag Cipher: {flag_cipher}")
decrypted_flag = decrypt_flag(flag_cipher, decrypted_key)
print(f"Decrypted Flag: {decrypted_flag}")
#Decrypted Flag: b"hgame{Y0u'r3_re4l1y_g3t_0Ut_of_th3_upX!}"

hgame{Y0u’r3_re4l1y_g3t_0Ut_of_th3_upX!}

pwn

见博客中Hgame2025其他文章

本博客已稳定运行
发表了30篇文章 · 总计6万7千字

浙ICP备2024137952号 『网站统计』

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