前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >[系统安全] 五十三.DataCon竞赛 (2)2022年DataCon涉网分析之恶意样本IOC自动化提取详解

[系统安全] 五十三.DataCon竞赛 (2)2022年DataCon涉网分析之恶意样本IOC自动化提取详解

作者头像
Eastmount
发布2023-09-12 09:13:10
5330
发布2023-09-12 09:13:10
举报

前文详细介绍2020 Coremail钓鱼邮件识别及分析内容。这篇文章是作者2022年参加清华大学、奇安信举办的DataCon比赛,主要是关于涉网FZ分析,包括恶意样本IOC自动化提取和攻击者画像分析两类题目。这篇文章来自L师妹的Writeup,经同意后分享给大家,推荐大家多关注她的文章,也希望对您有所帮助。非常感谢举办方让我们学到了新知识,DataCon也是我比较喜欢和推荐的大数据安全比赛,我连续参加过四届,很幸运,我们团队近年来获得过第1、2、4、6、7、8名,不过也存在很多遗憾,希望更多童鞋都参加进来!感恩同行,不负青春,且看且珍惜!

  • 原文地址:https://www.wolai.com/iSjeGgwKtdGjeLzryhmn3S

作者的github资源:

  • 逆向分析:
    • https://github.com/eastmountyxz/ SystemSecurity-ReverseAnalysis
  • 网络安全:
    • https://github.com/eastmountyxz/ NetworkSecuritySelf-study

作者作为网络安全的小白,分享一些自学基础教程给大家,主要是关于安全工具和实践操作的在线笔记,希望您们喜欢。同时,更希望您能与我一起操作和进步,后续将深入学习网络安全和系统安全知识并分享相关实验。总之,希望该系列文章对博友有所帮助,写文不易,大神们不喜勿喷,谢谢!如果文章对您有帮助,将是我创作的最大动力,点赞、评论、私聊均可,一起加油喔!

声明:本人坚决反对利用教学方法进行犯罪的行为,一切犯罪行为必将受到严惩,绿色网络需要我们共同维护,更推荐大家了解它们背后的原理,更好地进行防护。(参考文献见后)

一.题目介绍

DataCon官网题目:

  • https://datacon.qianxin.com/opendata/openpage?resourcesId=34

目标:实现一个提取恶意样本IOC信息的自动化工具,即针对跨架构的Mirai僵尸网络,提取加密字符串、C2的host/port。

题目提供了967个Mirai二进制样本,其架构分布如下:

针对以上样本,具体要求如下:

  • 自动识别出Mirai家族样本,非Mirai家族样本不做提取
  • 单个Mirai样本的平均提取时间不超过20秒
  • 提取Mirai C2的域名/IP及对应端口信息
  • 提取Mirai加密字符串信息

二.Mirai-bot源码分析

Mirai家族是针对IOT设备的僵尸网络,运行在受感染设备的bot源码主要分为以下四个模块:

  • attack模块:解析C2下发的攻击命令、对Victim发起DoS攻击
  • scanner模块:扫描其它可能受感染的设备,将爆破口令上报给C2
  • kill模块:关闭特定端口并占用、删除特定文件并kill对应进程(排除异己)
  • public模块:实现了公共函数,供其它模块调用

因此,为提取加密字符串和C2的host/port,将重点分析字符串加解密函数(public模块table.c的相关函数)、host/port的设置函数(main.c中resolve_cnc_addr函数)

1.字符串加解密函数

为增大逆向分析的难度,Mirai对使用的字符串加密写入数组 table 中,其结构定义如下:

代码语言:javascript
复制
struct table_value table[TABLE_MAX_KEYS];
struct table_value {
    char *val;              //字符串指针
    uint16_t val_len;       //字符串长度
    BOOL locked;            //字符串是否加密
};

table.c中共有以下五个函数。首先,调用table_init初始化所有table中的字符串成员,针对每个字符串,调用add_entry添加到table中。在使用table[id]对应字符串前,调用table_unlock_val对table[id]解密。使用时,调用table_retrieve_val取出table[id]对应的字符串。使用完后,调用table_lock_val对table[id]重新加密。其中,加解密函数调用了toggle_obf对字符串进行异或操作。

代码语言:javascript
复制
void table_init(void);                           //初始化table中的成员
void table_unlock_val(uint8_t id);               //解密table中对应id的成员
void table_lock_val(uint8_t id);                 //加密table中对应id的成员
char *table_retrieve_val(int id, int *len);      //取出table中对应id的成员
static void add_entry(uint8_t id, char *buf, int buf_len);  //向table中添加成员
static void toggle_obf(uint8_t id);              //和密钥key异或,即加解密table中的字符串

其中,toggle_obf函数的实现如下,即对字符串的每个字节,使用密钥table_key的每个字节和它进行异或。由于是异或操作,加密函数table_lock_val和解密函数table_unlock_val完全相同。

代码语言:javascript
复制
static void toggle_obf(uint8_t id)
{
    int i;
    struct table_value *val = &table[id];
    uint8_t k1 = table_key & 0xff,
            k2 = (table_key >> 8) & 0xff,
            k3 = (table_key >> 16) & 0xff,
            k4 = (table_key >> 24) & 0xff;

    for (i = 0; i < val->val_len; i++)
    {
        val->val[i] ^= k1;
        val->val[i] ^= k2;
        val->val[i] ^= k3;
        val->val[i] ^= k4;
    }

#ifdef DEBUG
    val->locked = !val->locked;
#endif
}

对应PPT介绍如下:

2.C2 host/port的设置函数

为防止动态调试,bot设置了SIGTRAP信号处理函数anti_gdb_entry,并raise(SIGTRAP)主动触发该信号。

  • 如果程序正在被调试,该信号将被发送给调试进程,处理函数无法执行。
  • 如果没被调试,bot才会收到该信号,执行anti_gdb_entry函数,该函数的功能很简短,即把实际C2 host/port的设置函数resolve_cnc_addr赋值给函数指针resolve_func。
代码语言:javascript
复制
static void anti_gdb_entry(int sig)
{
    resolve_func = resolve_cnc_addr;
}

在与C2通信时,调用resolve_cnc_addr函数设置srv_addr变量。首先从table中获取域名,调用resolv_lookup向DNS服务器查询其可能的IP,设置sin_addr整型IP(4字节)。再从table中获取端口,设置sin_port(2字节)

代码语言:javascript
复制
struct sockaddr_in srv_addr;
struct resolv_entries {
    uint8_t addrs_len;
    ipv4_t *addrs;
};
static void resolve_cnc_addr(void)
{
    struct resolv_entries *entries;

    table_unlock_val(TABLE_CNC_DOMAIN);
    entries = resolv_lookup(table_retrieve_val(TABLE_CNC_DOMAIN, NULL));
    table_lock_val(TABLE_CNC_DOMAIN);
    if (entries == NULL)
    {
#ifdef DEBUG
        printf("[main] Failed to resolve CNC address\n");
#endif
        return;
    }
    srv_addr.sin_addr.s_addr = entries->addrs[rand_next() % entries->addrs_len];
    resolv_entries_free(entries);

    table_unlock_val(TABLE_CNC_PORT);
    srv_addr.sin_port = *((port_t *)table_retrieve_val(TABLE_CNC_PORT, NULL));
    table_lock_val(TABLE_CNC_PORT);

#ifdef DEBUG
    printf("[main] Resolved domain\n");
#endif
}

对应PPT介绍如下:

三.二进制样本分析

1.签到题3(附件2)

1.1 提取解密字符串

观察到,.rodata节中有一系列加密字符串。

查看这些加密字符串的引用,可以找到table_init函数。该函数很长,不断地分配内存和add_entry,且只有一个基本块。通过分析其它样本,这里需要注意的是:

  • add_entry时,.bss段中数组table的顺序和.rodata中字符串列表rodataTable中的顺序并不一致
  • 通过对其它样本分析,加密字符串的长度并不以\0作为结束,而是由这里设置的table.val_len决定
  • table的id是以1开始的

查看table的引用,发现有4个函数使用了该变量。有两个函数完全相同,即table_lock_valtable_unlock_val,另外一个则是table_retrieve_val

查看table_lock_val,它访问了.data节中的key

还原加解密算法,于是可以对.rodata中的字符串解密,得到字符串明文configs如下:

对应PPT介绍如下:

1.2 提取C2 host/port

从源码分析中可知,anti_gdb_entry函数很短,就一个赋值语句,可按长度和操作码对其筛选如下:

于是可以找到resolve_cnc_addr如下。该函数设置了0x22C7C处的srv_addr的变量。

srv_addr变量的类型是0x10字节的struct sockaddr_in,定义如下:

代码语言:javascript
复制
struct sockaddr_in {
  short int sin_family; 
  unsigned short int sin_port;     //2字节
  struct in_addr sin_addr;         //整型IP,4字节
  unsigned char sin_zero[8]; 
}; 

srv_addr+0x2为port,srv_addr+0x4为整型ip。这里的ip使用的是局部变量0xAB92572F,port是table[1]中的内容b’\x05\x16’。由于网络字节序是大端字节序,因此host为47.xx.xx.171,port为1302。

2.签到题4(附件3、4)

附件3可以使用upx.exe成功脱壳,附件4的壳无法识别。

UPX壳包含以下两个关键的结构体:

  • l_version,l_format
  • p_filesize,p_blocksize
代码语言:javascript
复制
struct l_info // 12-byte trailer in header for loader
{ 
    uint32_t l_checksum; // checksum
    uint32_t l_magic;    // UPX! magic [55 50 58 21]
    uint16_t l_lsize;    // loader size
    uint8_t l_version;   // version info
    uint8_t l_format;    // UPX format
}; 
struct p_info // 12-byte packed program header follows stub loader
{ 
    uint32_t p_progid;    // program header id [00 00 00 00]
    uint32_t p_filesize;  // filesize [same as blocksize]
    uint32_t p_blocksize; // blocksize [same as filesize]
};

upx.exe在识别壳、脱壳时,需满足以下3个规则:

  • 在1)头部l_info.l_versionl_info.l_format3)尾部-0x20的2字节:版本和格式信息均相等
  • 在1)头部l_info.l_magic2)尾部-0x243)尾部-0x2C的4字节:均有l_magic(UPX!)
  • 在1)头部p_info.p_filesize2)头部p_info.p_blocksize3)尾部-0xC的4字节:均相等

(尾部上面一个magic的偏移不一定是-0x2C,可能是-0x29-0x2F

附件4如下:

发现尾部-0x20的2字节和附件3一样,应该是UPX版本和格式相同,然后搜索"0D 17",发现开头也有,于是可以还原UPX头部和尾部的格式。

根据上述提到的3个规则,将0填充对应值,还原后如下:

于是可以使用upx.exe脱壳

四.自动化思路

自动化包括脱壳、提取解密字符串、提取C2三个阶段。

1.脱壳

(1)识别UPX壳,满足

  • 函数个数小于4(IDAPython)
  • 字节序列中包含version_format(0D 17或0D 0C)

(2)修复UPX壳

  • 根据第一个version_format定位UPX头部,最后一个定位UPX尾部
  • 根据尾部的version_format,去掉尾部多余字符
  • 填充UPX! 55 50 58 21
  • 根据尾部,填充头部的p_info.p_filesizep_info.p_blocksize

(3)使用 upx.exe脱壳

基本思路如下图所示:

2.提取解密字符串

(1)找初始化函数 init_table

特征:(从小到大遍历所有函数,找到满足以下两点的最长函数)

  • 只含一个基本块
  • 内存访问只有.rodata和.bss
  • 代码很长

部分样本的init_table函数内嵌在了main函数里,也可将找初始化函数init_table转换为找相同初始化功能的基本块。

(2)记录密文字符串 rodataTable_addrs

分析init_table函数:

  • 记录.rodata段中的密文字符串的所有地址rodataTable_addrs
  • 记录.bss段table中的所有元素地址table_addrs,从而确定table的起始地址
  • 记录密文字符串长度strlens(malloc函数的参数)

部分样本的init_table函数中调用了add_entry函数,在init_table中只有table元素的偏移,得在add_entry中找table的起始地址。

(3)找解密函数 table_unlock_val 查看table的函数引用,两个完全相同的函数为加解密函数。同时,确定table_retrieve_val函数。

a) 部分样本中,table的引用还有两个函数,则还有toggle_obf函数,且table_retrieve_valtoggle_obf存在调用关系,可以确定调用函数是table_retrieve_val,被调函数是toggle_obf。 b) 部分样本没有其它引用table的函数,则table以立即数写入,需逐函数查找。

(4)记录解密密钥 key 分析table_unlock_val函数,记录.data段中的解密密钥key

(5)解密得到configs 对rodataTable_addrs的密文字符串,使用密钥key进行异或解密,得到明文字符串。至于configs中的地址为table_addrs

以上重要参数基本上以立即数出现、指令相对统一和简单,并且没有初步的分析不便于设置模拟执行的相关地址和参数,因此在该步骤中仅使用IDAPython分析指令,得到相关函数和变量的地址。

基本思路如下图所示:


3.提取C2 host/port

总体思路是,通过anti_gdb_entry函数,找到resolve_cnc_addr函数,该函数中包含C2 host/port的srv_addr结构。模拟执行init_tableresolve_cnc_addr函数,读取srv_addr中的ip、port。因此,可分为查找相关函数和变量、模拟执行获取host/port两步。

3.1 查找相关函数和变量

第一步,找信号处理函数 anti_gdb_entry、C2地址设置函数 resolve_cnc_addr

anti_gdb_entry特征:(从小到大遍历所有函数,找到满足以下两点的函数)

  • 只包含两个变量(一个是.data节的resolve_func函数指针、一个是.text节的resolve_cnc_addr函数地址)
  • 函数大小固定(同一架构下,该赋值操作的指令模式相同,例如ARM里为"LDR", “LDR”, “STR”, “BX”)

a) 部分样本没有找到anti_gdb_entry函数,但是通过分析样本发现,一些resolve_cnc_addr在设置host/port时,会调用table_retrieve_val(1)。因此,可进一步查看table_retrieve_val的引用,找到参数为1的函数即为resolve_cnc_addr。 b) 部分样本的resolve_cnc_addr函数内嵌在了main函数里,也可将找初始化函数resolve_cnc_addr转换为找类似功能的基本块。

第二步,找地址变量 srv_addr。 分析resolve_cnc_addr函数,记录.bss中的所有变量,包括srv_addrsrv_addr+0x2处的port、srv_addr+0x4处的ip,取最小值即为srv_addr

部分样本还包含srv_addr-0x10的地址,但没有对其0x10范围内的其它引用,可以过滤掉。

3.2 模拟执行获取host/port

下面代码中,以ARM架构为例。

第一步,设置内存布局

  • (1) 将样本的各个节写入内存中,基址为imagebase。
代码语言:javascript
复制
ADDRESS = idaapi.get_imagebase()
mu = Uc(UC_ARCH_ARM, UC_MODE_LITTLE_ENDIAN)
mu.mem_map(ADDRESS, 2 * 1024 * 1024)
for segm_ea in idautils.Segments():
    segm = idaapi.getseg(segm_ea)
    data = idc.get_bytes(segm.start_ea, segm.size())
    mu.mem_write(segm.start_ea, data)
  • (2) 设置栈、栈顶寄存器
代码语言:javascript
复制
mu.mem_map(STACK_ADDR, STACK_SIZE)
mu.reg_write(UC_ARM_REG_SP, STACK_ADDR + (STACK_SIZE//2))
  • (3) 设置堆 这里简化了堆的结构,仅仅在HEAP_ADDR保存了所有的UserData,并用unicorn_heap记录所有已分配UserData的起始地址,最后一项为最后一个UserData的结束地址。
代码语言:javascript
复制
mu.mem_map(HEAP_ADDR, HEAP_SIZE)
unicorn_heap = [HEAP_ADDR]

第二步,设置hook函数 针对以下三类函数和指令,需设置相应的hook。

  • (1) malloc函数 针对静态链接,由于库函数代码基本不变且代码较长,可根据函数大小进行筛选,得到hook的地址。针对动态链接,由于只在模拟执行init_table时会遇到,可在init_table调用.plt中的地址时进行hook。 该函数是分配内存,hook以后,利用unicorn_heap返回UserData地址即可。
代码语言:javascript
复制
 ret = mu.reg_read(UC_ARM_REG_R14)
  mu.reg_write(UC_ARM_REG_PC, ret)
  arg0 = mu.reg_read(UC_ARM_REG_R0)   #malloc请求UserData的大小
  addr = unicorn_heap[-1]             #在unicorn_heap中分配内存
  unicorn_heap.append(unicorn_heap[-1] + arg0)  #更新最后一个块的结束地址
  mu.reg_write(UC_ARM_REG_R0, addr)   #返回分配的内存地址
  • (2) resolve_lookup函数 由于是Mirai的公共函数,大小是固定的几个且代码较长,可根据函数大小进行筛选,得到hook的地址。 该函数是将域名转为IP,hook以后,根据函数的参数,得到域名字符串的地址。如果该地址在.rodata中,直接提取即可。如果该地址在unicorn_heap堆中,可根据UserData的分配顺序,得到域名在configs中的索引ip_domain_index,从而提取host的域名。同时,为避免后面的代码在使用struct resolv_entries *entries;时发生内存访问错误,需简单伪造一下结构体内容。
代码语言:javascript
复制
ret = mu.reg_read(UC_ARM_REG_R14)
mu.reg_write(UC_ARM_REG_PC, ret)

arg0 = mu.reg_read(UC_ARM_REG_R0)
if arg0 in unicorn_heap:
    ip_domain_index = unicorn_heap.index(arg0)
    if len(configs) > ip_domain_index:
        ip_domain = configs[ip_domain_index][1]
        ip_domain = ''.join([chr(i) for i in ip_domain])
elif idc.get_segm_name(arg0) == '.rodata':
    ip_domain = read_str_until_zero(arg0)

entries_ptr = unicorn_heap[-1]   #指向entries
unicorn_heap.append(unicorn_heap[-1] + 0x8)
addrs_ptr = unicorn_heap[-1]     #指向entries.addrs
unicorn_heap.append(unicorn_heap[-1] + 0x4)
entries = p32(1) + p32(addrs_ptr)
addrs = p32(0x01020304)
mu.mem_write(entries_ptr, entries + addrs)
mu.reg_write(UC_ARM_REG_R0, entries_ptr)
  • (3) 其它发生错误但不为table相关功能的函数和指令(例如"__udivsi3"和"__umodsi3"指令)。

第三步,模拟执行init_table 起始地址为init_table.start_ea,结束地址为init_table.end_ea的函数返回指令。

第四步,模拟执行 resolve_cnc_addr 起始地址为resolve_cnc_addr.start_ea,结束地址为最后一次修改srv_addr的下一条指令。

第五步,读取ip/port 如果没有调用resolve_lookup函数,则host以ip的形式出现,读取srv_addr+4的内存即为ip。port为srv_addr+2的内存内容。

基本思路如下图所示:

五.小结与展望

通过人工分析json对应样本和签到题可以发现,提取configs的关键函数是init_table,提取host/port的关键函数是resolve_cnc_addr。通过模拟执行这两个函数,将字符串的初始化、加解密、提取都交给unicorn,然后读取srv_addr处的内容,能有效提升自动化的准确率,降低IDAPython静态分析的复杂度。

最后,感谢老师和实验室所有小伙伴,感谢DotaCon团队,感谢师妹师弟,感谢DataCon为我们提供这么好的比赛。基础性文章,希望对您有所帮助,感恩遇见,不负青春!

欢迎大家讨论,是否觉得这系列文章帮助到您!任何建议都可以评论告知读者,共勉。

  • 逆向分析:
    • https://github.com/eastmountyxz/ SystemSecurity-ReverseAnalysis
  • 网络安全:
    • https://github.com/eastmountyxz/ NetworkSecuritySelf-study

参考链接:

  • Mirai 源码分析 (seebug.org)
  • linux程序安全之反调试反跟踪 - 知乎 (zhihu.com)
  • https://cujo.com/upx-anti-unpacking-techniques-in-iot-malware/

前文回顾(下面的超链接可以点击喔):

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2023-09-06 00:42,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 娜璋AI安全之家 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 二.Mirai-bot源码分析
    • 1.字符串加解密函数
      • 2.C2 host/port的设置函数
      • 三.二进制样本分析
        • 1.签到题3(附件2)
          • 1.1 提取解密字符串
          • 1.2 提取C2 host/port
      • 四.自动化思路
        • 1.脱壳
          • 3.提取C2 host/port
            • 3.1 查找相关函数和变量
            • 3.2 模拟执行获取host/port
        • 五.小结与展望
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档