网络设备代码审计与漏洞挖掘(下)

Published: 二 14 九月 2021

In Misc.

这一篇还没写完,以后再补,由于漏洞涉密所以应该不会有可以放出来的例子了,以后去网上找找吧...

二、漏洞挖掘

文档阅读

在正式分析前,如果有文档要先看文档,特别是内部的产品,一般都能要到仔细的文档,如产品使用手册,功能模块实现手册,各种流程图,开发手册等,通过这些文档就基本能知道这个设备是干什么的,有哪些服务,分别实现了哪些功能,每个功能怎么实现的,数据流逻辑是什么等等: image.png 而对于外部产品,一般就只能获取到使用手册,有时会有部分技术架构信息,一定要先熟悉这些信息,有了这些信息会避免很多逆向工作,如: image.png

服务分析

熟悉文档后,就可以分析服务了,一般就使用如下命令对服务有个大致了解:

ps aux  # 获取进程信息
pstree  # 获取进程关系
netstat -pantu | grep -i listen  # 监听的端口
iptables -nvL  # 防火墙规则
sockstat -l  # socket信息
lsmod   # 有哪些内核模块
lspci   # 主要查看网卡驱动有没有问题

有了初步了解后,就需要具体分析业务流程:

  1. 使用了什么技术栈:如使用什么语言,之前的设备perl/c/lua/php/cpp/shell比较多,现在也出现了nodejs/go/python了。
  2. 关键逻辑怎么实现:如VPN功能,使用apache扩展?nginx扩展?完全自己实现?用户态或内核态实现?
  3. 数据流是怎样的:一个请求会经哪些服务,路由怎样的?
  4. ...

这部分未完待续...

逆向分析

对于内部产品的审计,一般是不需要做逆向分析的,因为通常能拿到源码,所以只有在一些特殊的场景需要逆向,如确认编译选项,或某些位置在编译后更清晰等。而在外部产品中一般是避免不了逆向的,但是此时一定要牢记使命,我们是为了挖掘漏洞,因此不要陷入分析功能的巨量工作中,要秉持够用原则,只对关键的感兴趣的点进行分析,只需大致了解它的逻辑,而且用好三分逆向七分蒙不要过度逆向,下面介绍一些分析时的小技巧。

善用程序属性

确认一些程序属性,如静态链接还是动态链接,是否存在符号与调试信息等。动态链接的程序需要去各种so文件里交叉分析很麻烦,但是它的导出函数一般是保留的,因此可以识别函数名,可以编写脚本辅助识别各导入函数的定义位置(以前写了搞丢了,有时间再补上)。而静态链接就不必在多个文件中穿插着分析了,但是它可以把符号去的很彻底,因此可以研究下系统中是否存在它的动态链接版本,再由动态库的符号信息为静态库命名。 之后还可以先查找可用信息,全局查找哪些文件未去除符号,可使用如下脚本扫描:

find . -name '*' | xargs file |grep -i elf |grep no
善用调试信息

研究它的日志与调试机制,包括很多格式化串。很多程序会把很多日志代码编译进去,这些日志代码将有助于理解程序的功能,有的日志甚至包含函数名称等信息,可以帮助识别函数,一些格式化串也有同样的功能,识别出这些函数特征后,就可以编写一个脚本自动识别出一些函数名。对于那些可以以调试模式启动的程序,可以看看如何进入调试模式(添加启动参数,创建特定文件,发送特定信号等),调试模式可以输出很多详尽的信息,或者去掉一些有利于分析的限制。如下是对某设备进行分析时,识别出的几种调试符号,于是编写脚本可识别出很多函数名:

from idaapi import *
from ida_helper import *
import re

func_map = {
    '__ns_assert': 0,
    '_nsexception_mark': 3,
    '__assert': 0,
    'dprintf_pipe': 3,
}


def get_one(arr, start, end):
    for one in arr:
        if start <= one <= end:
            return one


plt_seg = get_segments_info()['.plt']

for func_name, arg_id in func_map.items():
    code_addr = name_to_addr(func_name)
    print('func_name:', func_name, code_addr)
    if not code_addr:
        continue
    func_ref_addr = get_one(get_xrefs(code_addr), plt_seg['start_ea'], plt_seg['end_ea'])
    func_ref_addr = func_ref_addr or code_addr
    all_used_addr = get_xrefs(func_ref_addr)
    print('func_name:', func_name, 'used_addr:', all_used_addr)
    for used_addr in all_used_addr:
        try:
            arg_val = get_call_arguments(used_addr)[arg_id]
            new_func_name = GetString(arg_val)
            if not re.match(r'^\w+$',new_func_name):
                print('invalid func name', new_func_name)
                continue
            func = get_func(used_addr)
            ori_func_name = get_ea_name(func.start_ea)
            if ori_func_name.startswith('sub_'):
                new_func_name = ori_func_name.replace('sub', new_func_name)
                print('ori name: ', ori_func_name, 'new name:', new_func_name)
                set_name(func.start_ea, new_func_name)
        except Exception as e:
            print(e)%

image.png 一般一个带函数名的日志函数在一个函数里被调用多次时,它的函数名字符串是相同的,都是表示当前函数的函数名,但是有时会遇到在同一个函数里调用多次,但函数名字符串不相同,可能会比较迷惑,这一般是一些函数被内联了,此时只能再深入分析看该函数应该选用哪个函数名,例如下图分析到sub_44F630第一个参数可能是当前函数的函数名,但是发现main中调用了好几次该函数,第一个参数却不一定是main,则很可能是其他函数被内联劲main函数了: image.png

分析结构体

逆向中有个很大工作是分析结构体,实际上大多数代码逻辑并不复杂,但是由于没有结构体详细定义,能看到的只有一坨无意义的变量与偏移操作,而它的域是通过结构体偏移去访问,没有定义为结构体时很多域是无法获取正确的交叉引用的,因此在评估有必要后,需要先把结构体定义分析出来:

  1. 对此第一步是分析其大小,如果是动态分配到可以直接从malloc等函数知道大小,否则需要去其他地方分析,例如很多结构体是数组,因此看它的循环操作偏移也可以确定其大小,还有一些会在初始时调用memset等功能初始化,也可以获取大小。
  2. 在识别出大小后,就需要确认它每个域的定义,首先确认各个域的大小,看对某偏移的读写操作可知大小,一般也可以知道它的作用,按作用命名即可,在条件不足以获取作用时,就先命名为xyz,之后有足够条件就可以命名了,而有些偏移还没有足够的信息(在当前函数没使用到),此时可以使用padx[0xYY]对其进行占位,之后遇到了再重定义即可。

抓包分析

在挖掘时一定要避免纯静态分析以及完整分析,要去找一些关键点入手,类似黑加白结合,此时流量信息就很重要了,对此若有日志可以先瞅瞅日志,之后一般需要把tcpdump扔目标上去抓包,之后使用wireshark分析,不过现在基本都上SSL/TLS了,所以数据都是密文,对此一般可以通过导入私钥去解密:

image-20211016105131188

但是现在很多都会使用DH做密钥协商,它的特点就是前向安全,此时私钥也解不开,对此可以看看它自身是否支持导出会话密钥,像Chrome可以通过设置SSLKEYLOGFILE环境变量来输出密钥,或者可以打勾子去DUMP密钥。不过到此时都是只能查看而不能修改,但在做测试时需要修改数据,因此更好的办法是用中间人,此处推荐使用mitmproxy,使用它可以很方便的对数据进行查改操作,而且Python写的也很好做定制,不过它有些小问题,需要多读读它的代码:

screenshot of mitmproxy's interface

此外还可使用mitm_replay,它的思路很有意思,它将请求和响应封装为http从而可对接burpsuite等工具实现代理拦截。

漏洞挖掘

一般还是先挖掘WEB部分的漏洞,专注于用户页面(管理页面一般不会暴露在公网),web的就那套路,正向与逆向,授权前与授权后。只是设备一般使用cgi,不再是常规的php/java语言了,这里要针对特定语言挖掘。另外是后端服务,一些设备web只是作为前端,最终调用后端服务完成任务,因此接下来还是关注后端服务,此处根据服务特性挖,什么缓冲区溢出,命令注入,还有超好用的SSRF等,这部分内容过多,会单独写几篇。

三、扫尾工作

挖完洞后你有什么想法?

  1. 对于内部的代码审计,需要编写完整的审计报告,漏洞点,危害,修复方案,之后还需要跟踪修复情况。
  2. 对于外部的漏洞挖掘,那....嘿嘿嘿🤤....要么就是报给厂商献给国家,要么就是内部留着在某大型行动中发光发热。
  3. 不管内外,其实这个挖掘过程的收获可以反哺产品线等,比如在挖洞的过程中也会分析对方用了什么保护手法,用了什么技术架构等,这些都是可以学习滴东西。

下面就描述下发光发热要做的步骤。

脱取业务数据

脱数据不是为了做坏事!!!该操作极其危险,必须在授权下进行,这些数据可以方便进一步渗透,如获取账户密码等信息,进行撞库,拓宽攻击面。挖掘到设备漏洞后,需要尝试对设备进行漏洞利用,获取设备中存储的关键数据,如用户敏感信息,各种网络配置,获取这些数据方便红队选手的后续安全渗透测试。这些数据可能以文本形式存放在文件中,或者被加密了,可能存放在各种关系型/键值型数据库中,可能以自定义格式存放(如共享内存dump)等,由于此时已经比较熟悉设备了,所以可以写脚本快速获取这些数据。

后门技术

后门不是为了做坏事!!!在某运动中,维权好多得分,设备是一个比较复杂的系统,因为我们已经比较熟悉它所以可以根据它的特性留下一些隐秘的后门:

  1. 有web服务的尝试留下webshell,最好是文件不落地的内存马
  2. 在任务计划或者服务中留反弹shell,最好通讯内容是加密的
  3. 如果端口限定太死,则尝试通过iptables将带有特定标志的请求转发到设备中监听本地的shell
  4. 有些设备把协议栈放用户态了,如使用dpdk等,因此可以向用户态协议栈程序处植入补丁,这样是比较难以发现的。(有点rootkit概念了,但是此处只操作用户态程序)
  5. 对它自带的完整性检查程序做修改,在不影响他功能时,避开我们修改过的文件,如md5sum等程序,让它在计算植入了后门的程序时直接返回原始md5值
  6. ...点点点,由于是一个系统,可以做的点太多了,总之就是充分利用它自身功能的特性!

痕迹清理

利用漏洞的时候一般会留下痕迹,重点检测一些地方: a.访问日志,清除对应时间对应请求记录; b.错误日志,有时候由于一些版本差异或参数错误可能会导致错误,需要找到对应错误日志予以精准清理或修改伪造; c.数据库日志,有时候存在一些数据的增删查改,会在数据库某些位置留下日志,需要找到变化的数据,精准清理或修改伪造; d.设备中程序自定义的日志,需要分析漏洞利用链中会调用哪些程序,可能会留下哪些日志,这些攻击日志需要精准清理或修改伪造。

业务实现原理

这类似逆向竞品分析,我们也不知道它这样做效果怎么样,我们只知道它是怎么做的,或许还会再想想为什么这么做,例如:

  1. 使用的什么系统,为啥会用centos,ubuntu,裸Linux,它的好处都有啥,谁说对啦就....划掉
  2. 怎么实现功能的,如SSL VPN它用的apache模块?nginx 模块?纯自己开发的框架?什么还有DPDK?好处是啥?
  3. 做了哪些保护?哪些实现起来简单?哪些极大阻碍了分析?这些主意很好,不过现在是我的了!

因为在挖掘过程中会做很多逆向分析,这些逆向的成果都可以输出来帮助改进产品。