国外的CTF骚套路是真的多……
Hacker's Playground 2021
Secure Enough
题目基于socket写了个通讯协议,连接端口7001
,可以从connectServer
看出来,也可以看流量
我先将这些函数名重命名,方便理解

随便开个socket服务端,然后运行
./my_client 192.168.0.108
连接成功后,进入task
函数
unsigned __int64 __fastcall task(unsigned int a1, __int64 a2)
{
task01(a1, a2);
if ( (unsigned int)task02(a1) == -1 )
{
puts("Receive Failed!");
exit(-1);
}
return generateAESkeyIV();
}
先看task01

对照流量,发现特征包头,看一下generate32Bytes
unsigned __int64 __fastcall generate32Bytes(_QWORD *a1)
{
unsigned int v1; // eax
__int64 v2; // rdx
__int64 v3; // rdx
int data; // [rsp+18h] [rbp-E8h] BYREF
int data_1; // [rsp+1Ch] [rbp-E4h] BYREF
char ctx[96]; // [rsp+20h] [rbp-E0h] BYREF
char ctx_1[96]; // [rsp+80h] [rbp-80h] BYREF
__int64 hash; // [rsp+E0h] [rbp-20h] BYREF
__int64 v10; // [rsp+E8h] [rbp-18h]
unsigned __int64 v11; // [rsp+F8h] [rbp-8h]
v11 = __readfsqword(0x28u);
v1 = time(0LL);
srand(v1); // 置随机数种子
data = rand();
MD5_Init(ctx);
MD5_Update(ctx, &data, 4LL);
MD5_Final(&hash, ctx);
v2 = v10;
*a1 = hash;
a1[1] = v2;
data_1 = rand();
MD5_Init(ctx_1);
MD5_Update(ctx_1, &data_1, 4LL);
MD5_Final(&hash, ctx_1);
v3 = v10;
a1[2] = hash;
a1[3] = v3; // md5(rand()) + md5(rand())
return __readfsqword(0x28u) ^ v11;
}
总的来说就是md5(rand()) + md5(rand())
再看一下RSAEncrypt
__int64 __fastcall rsaEncrypt(char *in, unsigned int flen, char *out)
{
__int64 result; // rax
__int64 rsaKey; // [rsp+28h] [rbp-8h]
rsaKey = importPublicKey();
if ( rsaKey )
result = (unsigned int)RSA_public_encrypt(flen, in, out, rsaKey, 1LL);
else
result = 0xFFFFFFFFLL; // -1
return result;
}
importPublicKey
导入了公钥

task01
的组包格式:01h + random32Bytes_1 + RSAEncrypt(random32Bytes_2)
回到task
,向下看task02
__int64 __fastcall task02(int a1)
{
__int64 result; // rax
char src[256]; // [rsp+10h] [rbp-210h] BYREF
char *s; // [rsp+110h] [rbp-110h] BYREF
unsigned __int64 v4; // [rsp+218h] [rbp-8h]
v4 = __readfsqword(0x28u);
memset(&s, 0, 259uLL);
if ( read(a1, &s, 259uLL) >= 0 ) // 从管道读取
{
if ( (_BYTE)s == 2 ) // 02 Header
{
rsaDecrypt((__int64)&s + 1, 256u, (__int64)src);
memcpy(serverSalt, src, sizeof(serverSalt));// salt
result = 0LL;
}
else
{
puts("Not a valid response packet");
result = 0xFFFFFFFFLL;
}
}
else
{
perror("Failed to receive");
result = 0xFFFFFFFFLL;
}
return result;
}
其中rsaDecrypt
解密的公钥与RSAEncrypt
相同
从服务端接收RSA加密数据后解密,将前32Bytes写到serverSalt
中
返回task
,看generateAESkeyIV
unsigned __int64 generateAESkeyIV()
{
generate32bitHash("A", 1, key);
generate32bitHash("BB", 2, &key[16]);
generate32bitHash("CCC", 3, iv);
return generate32bitHash("DDDD", 4, &iv[16]);
}
从我命名上就知道这是一个生成AES密钥和偏移的函数,调用的generate32bitHash
实质上是一个简单的md5
unsigned __int64 __fastcall generate32bitHash(char *data, int length, char *retData)
{
char md5CTX[104]; // [rsp+20h] [rbp-70h] BYREF
unsigned __int64 v6; // [rsp+88h] [rbp-8h]
v6 = __readfsqword(0x28u);
MD5_Init(md5CTX);
MD5_Update(md5CTX, data, length);
MD5_Update(md5CTX, random32bytes_1, 32LL); // tcp0: a3e6f484d7865ab1056e15833c748bedb689c0613fa1146a35f129797a770514
MD5_Update(md5CTX, random32bytes_2, 32LL); // i don't know
MD5_Update(md5CTX, serverSalt, 32LL); // tcp0: 0AE04F33E00939BA0E50C7CCC0A90D933374486D9AFFFD052BE8C8462D666120
MD5_Final(retData, md5CTX);
return __readfsqword(0x28u) ^ v6;
}
显而易见random32bytes_1
,random32bytes_2
,serverSalt
都是用来生成AES的Key和Iv的
康康task03
unsigned __int64 __fastcall task03(int a1)
{
char src[12]; // [rsp+18h] [rbp-48h] BYREF
__int64 encdata; // [rsp+24h] [rbp-3Ch] BYREF
int v4; // [rsp+2Ch] [rbp-34h]
__int64 buf[5]; // [rsp+30h] [rbp-30h] BYREF
unsigned __int64 v6; // [rsp+58h] [rbp-8h]
v6 = __readfsqword(0x28u);
strcpy(src, "Give Me Key");
encdata = 3LL; // 03 Header
v4 = 0;
memcpy((char *)&encdata + 1, src, 0xBuLL);
buf[0] = 0LL;
buf[1] = 0LL;
buf[2] = 0LL;
buf[3] = 0LL;
aesEncrypt((__int64)&encdata, 0xCu, (__int64)buf);
write(a1, buf, 0x20uLL);
return __readfsqword(0x28u) ^ v6;
}
程序构造了\x03Give Me Key
,AES加密后发送
而后task04
接受来自服务端的加密数据,解密后就是flag

所以需要得到AES的key和iv
要得到key和iv就要得到random32bytes_1
,random32bytes_2
,serverSalt
这三个参数
而从流量里我们可以取得random32bytes_1
通过动态的调试,我们可以将RSA加密后的serverSalt
解密出来
所以现在只需要考虑random32bytes_2
怎么获得
答案就在generate32Bytes
,要知道C语言中的rand
是生成伪随机数,我们需要调用srand
来设置随机数种子,
如果随机数种子不变,那么每次重新运行程序,都会产生相同的随机数
具体可以参考linux C语言获取随机数rand()和srand(time(NULL))
而time(0)
意味着获取当前系统的10位时间戳(秒级)
并且从流量中,我们可以提取出通信时的时间戳

我们就可以复现出那时程序产生的随机数
But但是,仔细一点,你就会发现,连续调用2次generate32Bytes
,间隔时间很短,而且每次都调用了srand

说了这么多就这么一句random32bytes_1 == random32bytes_2
写个脚本算出AES key和iv,或者直接动调
import hashlib
def generate32bitHash(s):
hash = hashlib.md5()
hash.update(s)
hash.update(s1)
hash.update(s1)
hash.update(s3)
return hash.digest()
s1 = b''.fromhex("424f9f4bac8c1e58acc28a4f6d096fe9") + b''.fromhex("bc787cb450bcb24868e91da958082345")
s2 = b''.fromhex("e41e80a2ae45883114514b3ec9656e11") + b''.fromhex("461038caea5164709012e865c87862e5") # 不需要
s3 = b''.fromhex("403CFC587755773E3B03C913B3805F86") + b''.fromhex("214802538C38977E647F0F37EAF2C468")
key = generate32bitHash(b"A") + generate32bitHash(b"BB")
iv = generate32bitHash(b"CCC") + generate32bitHash(b"DDDD")
print(key.hex(), iv.hex())
把那些00h
去掉之后

最后,来自flag的温馨提示(
当心 随机