记一道协议栈逆向题目
三星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生成随机数不变。
现在逆向的能力实在不敢恭维,吐槽一下自己。