国外的CTF骚套路是真的多……

Hacker's Playground 2021

Secure Enough

题目基于socket写了个通讯协议,连接端口7001,可以从connectServer看出来,也可以看流量

我先将这些函数名重命名,方便理解

image-20210817095922766

随便开个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

image-20210817100509302

对照流量,发现特征包头,看一下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导入了公钥

image-20210817100948157

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_1random32bytes_2serverSalt都是用来生成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

image-20210817102113985

所以需要得到AES的key和iv

要得到key和iv就要得到random32bytes_1random32bytes_2serverSalt这三个参数

而从流量里我们可以取得random32bytes_1

通过动态的调试,我们可以将RSA加密后的serverSalt解密出来

所以现在只需要考虑random32bytes_2怎么获得

答案就在generate32Bytes,要知道C语言中的rand是生成伪随机数,我们需要调用srand来设置随机数种子,

如果随机数种子不变,那么每次重新运行程序,都会产生相同的随机数

具体可以参考linux C语言获取随机数rand()和srand(time(NULL))

time(0)意味着获取当前系统的10位时间戳(秒级)

并且从流量中,我们可以提取出通信时的时间戳

image-20210817103008467

我们就可以复现出那时程序产生的随机数

But但是,仔细一点,你就会发现,连续调用2次generate32Bytes,间隔时间很短,而且每次都调用了srand

image-20210817103720633

说了这么多就这么一句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去掉之后

image-20210817104026951

最后,来自flag的温馨提示(

当心 随机