首页
社区
课程
招聘
[原创]恶意代码分析之 API hash 反静态分析
2019-11-11 13:38 6413

[原创]恶意代码分析之 API hash 反静态分析

2019-11-11 13:38
6413

1 摘要

在恶意代码分析中,Windows 平台的 API 函数是非常重要的信息来源,比如通过分析恶意代码中使用的 API 函数,我们不需要对已加壳的文件进行逆向分析,因为我们只需要对恶意代码所执行的 API 调用来进行动态分析,就可以知道某个特定文件具体的功能了。通过这样的方法(分析 API 调用),我们可以确定一个文件是否具有恶意性,而有些 API 调用只有某些特殊类型的恶意软件才会去使用。比如说,常用的恶意 payload 下载 API是 URLDownloadToFile,而GetWindowDC 这个 API 一般用于间谍软件或键盘记录器等恶意工具(用于屏幕截取),因此恶意软件作者会通过使用动态导入的方式隐藏 API 函数名称,避免直接使用静态导入而完全显示出来,动态导入在 shellcode 编写中也非常常见,而本次的 Sodinokibi/REvi 勒索软件隐藏 API 函数案例为 API hash 匹配技巧,完全避免了包含函数名,仅显示函数名称的固定大小的哈希,也节省了恶意代码的存储空间。

2 起因

近期分析多个勒索软件时,会遇到使用计算 hash 来获取所需 API 函数的流程,这是一种非常常用的反静态分析技巧,本文对此进行学习记录。

3 原理

静态导入是在 Windows 下由 PE 文件的导入地址表(IAT)中列出的,动态导入是使用 Windows API 提供的 LoadLibrary、GetProcAddress 函数进行导入的。而 API hash 匹配导入,指为每个函数名称计算哈希值,后续通过计算哈希值后经过匹配得到所需的函数地址。

4 静态分析

IDA 载入该样本后,整体框架只有两个函数,查询后发现没有任何导入函数,如下:



双击进入第一个函数 sub_40369D,查看如下:


一步一步分析,再接着进入 sub_406A4D,发现有跳转,跟随跳转,如下:



函数一开头就是一个循环,发现 dword_41C9F8[esi]有循环赋值的操作,而 sub_405DCF 则是关键的重建 API 函数地址的函数(后续动态调试会发现),所以首要分析该函数。该处循环每次累加 0x4,直到累加和为 0x230大小时结束,也就是在 dword_41CC24地址处结束。每个 DWORD 值对应一个 API 函数,Sodinokibi/REvi 勒索软件会使用 API hash 解析成相应的 API 函数地址。进入该数组后,发现硬编码的 hash 值内容如下,每个值为 DWORD 类型。


进入上述的 API 构建函数 sub_405DCF 内部,如下:


可以发现存在很多分支结构,有很多判断条件,为了方便,转换成伪 C 代码看看。


函数开头的就对输入参数进行了处理,该函数的输入参数是刚刚硬编码的 DWORD 大小的值,也就是 API hash 值,为了便于分析,对其进行重命名为API_hash_a1。


输入的值首先会与 0x76C7 异或,得到的结果左移 16 位,然后再与 0xAFB9 异或,最后的结果再与之前输入的值进行异或,最终得到 v1。


接着由 v1 右移 21 位得到的 v2,在后续流程里会跟进 v2 的值来到不同的分支,如下:


跟进其中一个分支的函数里,发现又会调用 sub_405DCF,输入参数为硬编码的 DWORD 数值,同样是解码操作。


接着往下分析,遇到了 0x3C 与 0x78(0x18 与 0x60 的和),如果对 PE 文件结构熟悉,就会发现这与寻找导出表有关,因为 dll 文件从加载基址算起,偏移 0x3C 的地方就是其 PE 头,PE 头偏移 0x78 的地方存放着指向函数导出表的指针,如下图,就是寻找 PE 文件的导出表的过程。


为了便于查看,这里的话我们可以在 IDA 中自建本地类型,构建结构体,如下是 winnt.h 中导出表的定义。
typedef struct _IMAGE_EXPORT_DIRECTORY {
  DWORD Characteristics; //offset 0x0
  DWORD TimeDateStamp; //offset 0x4
  WORD MajorVersion;  //offset 0x8
  WORD MinorVersion; //offset 0xa
  DWORD Name; //offset 0xc
  DWORD Base; //offset 0x10
  DWORD NumberOfFunctions;  //offset 0x14
  DWORD NumberOfNames;  //offset 0x18
  DWORD AddressOfFunctions; //offset 0x1c
  DWORD AddressOfNames; //offset 0x20
  DWORD AddressOfNameOrdinals; //offset 0x24
 }

在 IDA 中加入 Local Types,如下:





右键选中需要转换的变量进行转换,如下:




上图可以很清楚的显示出导出表结构体里的相应字段,在下面的 while 循环里,通过比较之前获取的 v1 经过计算后的值 v15 与 sub_405BAE 生成的结果,看是否一致,如果一致就会获取到当前的 API 函数的地址(由最后的 return v14 + *(_DWORD *)(v21 + 4 * *(unsigned __int16 *)(v22 + 2 * v16));返回),进入 405BAE 查看,如下:


发现存在跳转,双击进入反编译失败。


只能来到原始的汇编区域看看,按 G 跳转到 sub_405BAE,如下:




上图由于 IDA 分析出现错误,导致 IDA 反汇编失败,所以选择该区域后,右键选中 Undefine,之后再创建函数。







下图为反汇编成功后的函数内部,取输入的函数名(a1)获取名称的每个字符进行计算,最后得到一个 hash 值,如下:


通过获取 dll 导出表里的 API 函数名,接着通过 sub_405BA 生成 hash 值,接着与经算术处理后的预先硬编码的值进行比较,如果条件为假,也就是相等的情况,就会得到需要的 API 函数地址,最后返回给 dword_41C9F8数组,伪代码如下:
v15 = v1 & 0x1FFFFF;
  v16 = 0;
  v17 = (_IMAGE_EXPORT_DIRECTORY *)(v13 + *(_DWORD *)(*(_DWORD *)(v13 + 0x3C) + v13 + 0x78));
  v21 = v13 + v17->AddressOfNameOrdinals;
  v18 = v13 + v17->AddressOfNames;
  v22 = v13 + v17->AddressOfNames;
  v20 = v13 + v17->AddressOfFunctions;
  API_hash_a1a = v17->NumberOfNames;
  if ( !API_hash_a1a )
    return 0;
  while ( (sub_405BAE((unsigned __int8 *)(v14 + *(_DWORD *)(v18 + 4 * v16))) & 0x1FFFFF) != v15 )
  {
    v18 = v22;
    if ( ++v16 >= API_hash_a1a )
      return 0;
  }
  return v14 + *(_DWORD *)(v20 + 4 * *(unsigned __int16 *)(v21 + 2 * v16));

为了验证上述过程,在本地编写 Python 脚本模拟两个 hash 值得计算过程,如下,发现 0xB6D391AE== wsprintfW :
func_name = "wsprintfW"  # 由动态调试调试得到的第一个API函数名

api_hash = 0xB6D391AE  # 第一个值


def get_api_hash(name):
    result = 0x2B
    for i in name:
        result = ord(i) + 0x10F * result
    return result & 0x1FFFFF


def my_api_hash(my_hash):
    result = my_hash ^ ((my_hash ^ 0x76C7) << 16) ^ 0xAFB9
    return result & 0x1FFFFF


print(hex(get_api_hash(func_name)))
print(hex(my_api_hash(api_hash)))


5 动态分析

对该样本去掉 ASLR 后,载入 ollydbg 如下:


找到 Optional Header 下的 DLL characteristic 字段,将 4080,改成 0080 即可。




单步进入 0040369D,如下:


发现第二个 call,调用的是一个硬编码的地址,怀疑 0040369D 会对其做操作。步过 0040369D 后,发现第二个 call [0041CB64]=75A61B00 (kernel32.SetErrorMode),已经获取到 API 地址,右键跟随该地址 0041CB64到数据区域查看,如下:





0041C9F8 数组已经获取到了所有所需的 API 函数地址,如下:



6 hash

"sodinunpacked.exe"
sha256:5f56d5748940e4039053f85978074bde16d64bd5ba97f6f0026ba8172cb29e93
sha1:74e3d82f7ee81109e150dc41112cf95b3a4b5307 
md5:890a58f200dfff23165df9e1b088e58f 

7 参考

https://blag.nullteilerfrei.de/2019/11/09/api-hashing-why-and-how/


[培训]《安卓高级研修班(网课)》月薪三万计划,掌握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法

上传的附件:
收藏
点赞2
打赏
分享
最新回复 (10)
雪    币: 19566
活跃值: (60248)
能力值: (RANK:125 )
在线值:
发帖
回帖
粉丝
Editor 2019-11-11 16:14
2
0
楼主高产啊!感谢分享!
雪    币: 722
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
think8765 2019-11-11 17:49
3
0
楼主, 有没有那个样本呢?
雪    币: 17421
活跃值: (5004)
能力值: ( LV9,RANK:450 )
在线值:
发帖
回帖
粉丝
jishuzhain 7 2019-11-11 20:54
4
0
think8765 楼主, 有没有那个样本呢?
勒索软件不宜传播,但文末给了hash,可以搜索下。
雪    币: 279
活跃值: (113)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
罗小墨 2019-11-22 10:18
5
0
学习了
雪    币: 119
活跃值: (32)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
wx_菠萝 2019-12-12 17:35
6
0
好巧,这个跟你一样,,,并且引用文章、样本HASH。。。、都一样。。。“他抄你原创。。。。
https://www.youtube.com/watch?v=R4xJou6JsIE
雪    币: 17421
活跃值: (5004)
能力值: ( LV9,RANK:450 )
在线值:
发帖
回帖
粉丝
jishuzhain 7 2019-12-12 18:58
7
0
wx_菠萝 好巧,这个跟你一样,,,并且引用文章、样本HASH。。。、都一样。。。“他抄你原创。。。。[em_19]” https://www.youtube.com/watch?v=R4xJou6JsIE
不是,是我观看过这个国外的视频,然后自己动手学习了一遍。
雪    币: 83
活跃值: (1047)
能力值: ( LV8,RANK:130 )
在线值:
发帖
回帖
粉丝
killpy 2 2019-12-13 10:34
8
0
怎么去掉掉 ASLR   改成0080干啥
雪    币: 17421
活跃值: (5004)
能力值: ( LV9,RANK:450 )
在线值:
发帖
回帖
粉丝
jishuzhain 7 2019-12-13 16:30
9
0
killpy 怎么去掉掉 ASLR 改成0080干啥
看下这个文章  https://www.jianshu.com/p/91b2b6665e64
雪    币: 159
活跃值: (695)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
xiaozhu头 2019-12-20 18:04
10
0
请问哪里能找到这个恶意样本,刚刚搜了一阵子,没搜到。
雪    币: 17421
活跃值: (5004)
能力值: ( LV9,RANK:450 )
在线值:
发帖
回帖
粉丝
jishuzhain 7 2019-12-20 18:11
11
0
xiaozhu头 请问哪里能找到这个恶意样本,刚刚搜了一阵子,没搜到。
https://bbs.pediy.com/thread-254296.htm 试试这个
游客
登录 | 注册 方可回帖
返回