实战|记一次攻防样本——shellcode分析(上)

举报
亿人安全 发表于 2024/10/31 23:18:38 2024/10/31
【摘要】 原文首发在:奇安信攻防社区https://forum.butian.net/share/3817书接上文,笔者发的一篇对某红队钓鱼样本分析的文章:《记一次(反虚拟+反监测+域名前置)钓鱼样本分析及思考》 本文主要针对上文中样本使用的shellcode展开分析,非常详细的记录了笔者分析该shellcode过程;以及对其使用的相关技术进行分析拆解;0x01 背景书接上文,笔者发的一篇对某红队钓鱼...

原文首发在:奇安信攻防社区

https://forum.butian.net/share/3817

书接上文,笔者发的一篇对某红队钓鱼样本分析的文章:《记一次(反虚拟+反监测+域名前置)钓鱼样本分析及思考》 本文主要针对上文中样本使用的shellcode展开分析,非常详细的记录了笔者分析该shellcode过程;以及对其使用的相关技术进行分析拆解;

0x01 背景

书接上文,笔者发的一篇对某红队钓鱼样本分析的文章:记一次(反虚拟+反监测+域名前置)钓鱼样本分析及思考;

本文主要针对上文中样本使用的shellcode展开分析,非常详细的记录了笔者分析该shellcode过程;以及对其使用的相关技术进行分析拆解;

0x02 分析

对于shellcode我们首先可以通过一些模拟器来查看其内部的大致函数调用情况,然后有针对性的开展分析;

一、自动化模拟分析探虚实

笔者一般使用speakeasy这款工具,https://github.com/mandiant/speakeasy

模拟运行的结果如下图:

图片

如上,可以看到这个样本前面做的一些动作,拿到几个关键函数的调用地址,其中比较明显特征的有:CreatFileMappingA、MapViewOfFile,后面也调用了这两个函数,结合我们dump下来的shellcode文件大小(3百多kb),不难看出里面应该是藏了一个pe文件,这里的逻辑是把藏于其中的pe文件从文件格式映射成内存格式,并且还调用了VirtualProtect来修改内存权限属性,应该是要修改相关数据;

在大致知道了这个情况之后,我们可以做一些尝试,比如直接去dump下来的shellcode文件里面找,是否存在相关pe文件:

二、妙计上心头直捣黄龙

我们在dump下来的shellcode文件里面查可执行文件:

图片

上图是我们查看shellcode中pe文件dos头的情况,可以发现有一个4d5a开头的地方,但是0x3c偏移,以及后面的pe头都没有,所以大概率不是;也有可能是:是,但是其他数据被加密了,需要动态解密还原出来,然后才去拉伸;所以这里我们尝试走捷径失败;

三、老老实实正常分析

那没办法就直接怼dump的文件把:

如下,可以看到,上来第一部分就是调用sub_188e6(这个地址是内置的一个相对地址+获取的运行时绝对地址拿到的和call $+5,pop 操作类似),然后下面的第二部分就是传入几个参数,调用第一部分返回的rax函数,r8d传入的像是特征码;(和CobaltStrike有点像,但是前面头不符合,并且没有出现pe文件头的特征)

图片

我们跟进sub_188e6,直接ida f5看逻辑(一般来说分析shellcode的时候是没有比较逐字节扣的,能f5直接f5即可,但是有些做了编码壳的shellcode还是需要先简单分析壳逻辑,动态调试脱壳后再f5即可;例如:之前笔者分析的一个带编码壳的shellcode 中的shellcode)

如下图,上来第一部分对一个v13数组变量进行构造赋值操作,然后第二部分调用sub_18c66对v9变量进行赋值:

图片

找PE

跟进sub_18c66,其实现如下:

图片

简单转化下出现数据的编码:

图片

上图,我们可以直观的看出,做了一个递减的循环,寻找当i对应地址的WORD为YA的时候,并且其0x3c偏移处的值在0x40-0x400之间,并且i+(0x3c偏移处地址的值)的地址对应值的WORD为0x4a51(JQ)的时候,i的值;

i的起始取值来自sub_18b66,如下,该函数就是返回函数的返回值;

图片

所以我们简单总结就知道了v9的赋值函数sub_18c66,其实就是从函数的返回地址开始,往前找,找到一个符合上述分析条件的地址;并且我们稍加留意可以看到条件当中出现了0x3c这个敏感偏移;这不就是回溯找PE文件位置吗,只不过这里攻击者做了特征隐藏,DOS头的MZ到这里变成了AY,PE头的PE到这里变成了QJ;(难怪刚刚我们上面查pe文件的时候没找到)

按照这个逻辑我们再次查看shellcode的二进制文件,如下图可以看到就是在刚开始的地方;(结合上面我们直接分析的开头代码,这里有点像反射dll加载,但是又不全是,因为做了一些改良,往前面头部加了一些lj代码)

图片

然后我们回到sub_188E6的主逻辑上;

如下:先是对v13数组前两个元素做一个条件判断(这个条件肯定是成立的,上面的赋值就是直接这样赋值的,取低32位,比较也成立,所以这里就是一个恒真式),接着调用sub_18cf6传入v13变量地址;

图片

peb找函数地址

跟入sub_18cf6函数:

其实现如下:

\_\_int64 \_\_fastcall sub\_18CF6(\_QWORD \*a1)  
{  
  int v1; // eax  
  \_\_int64 result; // rax  
  \_\_int16 v3; // \[rsp+0h\] \[rbp-68h\]  
  unsigned \_\_int16 v4; // \[rsp+0h\] \[rbp-68h\]  
  \_\_int64 v5; // \[rsp+8h\] \[rbp-60h\]  
  int v6; // \[rsp+10h\] \[rbp-58h\]  
  unsigned int \*v7; // \[rsp+18h\] \[rbp-50h\]  
  int v8; // \[rsp+20h\] \[rbp-48h\]  
  int v9; // \[rsp+20h\] \[rbp-48h\]  
  \_\_int64 \*i; // \[rsp+28h\] \[rbp-40h\]  
  unsigned int \*v11; // \[rsp+30h\] \[rbp-38h\]  
  unsigned int \*v12; // \[rsp+38h\] \[rbp-30h\]  
  unsigned \_\_int8 \*xx\_address; // \[rsp+40h\] \[rbp-28h\]  
  \_BYTE \*v14; // \[rsp+48h\] \[rbp-20h\]  
  unsigned \_\_int16 \*v15; // \[rsp+50h\] \[rbp-18h\]  
  
  for ( i \= \*(\_\_int64 \*\*)(\*(\_QWORD \*)(\_\_readgsqword(0x60u) + 0x18) + 0x20i64); i; i \= (\_\_int64 \*)\*i )  
  {  
    xx\_address \= (unsigned \_\_int8 \*)i\[10\];  
    v3 \= \*((\_WORD \*)i + 0x24);  
    v8 \= 0;  
    do  
    {  
      v9 \= \_\_ROR4\_\_(v8, 13);  
      if ( \*xx\_address < 97u )  
        v1 \= \*xx\_address;  
      else  
        v1 \= \*xx\_address \- 0x20;  
      v8 \= v1 + v9;  
      ++xx\_address;  
      \--v3;  
    }  
    while ( v3 );  
    if ( v8 \== 0x6A4ABC5B )  
      break;  
  }  
  v5 \= i\[4\];  
  v11 \= (unsigned int \*)(\*(unsigned int \*)(\*(int \*)(v5 + 0x3C) + v5 + 0x88) + v5);  
  v12 \= (unsigned int \*)(v11\[8\] + v5);  
  v15 \= (unsigned \_\_int16 \*)(v11\[9\] + v5);  
  v4 \= 6;  
  while ( 1 )  
  {  
    result \= v4;  
    if ( !v4 )  
      break;  
    v14 \= (\_BYTE \*)(\*v12 + v5);  
    v6 \= 0;  
    do  
      v6 \= (char)\*v14++ + \_\_ROR4\_\_(v6, 13);  
    while ( \*v14 );  
    if ( v6 \== 3960360590  
      || v6 \== 2081291434  
      || v6 \== \-1850750380  
      || v6 \== 2034681371  
      || v6 \== 122922236  
      || v6 \== \-751679228 )  
    {  
      v7 \= (unsigned int \*)(v11\[7\] + v5 + 4i64 \* \*v15);  
      switch ( v6 )  
      {  
        case \-334606706:  
          a1\[2\] \= \*v7 + v5;  
          break;  
        case 2081291434:  
          a1\[1\] \= \*v7 + v5;  
          break;  
        case \-1850750380:  
          a1\[4\] \= \*v7 + v5;  
          break;  
        case 2034681371:  
          a1\[5\] \= \*v7 + v5;  
          break;  
        case 122922236:  
          a1\[3\] \= \*v7 + v5;  
          break;  
        default:  
          \*a1 \= \*v7 + v5;  
          break;  
      }  
      \--v4;  
    }  
    ++v12;  
    ++v15;  
  }  
  return result;  
}

可以明显看出,函数sub_18cf6存在两部分,第一部分是一个for循环,第二部分是一个while循环:

图片

我们先不着急分析详细逻辑,我们先在看下大的方面这个函数大概率是用来干啥的,首先我们从主函数的sub_188E6看,其调用这个sub_18cf6是没有获取其返回值的,其次传入的是一个指针;

然后我们结合sub_18cf6内容,先看下哪里对传入的指针做了处理,如下

图片

上图中可以看到,在sub_18cf6的第二部分while循环里面,对指针指向的数组的几个元素做了赋值操作;所以分析到这,我们也不难看出这个函数其实就是在给主逻辑函数里面的v13变量(指向数组首地址的指针)赋值;

然后我们再来看sub_18cf6 里面两部分详细逻辑:

第一部分:

图片

上图首先通过fs拿peb拿ldr_list,遍历list,拿basedllname

图片

接着,计算计算dllbasename的特征码(特征码算法:name,逐位小写转大写累加上次结果,结果循环右移13位),找到结果是0x6a4abc5b的这个特征码就结束;

图片

然后获取dllbase地址和以及获取导出表地址,i[4]就是0x20的相对偏移(相对InMemoryOrderLinks内存加载顺序列表),对应的就是dllbase;

图片

图片

接着取导出函数名称表、导出函数序号表:

图片

图片

然后就是第二部分的while循环了,非常直观的取导出名称,然后计算特征码(方法和上面一样就是不做大小写转换了)

图片

内置了几个要找的特征码,当匹配到的时候,就找到对应函数的地址,通过传入的指针,带出返回

图片

简单总结,sub_18cf6这个函数的其实就是一个类似初始化操作的函数,找到之后几个要使用的函数地址;

然后回到主逻辑:sub_188E6:

一个恒真的if(sub_19086是直接返回0),调用sub_19096,传入的参数还是v13指针(也就是刚刚做了些函数地址赋值的数组)

图片

跟入sub_19096,其实现如下,还是一样,我们可以看到主逻辑其实没有获取其返回的值,结合我们观察传入指针的,这里其实和上个函数差不多,也是个传入指针指向的内容做一些初始化赋值,然后通过指针带回;

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。