尝试用发包的方式来让客户端进入新的逻辑,节省函数指针的间接引用所需要的大量静态分析时间。
于是,我修改补了一段畸形的http报文:

日志在开到最大的情况下,可以看到http_in_close函数,这是以前从未见过的,可以猜测得知这大概率是之前那几个结构体里的某个函数指针,在http_resp_body_recving进行了调用,进入了这段逻辑:

事实证明,httpdns成功接收了包。
但是光发包还不行,还得尝试让它有意义,而这里的jsmi_parse_start与一个名为cjson的库有关,这里恰好与我最初处理的json结构体联系起来,其恰好就是cjson的核心结构体jsmntok_t,其作用就是用扁平数组的方式来存储一个json字段。


大概逆向通读了一下http_in_close这个函数,发现它主要是在检查和接收几个键值对,于是尝试构造一段符合要求的json文本:

成功触发了http_in_close的校验逻辑,证明这种伪造是有效的,但是报错了enable disable,但是enable明明已经拼进了json字段中,猜测可能需要嵌套进去:

修改了enable的位置,这次没有字段缺失报错,显示了jsmi parse err,找到报错位置:

问题就出在jsmn_parse的返回值上,jsmn_parse同样是cjson的库函数,其作用是把第二个参数的字符串指针指向的json文本转化为tokens的形式(也就是之前所见的扁平数组)。
显然,jsmn_parse的返回值是一个负数错误码,进而引发了后续的问题,所以这里看一下具体是哪个状态码:

返回-2,表示的是“token数不足”,所以应该尝试减少json字段的嵌套深度,或者键值对数量,来强行把数据合法地塞进去。
socat实在是太好用了。
在目录里挂了一个名为gslb的文件后,成功返回了200状态码:

但实际上,它还是出现在了[E]日志里,仔细比对后,发现http.server返回的HTTP的版本号是1.0,而miio_client希望接收的协议版本号是1.1,所以直接把包丢弃了:

考虑到为此专门部署一个httpd或者用http.server模块实现一个简易的问答模型实在没必要,我干脆用socat转发stdout来模拟一次回包。
写一个简易脚本向stdout输出固定格式的http报文:

敲下socat -v TCP-LISTEN:53000,reuseaddr,fork EXEC:./res.sh,实现监听与转发,这样,脚本就会在tcp连接建立2秒后发出报文。
(这里反复尝试了好几次,ARP的欺骗效果不太稳定,不知道是不是缓存残留,miio_client多次试图连接原本的小米云。)

socat的-v能把双方的报文输出到stderr,这里成功完成了双向通信,同时wireshark抓包正常:

dbg顺便连上去,尝试抓取该程序在真正得到200状态码的正确报文后的逻辑:

然后就是机械的下断点,跟踪。
最后可以确定的是,程序完整地走完了整个http_disconnecting部分,进入了http_resp_body_recving,最后在对v195->field_30进行状态判断时卡住,进入循环,并反复报错:tls_shake failed,即TLS握手失败,但好在它有一个if-else,如果v196变量校验失败,就会尝试用http降级接收,后续可以考虑给v196设置一个有效值,暂时绕过TLS,理清大的状态:

nft指令快要忘光了,今天用之前又现复习了一小时。
发现我一直在给同一个结构体改名字,在http_resp_frame_helper和http_req_frame_helper之间游移不定,但其实应该还是前者更合理一些。

目前已知的是,当它开始第一次连接时,它会先走httpc_connecting,然后走httpc_req_head_sending和httpc_req_body_sending发一个get请求,随即进入httpc_resp_head_recving,并根据某字段进入了一个分支,在该分支下,如果收到的请求行是“HTTP/1.1 200 OK”,它就先用httpc_disconnecting把连接关掉,关起门来判断读取的报文和目前的缓冲区指针哪个大,然后根据结果去判断继续读还是File done。
但关键问题在于,它具体是如何解析的,是由某结构体里的函数指针所指向的函数进行处理的,正常走根本抓不到:

恢复结构体彻底陷入了瓶颈,所以我不得不尝试脱离纯粹的静态分析,抓一些除了400响应码以外的其他报文。
这时候之前搭建的局域网就派上用场了。
虽然它是硬编码了ip,但事实上,如果我通过ARP广播进行欺骗,伪装成一台网关,让它把报文发到这个网关,并且利用nft修改转发规则,就可以让明文的http流量经过攻击机,并到达除了小米云以外的服务器,这时我就可以用那台服务器起一个http服务,自己伪造响应报文观察它的反应。
使用一台kali虚拟机来完成这一过程:

首先启动bettercap中间人工具,通过probe和show证明两台虚拟机之间相互可达。
,随后设置为双工模式,开始发起ARP广播。


在小米设备的虚拟机上通过ip neigh查看ARP的缓存情况,发现192.168.122.1(网关)的Mac地址已经和192.168.122.251(kali)一样,证明ARP欺骗生效。
用nft建立一个表,分别在prerouting钩子挂一个dnat链,在postrouting钩子挂一个snat链,前者负责把受攻击设备的数据包的dst改成个人服务器的ip,以此保证路径被篡改;后者负责把数据包的src改成kali自己的ip,负责保证回来的报文可以正常根据NAT规则表返回。

成功劫持:

随后我在自己的服务器上开了一个python的http.server:


成功了,不过返回去的是404 Not Found,所以有必要放一个get请求的同名文件,看看能否返回200。
无符号的程序,还是尽量从日志下手。*
gdbserver的效果不太理想,哪怕是一台qemu-system虚拟机,也完全不支持硬件watch调试。
我又传了一个静态编译的strace进去,同样收效甚微。
最后还是靠着观察日志,终于找到了这个状态机的收发函数。下面是较为完善的状态机:
while循环
{
switch(handle_method)
{
case 0: 确定某个指针是否存在,存在就令handle_method为1,否则为7
case 1: httpc_connecting
case 2: httpc_req_head_sending
case 3: httpc_req_body_sending
case 4: httpc_resp_head_recving
case 5: httpc_resp_body_recving
case 6: httpc_disconnecting
}
}
对http_connecting进行处理:

观察到它主要根据field20这一int类型的变量来决定是否走tls_open,所以dbg在此下断点:

然后直接把这个x0寄存器改成1,观察它内部发生变化的几个变量:



结果发现,field18这个变量被赋值了一个指针,位于rodata区域,IDA里跳转过去,发现正好是之前研究过的那份证书。
但是tls_open内部是一个比现在这个http处理更为复杂的状态机,并且几乎没有任何日志字符串可供提示,逆向会花费更多的精力,不过还是可以依靠它恢复一些结构体。
定位了GET请求发包时的拼接点:




目标是定位到这段参数是在何时被存储进结构体的,由此,我就需要捕获这个结构体的内存被修改的时机。
在正常情况下,这可以通过dbg下硬件断点,通过watch监视来解决,不过qemu-user的gdb_stub并不支持这一功能,同时也出于需要组建局域网进一步验证的考量,我选择使用更为全能的qemu-system来解决问题,可以静态编译一个gdbserver实现监视。
手动配置网桥会比较麻烦,所以我使用了virt-manager工具来辅助组网。
该路由器基于Openwrt二次开发,先去官网下载一个适配qemu的通用内核:

通过dd和mkfs制作ext4文件,并通过mount -o loop,将该文件模拟成一个loop设备进行挂载,将解包的根目录复制进去:

下一步是配置虚拟机:


多余的外设统统删掉,只保留串口,控制台和网卡:

成功登入:

这里我在后面又补了一个串口便于操作,一开始是想通过ssh登录的,但openwrt实现ssh的组件是dropbear,同时这个路由器使用的组件比较老,在加密上有诸多版本匹配缺陷,最后还是选择了双串口。
串口是一个字符设备,用于进行常规的控制台通信,ttyAMA的一系列串口就主要服务于aarch64架构。
但事实上,如果仅仅只是通过手动添加硬件设备来增加串口是不够的,哪怕内核引导参数里同时写了console=ttyAMA0和console=ttyAMA1,但因为启动时只有一个控制台被启用,所以它拿到了/bin/sh,而另一个其实是空置的状态,所以需要人为地在启动脚本中mknod出新的字符设备,让它能通过open /dev/ttyAMA1这种方式来与接口通信,并通过sh 0</dev/ttyAMA1 /dev/ttyAMA1>1 2>&1来绑定交互式shell:

除此之外,我顺便在启动脚本里写了两条网络配置:
ip addr add 192.168.122.2/24 dev eth0 配置静态ip
ip route add default via 192.168.122.1 添加路由规则,让网络包顺利走网卡
在VMware,virt-manager这类虚拟机管理器中,一个NAT模式的虚拟局域网内的某个ip往往遵循这样的链路:
客户机自身 -> 虚拟交换机 -> 虚拟路由器(NAT)-> 宿主机网卡
其中,这个虚拟路由器其实算是一个网络接口,通过iptables/nft的的NAT转换规则模拟一台路由器,将数据包根据POSTROUTING规则执行地址转换,并发给物理网卡。

成功发包。
花了一些时间找gdbserver,用musl库的工具链编译,传进虚拟机:
