记一道比较简单的协议栈逆向题目

记一道协议栈逆向题目

  三星ctf的一道协议相关的逆向题目,出题人自实现了一个简单的协议栈,给了可执行文件的同时,给了一个流量包。

  题目不是特别难,但是考察的点比较多,学到了一些东西,也比较麻烦。比赛的时候没有解出来,赛后根据队友的writeup进行复盘。总结一下自己失误的地方,一方面因为自己对于openssl库函数不是很熟悉,对AES加密算法不够了解,另一方面缺乏逆向协议栈的基础,在IDA反汇编结果不理想的时候,没有仔细理清栈桢的结构,导致了题目走了弯路,这里记录一下这道题目,日后在遇到协议栈分析的时候,希望自己能够有一些基本的思路和方法。

静态分析

  进入主函数,主函数如下,其中connect2host函数主要是建立套接字返回文件流,AESencrypt函数是对字符串进行AES加密,AESdecrypt函数是对字符串进行解密,这三个函数不是关注的重点,生成AES加密key和iv向量的功能都在getkey这个函数中实现了。

   getkey函数中,senddata函数是向服务器端发送一段数据,receive函数是接收服务器端的数据。

unsigned __int64 __fastcall senddata(int fd, __int64 a2)
{
  _QWORD to[31]; // [rsp+20h] [rbp-240h] BYREF
  __int64 v4; // [rsp+11Ah] [rbp-146h]
  char buf; // [rsp+130h] [rbp-130h] BYREF
  _BYTE v6[7]; // [rsp+131h] [rbp-12Fh] BYREF
  _QWORD v7[31]; // [rsp+151h] [rbp-10Fh] BYREF
  __int64 v8; // [rsp+24Bh] [rbp-15h]
  unsigned __int64 v9; // [rsp+258h] [rbp-8h]

  v9 = __readfsqword(0x28u);
  memset(to, 0, 0x100uLL);
  HIWORD(v4) = 0;
  generateMD5(randhash);                        
  generateMD5(from);                            
  memset(&buf, 0, 0x120uLL);
  *(_WORD *)((char *)&v8 + 5) = 0;
  HIBYTE(v8) = 0;
  buf = 1;
  memcpy(v6, randhash, 0x20uLL);
  if ( (unsigned int)rsa_encrypt((__int64)from, 0x20u, (__int64)to) == -1 )// rsa(randhash)
  {
    perror("Failed to encrypt");
  }
  else
  {
    v7[0] = to[0];                              // 数据包:标志位 + randhash + rsa(from)
    v8 = v4;
    qmemcpy(
      (char *)v7 + 7,
      (char *)to - ((char *)v7 - ((char *)v7 + 7)),
      8LL * ((((unsigned int)((char *)v7 - ((char *)v7 + 7)) + 258) & 0xFFFFFFF8) >> 3));
    write(fd, &buf, 0x123uLL);                  // 发送到7001端口的密文
  }
  return __readfsqword(0x28u) ^ v9;
}

  senddata函数中,栈地址中从buf开始的后面的数据都是发送给服务器端的数据,buf = 1定义了数据帧的标志位,randhash是generateMD5函数生成的md5哈希值,被赋值给v6指向的地址。from也是generateMD5函数生成的哈希值,这个哈希值进行了rsa加密,加在了数据帧的末尾。(这个反汇编的结果,确实有些反人类)

  整个数据包的结构就是:标志位(0x1)+ randhash(32 bytes长度) + rsa(from),通过流量包可以看到标志位。

   generateMD5函数值得仔细研究一下,这个函数中有玄机。

unsigned __int64 __fastcall generateMD5(_QWORD *init_buf)
{
  unsigned int seek; // eax
  __int64 v2; // rdx
  __int64 v3; // rdx
  int randnum1; // [rsp+18h] [rbp-E8h] BYREF
  int randnum2; // [rsp+1Ch] [rbp-E4h] BYREF
  char c[96]; // [rsp+20h] [rbp-E0h] BYREF
  char v8[96]; // [rsp+80h] [rbp-80h] BYREF
  __int64 v9; // [rsp+E0h] [rbp-20h] BYREF
  __int64 v10; // [rsp+E8h] [rbp-18h]
  unsigned __int64 v11; // [rsp+F8h] [rbp-8h]

  v11 = __readfsqword(0x28u);
  seek = time(0LL);                             // 种子固定
  srand(seek);
  randnum1 = rand();
  MD5_Init(c);                                  // 初始化MD5 Contex
  MD5_Update(c, &randnum1, 4LL);
  MD5_Final(&v9, c);                            // 输出md5值
  v2 = v10;
  *init_buf = v9;                               // 指针赋值,v9指向地址保存md5值
  init_buf[1] = v2;
  randnum2 = rand();
  MD5_Init(v8);
  MD5_Update(v8, &randnum2, 4LL);
  MD5_Final(&v9, v8);
  v3 = v10;
  init_buf[2] = v9;                             // md5(rand())+md5(rand())
  init_buf[3] = v3;
  return __readfsqword(0x28u) ^ v11;
}

  generateMD5函数中,用time(0)生成了一次种子,然后使用srand函数为rand函数提供种子,生成两次md5值,init_buf为两次md5值之和。在generateMD5函数中,第一次生成的MD5值和第二次生成的MD5值自然是不同的,但是generateMD5函数实际上被调用了两次,而且两次间隔时间非常短,这样一来,相当于time(0)固定,生成的种子不变,每次generateMD5函数调用srand函数随机播种了两次。

  那么rand函数生成的随机数会有什么变化呢?其实每次生成的随机数是相等的。

  实验如下:

   所以说数据帧的结构实际上就是:0x1 + randhash + rsa_encryt(randhash)。从流量分析中,我们可以得到randhash的值。

  receive函数主要是接受服务器端返回的数据,并且对数据进行rsa解密,并且将被解密的数据拷贝到bss段,这个salt需要根据题目给出的流量包,来动态调试得出。

 

   之前所有拷贝到bss段的数据的值,实际上都用来在generatehash函数中生成AES加密所需要的key和iv。

   randhash等于from之前已经提到,可以在流量中提取出来,salt可以根据动态调试找出来,然后叠加求md5值,就可以算出key和iv,这道题的flag也就在key里面了,题目中也提示了:

 

动态调试

  动态调试获取salt,wireshark提取服务器端返回的数据,写个socket脚本挂住:

import socket
from requests import *
import binascii

def server():
    host,port = "127.0.0.1",7001
    s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    s.bind((host,port))
    s.listen(2)
    server_data1 = "020f4b82b9d771a2625de1339269ead8599308a5119f3c8a3eb2e266f04210c2ac7e5657072ecd5fb777a99a8d57d94e39fa7001dd926ac42e4e9c944cd086868605d59db718caf0738f9983575119e4ae63f84c7a274eba7b39b9dc19a749a9bca7bead0aa75ea8f2c34a48dda8a4812e933249e945f66858785947d95168154b18e44f0ffa4f3c0a336ee2fc72f6b0aa1deeba5cd4646e68ae591923dc2894597862a753c3f86409cc19b8b5070de08fdab340618e6fb9370d95bf07670d76cdf320d5bd3bf10c26ec89f47956a4e6f850f751d7480c82cb25f7a48ba167d207d7a3836c7dee679a7ac1e004e0399598994e7542d63e65eb24b41158c66728720000"
    server_data2 = "029d1a9edb0d1ec28a3d941bee70e42af795bfec2bbe5a9ccc61a838c037addc6d0506512bd9295af10be912343dfc582bc44c1eff6e9989f3b8a005a92f4b67edc7fae41ada053779c91902801af473510e14401978c35458a599d5711ec411a224598163e4d08ac6dddbd10100064793da6bf2f03a14d33ebdc251d7cb3f149dc995abde49ca04339fd474a118489baedb300055d8a847dda102266dbdcb2cb497706fde541bbb4315f967d105f4a1cd54d6c92ada31aacd65c65e74654e23a7d7ac3174c2247d7f7796fee47e851558ce1d98470ce3ae83a42ff63bf8402d04cf0a48209677b950c401829a85063a1754d7dc25f0ef4cbe753e034081756d170000"
    server_data1 = binascii.a2b_hex(server_data1)
    server_data2 = binascii.a2b_hex(server_data2)
    print(server_data1)
    while True:
        c,addr = s.accept()
        #s.recv(1024)
        #c.send(payload)
        #data = c.recv(1024)
        while True:
            data = c.recv(1024)
            print(data)
            c.send(server_data1)

def main():
    server()
    # print(key)
    
if __name__ == "__main__":
    main()

  断点下载receive函数调用memcpy前,rsi寄存器保存的地址里的值就是salt。

 总结

  这道题主要记录一下分析的过程:1.学到一些openssl函数;2.学到IDA里面分析协议栈的一点技巧;3.伪随机数生成种子过程中,当seek固定的时候,重置srand生成随机数不变。

  现在逆向的能力实在不敢恭维,吐槽一下自己。

 

 

 

  

posted @ 2021-08-19 04:05  Riv4ille  阅读(237)  评论(0编辑  收藏  举报