
Disclosure of Accfly camera vulnerabilities: CVE-2020-25782, CVE-2020-25783, CVE-2020-25784, CVE-2020-25785.
2020年初,在我之前的工作单位,我有机会参加了一次内部pwn2own风格的活动。有几个可用的目标,但我最感兴趣的是 Accfly 无线安全摄像头。不幸的是,我没能在实际活动中完成我的研究,但由于没有其他人尝试攻击这个设备,我继续进行了下去。
研究的主要焦点是可能导致远程代码执行(RCE)的漏洞。这种漏洞允许攻击者完全控制设备,对于视频摄像头来说,可能导致所有者隐私的完全泄露。不幸的是,该设备的固件被发现存在许多此类问题。
首先,该设备不提供任何身份验证。因此,能够连接到它的攻击者可以自由访问并重新配置它。最简单的形式是可以持续重启设备,使其对合法用户完全无法使用。这种攻击的范围略有局限,因为该设备设计用于WiFi网络,通常位于NAT之后,因此无法直接从互联网访问。然而,设备与其所有者的智能手机应用之间缺乏加密,加上使用供应商的服务器作为通信代理,为MitM或DNS操纵攻击创造了机会,这可以突破WiFi NAT的限制。
此外,该应用程序使用专有的二进制协议进行通信。它采用C和C++混合实现,并且被发现充满了不安全的字符串处理函数。主可执行文件包含大量未使用的代码,这表明它在其他设备上被重复使用。这使得维护更加困难,并增加了攻击面。该应用程序未启用任何现代安全机制来保护其免受许多常见利用技术的影响。此外,它甚至不限制用户权限,以最高可用权限(root)运行。
作为这项研究的结果,记录了以下四个漏洞。
CNetClientManage::ServerIP_Proto_Set 中存在未经验证的基于栈的缓冲区溢出CNetClientTalk::OprMsg 中存在未经验证的基于堆的缓冲区溢出CNetClientGuard::SubOprMsg 中存在未经验证的基于栈的缓冲区溢出CFtpProtocol::FtpLogin 中存在未经验证的基于栈的缓冲区溢出其中三个漏洞开发了RCE利用程序,允许攻击者完全控制设备。然而,由于供应商对漏洞报告尝试缺乏响应,此仓库仅包含有限的PoC利用程序,这些程序只会使应用程序崩溃。
问题在软件版本 V3.10.73 中被发现,并在软件版本 V4.15.77 中得到验证,这是本文发布时(2021年1月26日)可用的最新版本。
如有任何问题,请随时通过电子邮件(参见git提交)或通过Github问题与我联系。如果您认为某个IoT设备可能值得黑客攻击,正在寻找安全研究员,或者只是想打个招呼,我很乐意收到您的来信。您也可以**请我喝杯咖啡!**
目标设备是一个视频摄像头设备,通过配套的手机应用进行控制。我的分析从摄像头的网络流量开始,然后深入到摄像头固件。所有通信都使用自定义的二进制协议。命令要么在同一个网络中直接发送到移动设备,要么通过设备制造商的服务器传递。摄像头软件本身监听多个TCP端口(23456、34567)和UDP端口(34568、34569)。网络流量没有加密或身份验证,这允许进行MitM攻击或在摄像头暴露在网络中时直接访问。似乎在没有身份验证的情况下也可能访问视频流,但我没有对专有协议进行足够的逆向工程来尝试这一点。
在简要的通信概述之后,下一步是尝试获取设备固件的访问权限。我的第一次尝试是通过劫持设备更新过程直接下载固件,但在网络流量中没有发生类似的事情。如果不是一位同事从闪存中提取了固件提供了非常必要的帮助,我可能就会在这个阶段停滞不前,这使我能够继续这项研究。
固件被发现运行在MIPS小端CPU上的Linux上。只有一个有趣的进程,叫做 Alloca,它负责视频捕获并处理所有网络通信。应用程序是用C++创建的,并且包含大量在这个设备上未使用的代码。这表明同样的软件也在其他设备上使用。
尽管这个问题是最后被发现的,但它对大多数其他漏洞的实际利用至关重要,因为它们源于使用不安全的C语言字符串函数。虽然在类似场景中存在几种可用于成功执行代码的技术,但应用程序的创建方式使它们大多无用。主要问题是 Alloca 的代码和数据被静态分配在低地址(< 0x01000000)。因此,尝试重用现有代码(例如ROP等)是无用的,因为它们需要能够将地址写入程序的内存。由于C语言字符串使用 \x00 作为终止字符,并且字符串函数在第一个这样的字节处停止处理,因此不可能使用多个NULL字节。此外,栈位置是随机的,并且应用程序是多线程的,这使得其他技术更加不可靠。
这个漏洞是多个线程之间共享数据和不安全使用 strcpy 的结果。虽然我花了很长时间分析这个特定问题,但直到这篇出版物前几周,我才发现它作为数据泄露向量的可能性。有趣的是,由于泄露了C++对象的堆地址,这个漏洞也允许远程代码执行。然而,此攻击未包含在本报告中。
Alloca 应用程序可以通过FTP进行自我更新。此操作可以由服务器请求,该服务器还提供必要的用户名、密码和文件名。启动更新的函数如下所示:

三次对 strcpy 的调用显然不安全,并导致堆溢出,因为 ftpUpgrade 对象是动态分配的。不幸的是,复制执行的顺序和 ftpUpgrade 结构布局使得实际上无法启动一个会泄露数据的线程。仔细观察传入的数据包,揭示了以下结构:
struct ftp_upgrade_pkt {
struct pktHeader;
char username[16];
char password[16];
char filename[128];
}
而 ftpUpgrade 对象看起来像这样:
struct CNetClientFtpUpgrade {
// ... 这里有一些内容
char filename[128];
char unknown[6];
char username[16];
char password[16];
CFtpDownlad *;
CNetClientConnect*;
int something[5];
bool threadRunning;
// ... 以及更多
}
泄露可能发生在其中一个内部指针(CFtpDownload*、CNetClientConnect*)被应用程序填充之后。此外,用户名和密码(再次复制,但这次是安全的)被复制到新创建的对象中,然后其指针才被存储在可泄露的位置,因此泄漏只能通过 filename 发生。因此,文件名必须非常长,但由于 strcpy 的顺序和终止行为,足够长的文件名将导致更长的用户名和密码,从而实际上覆盖 threadRunning 并且根本不启动线程。
如果这段代码是单线程的,那就没什么可做的了。但是,由于新的 FtpDownload 线程被生成并运行 DownloadFile 函数,它提供了一个有趣的机会,因为它与处理传入数据包的线程共享 CNetClientFtpUpgrade 对象。它不仅有多个可以外部控制的IO操作(DNS请求、FTP连接处理),而且它还会尝试连接FTP最多10次(这在 DownloadFile 的调用者中完成)。这允许控制 FtpDownload 线程的执行(通过阻塞其IO操作),从而为消息处理线程处理其他请求留出时间。

简而言之,只需发送多个更新请求,就可以更改已经运行的 FtpDownload 线程使用的 filename(和其他参数),并接收泄露的堆地址。作为奖励,FtpSize 函数(以绿色标记)使用被泄露地址所引用的对象内部的缓冲区来存储 filename 本身,从而允许轻松注入第一阶段的shellcode。唯一的限制是长度和由于使用 strcpy 而导致的NULL字节缺失。提供了一个示例 PoC,它只是从设备中泄露一个堆地址。
函数 CNetClientManage::ServerIP_Proto_Set 中未经验证的基于栈的缓冲区溢出
传入流量处理完全缺乏身份验证,这促使我寻找数据包处理程序。其中一个有趣的函数是 ServerIP_Proto_Set。它似乎用于为DNS解析创建一个静态覆盖。我没有找到通过这种方式重定向流量的方法,但这里还有另一个缓冲区溢出(以橙色标记)。

直接从数据包读取的数据在 sprintf 函数内部使用。在这种情况下,假设数据包中的数据将适合16字节的缓冲区,但使用普通的 %s 格式允许写入任意多的字节,只要它们不包含NULL。
这个漏洞相当有限。虽然可以在栈上写入大量数据,因此使用NOP滑板可能有效,但无法写入NULL字节。即使尝试写入单个NULL字节也会失败,因为 sprintf 函数格式在其前面加上了一个 \n。另一个障碍是存储在缓冲区之后的 CMutex 对象。任何溢出尝试都必须用正确的值填充这个互斥量(或者至少是满足 CGuard 析构函数的值 - 以红色标记)。这很麻烦,因为析构函数会两次解引用传递的变量,然后在其值上调用 pthread_mutex_unlock。经过一些测试,我发现填充了NULL的缓冲区足以从 pthread_mutex_unlock 正确返回,但它仍然需要被解引用成一个合适的内存地址。
泄露 来救援了。这个攻击有点复杂,因为我们需要一个不包含任何NULL字节的堆地址。幸运的是,搜索更容易,因为设备允许我们执行远程未经验证的重启。每次重启都会提供不同的堆地址空间分配。所以可以简单地重置设备并泄露一个地址,直到找到一个合适的地址。方便的是,这也允许存储第一阶段的简短shellcode。由于我们需要克服互斥量问题(需要指向指针的指针),我们泄露另一个地址,这次将先前泄露的地址作为文件名传入。下图显示了预期的内存布局:

如果一切按计划进行,可以将第二个地址作为互斥量传递,第一个地址作为返回地址。然而,对于像 PoC 那样仅使应用程序崩溃来说,这并不是必需的。
函数 CNetClientTalk::OprMsg 中未经验证的基于堆的缓冲区溢出
该设备应该允许双向语音通信。另一个处理传入数据包的程序似乎负责接收和播放音频。audio_pkt_hdr 网络数据包由以下结构描述:
struct audio_pkt_hdr {
struct pktHeader field_0x0;
int field_0x14
int field_0x18
int field_0x1c
char audioBuff[0x140];
}
其中一个 pktHeader 结构字段是数据包的长度(通过网络传输)。发送者可以自由设置此字段。易受攻击的部分是使用传入数据包头中提供的不可信长度值,直接从传入数据包复制数据。

可以看到,CNetClientTalk 对象是通过以下构造函数创建的:

所以上面的 memcpy 调用导致了堆上的缓冲区溢出。不幸的是,实际利用这个问题相当困难。即使可以重复覆盖堆,我也没有找到一种方法来控制溢出缓冲区之后将存储什么数据。由于该应用程序有超过50个活动线程,其中一些负责处理音频和视频,它不断地分配和释放内存。这导致堆数据不断变化,从而难以预测缓冲区之后存储的内容并正确覆盖它。
函数 CNetClientGuard::SubOprMsg 中未经验证的基于栈的缓冲区溢出
这是另一个处理传入数据包的程序。这次,网络数据包具有以下结构(省略了公共数据包头):
struct pkt_hdr_sub_cliGuard {
dword deviceId;
dword userId;
dword magic;
dword subCmd;
dword field_0x10;
dword field_0x14;
dword field_0x18;
dword field_0x1c;
dword guard_icommand;
dword moreThenRandomStackValue;
dword itemCnt;
char array_of_0x18[24];
};
同样,有趣的部分是最后一个数组(因为我们可以根据需要增大这个数据包),它包含一个大小为24的内部结构。该漏洞源于一个假设,即接收到的 itemCnt 不会超过6,因为复制目标缓冲区大小为144(=24*6),这可以在下面的清单中看到(橙色高亮):

这一次复制是使用 memcpy(绿色高亮)执行的,因此对允许的字符没有限制。复制是通过一个while循环(以蓝色标记)以块为单位进行的。值得注意的是,计数器 cnt_v0 在循环内递减,因此块是以相反顺序复制的。包括易受攻击的缓冲区后面的变量 buf,溢出需要256字节,然后是4个寄存器($s0-$s3)和 $ra。由于我们不知道内存布局,PoC 代码使用了一种ROP技术。使用了一个单一的小工具,它会播放设备内置的某种声音(然后崩溃)。
函数 CFtpProtocol::FtpLogin 中未经验证的基于栈的缓冲区溢出
我分析的一个初始方向是寻找更新过程。正如我所发现的,该设备有一个FTP更新功能,可以通过发送更新请求来启动,结果是从外部FTP站点下载固件。与其他漏洞一样,在请求设备更新之前不需要进行身份验证。对ftp功能的深入分析揭示了 CFtpProtocol::FtpLogin 函数中存在基于栈的缓冲区溢出。从下面的反编译清单中可以看出,该函数将一个大小为256的 char 数组传递给函数 FtpPwd。

FtpPwd 用于从FTP服务器获取当前工作目录。它将其内部缓冲区加载最多1500字节的响应,然后将它们复制到提供的缓冲区。这个调用序列导致1242字节的溢出。在这种情况下,允许的字符非常有限,因为使用 "(双引号)会导致输入字符串缩短(使用 strchr 在C字符串中搜索 char)并且不会溢出缓冲区。幸运的是,只需要传递一个地址,代码执行将被重定向到该地址。

要利用此漏洞,需要控制DNS或重定向(或MitM)到FTP服务器的连接。应用程序没有任何现代保护措施,因此可以直接从栈执行代码。如果没有地址泄露,最好的办法要么是猜测栈位置,要么是将执行重定向到单个函数,然后使应用程序崩溃。我的第一次尝试就是这样,播放一个内置的声音,这在 PoC 中提供了。使用 泄露,可以完全控制设备。