从恶意软件Nymaim看DGA

阅读量    32512 |   稿费 200

分享到: QQ空间 新浪微博 微信 QQ facebook twitter

 

Nymaim恶意软件首次发现是在2013年。它主要是被用作其他恶意软件的下载器,如勒索软件,后来它也开始为了实现点击欺诈而进行搜索操控。

也许是因为这个恶意软件使用了一种有效而有趣的混淆,关于Nymaim和它得DGA的文章已经有了很多。这种混淆催生了很多有创造性的帮助分析恶意软件的工具,例如 GovCERT.CH 或者CERT Polska

除了混淆之外,Nymaim的有趣之处还在于它试图通过在A记录种添加校验和与在使用之前转换IP地址保护自己,具体介绍可以看CERT Polska的 “Nymaim revisited”,Talos 的 “Threat Spotlight: GozNym”和Alberto Ortega的 “Nymaim Origins, Revival and Reversing Tales”

本月,Nymaim的新版本对上述特性进行了一些修改:

  • 除了加壳之外,混淆被全部的抛弃。相反,恶意软件甚至使用有用的日志消息和带有描述性名称的配置。
  • IP转换略有变化,使用了不同的常量,但其他方面都坚持与之前的过程相同。
  • DGA已经被完全重写。它现在是基于单词列表的,比如Matsnu的DGA, Suppobox,或者Nymaim的近亲Gozi。
  • 除了使用DGA,Nymaim还有一个硬编码域名列表,它们遵循与DGA域名相同的模式,但是在依赖于时间的DGA域名之前进行尝试。

这篇博客文章关注的是Nymaim的DGA和IP转换方面。例如,下面是2018年4月27日的前10个域名:

virginia-hood.top
shelter-downloadable.ps
tylerpreparation.sg
zolofteffectiveness.ch
stakeholders-looked.hn
williampassword.sc
thailandcool.re
thoughtsjazz.ec
recovery-hairy.ac
workshopsforms.hn

我分析了来自Virustotal的样本:

MD5 30bce8f7ac249057809d3ff1894097e7
SHA-256 73f06bed13e22c2ab8b41bde5fc32b6d91680e87d0f57b3563c629ee3c479e73
SHA-1 b629c20b4ef0fd31c8d36292a48aa3a8fbfdf09c
文件大小 484 KB
编译时间戳 2010-06-13 18:50:03 (可能是假的)
首次上传至 Virustotal的时间 2018-04-17 21:49:18
Virustotal 链接 link

我将其解压缩得以下可执行文件。所有截图都来自加载地址0x400000的这个示例:

MD5 379ba8e55498cb7a71ec4dcd371968af
SHA-256 3eb9bbe3ed251ec3fd1ff9dbcbe4dd1a2190294a84ee359d5e87804317bac895
SHA-1 5f522dda6b003b151ff60b83fe326400b9ed7716
文件大小 368 KB
编译时间戳 2018-03-02 23:12:20
首次上传至 Virustotal的时间 2018-04-26 12:19:41 (我上传的)
Virustotal 链接 link

 

分析

本节描述DGA的详细信息。如果你只对Python的实现感兴趣,请参阅DGA部分。

种子

Nymaim的新DGA的种子由三部分组成:

  1. 硬编码的32位大写的16进制字符串,推测为MD5散列值(在分析的样本中是3138C81ED54AD5F8E905555A6623C9C9)。Nymaim将它称为生成密钥
  2. 一年中从零开始的一天。例如,1月1日是第0天。这个值比ISO定义的值小1,ISO定义1月1日为一年中的第1天。从这个值减去一个计数器中的值,计数器从0开始,一直到一个天数增量DayDelta(在样本中是10)。这意味着如果有必要,DGA将重新访问过去10天的域名(除了在年关的时候,详情见下面的滑动窗口)。
  3. 当年的最后两位数字。

这三个值组合成一个字符串。然后对这个字符串进行md5散列,其结果表示为小写的十六进制字符串。请注意,这与生成密钥相反,生成密钥都是大写的。得到的字符串是随后的伪随机数生成器的种子和基础。

seed

伪随机数生成器

伪随机数生成器(PRNG)使用MD5哈希字符串的前8个字符,并将其作为32位整数(即随机数)的大端十六进制表示。然后丢弃MD5散列的前7个字符,其余的字符再次用MD5散列并表示为小写的十六进制字符串。该字符串的前8个字符表示下一个伪随机值。关于整个过程和伪随机数生成器,请参阅下图:

seeding

DGA算法

DGA使用四个随机值从四个列表中选择字符串:

  1. 选择第一个单词列表中的一个单词。
  2. 选择一个分隔符。
  3. 选择第二个单词列表中的一个单词。
  4. 选择顶级域

然后将四个字符串连接起来形成域名。单词的选择是用随机值除以列表长度的余数作为索引从列表中选择:

CString *__thiscall dga(_DWORD *config, CString *szDomainName)
{
  dgaconfig *cfg; // esi@1
  int v3; // eax@2
  unsigned int nNumberOfFirstWords; // ecx@3
  randnrs objRandNrs; // [esp+Ch] [ebp-2Ch]@1
  int dgb2; // [esp+20h] [ebp-18h]@1
  int nr_random_values; // [esp+24h] [ebp-14h]@1
  char cstrDomainName; // [esp+28h] [ebp-10h]@3
  int dbg; // [esp+34h] [ebp-4h]@1

  dgb2 = 0;
  cfg = config;
  init_random_nrs(&objRandNrs);
  objRandNrs.self = &GetRuntimeClass;
  nr_random_values = 4;
  dbg = 1;
  do
  {
    v3 = rand_0(&cfg->random_hash);
    store_rand(objRandNrs.field_8, v3);
    --nr_random_values;
  }
  while ( nr_random_values );
  CString::CString(&cstrDomainName);
  nNumberOfFirstWords = cfg->nNumberOfFirstWords;
  LOBYTE(dbg) = 2;
  CString::operator+=(&cstrDomainName, cfg->rgFirstWords + 4 * (*objRandNrs.r % nNumberOfFirstWords));
  CString::operator+=(&cstrDomainName, cfg->rgSeparators + 4 * (*(objRandNrs.r + 4) % cfg->nNumberOfSeparators));
  CString::operator+=(&cstrDomainName, cfg->rgSecondWords + 4 * (*(objRandNrs.r + 8) % cfg->nNumberOfSecondWords));
  CString::operator+=(&cstrDomainName, cfg->rgTLDs + 4 * (*(objRandNrs.r + 12) % cfg->nNumberOfTLDs));
  CString::CString(szDomainName, &cstrDomainName);
  dgb2 = 1;
  LOBYTE(dbg) = 1;
  CString::~CString(&cstrDomainName);
  LOBYTE(dbg) = 0;
  cleanup_0(&objRandNrs);
  return szDomainName;
}

第一个单词列表包含2450个以字母R到Z开头的单词。最短的有4个字母,最长的有18个(telecommunications):

    "reaches", 
    "reaching", 
    "reaction", 
    "reactions", 
    "read", 
    "reader", 
    "readers", 
    "readily", 
    "reading", 
    "readings", 
    "reads", 
    "ready", 
    "real", 
    "realistic", 
    ...
    "zoom",                                                                 
    "zoophilia",                                                            
    "zope",                                                                 
    "zshops"

只有两个分隔符:零长度字符串和连字符-。第二个单词列表包含以字母C到R开头的4387个单词。最后一个单词是reached,而第一个单词列表恰恰是以reaches开始。

最后,这里包含了74个顶级域,其中顶级域.com出现了4次,.net出现了3次,这增加了.com.net被选中的概率。

顶级域列表如下:

.com, .com, .com, .net, .net, .net, .ac, .ad, .at, .am, .az, .be, .biz, .bt, .by, .cc, .ch, .cm, .cn, .co, .com, .cx, .cz, .de, .dk, .ec, .eu, .gs, .hn, .ht, .id, .in, .info, .it, .jp, .ki, .kr, .kz, .la, .li, .lk, .lv, .me, .mo, .mv, .mx, .name, .net, .nu, .org, .ph, .pk, .pl, .pro, .ps, .re, .ru, .sc, .sg, .sh, .su, .tel, .tf, .tj, .tk, .tm, .top, .uz, .vn, .win, .ws, .wtf, .xyz, .yt

滑动窗口

DGA每天生成一定数量(MaxDomainsForTry)的域名,对于分析的样本,域名数量(MaxDomainsForTry)为64。生成64个域名之后,伪随机数生成器通过从一年的某一天减去1获得前一天的种子重新计算随机数。这样算来,最多生成64*(10+1)= 704个域名:

maxdomainsfortry

在年关的时候,当日期比天数增量(DayDelta)小,那么天数的偏移量会变成负数。例如,在一月三号的滑动窗口的值为2,1,0,-1,… ,-8。负数将会产生新的一组域名。

negative

硬编码域名

Nymaim有一个包含46个硬编码域名的列表,它们遵循DGA模式,两个单词之间用一个可选的连字符分隔。这些域名都使用.com顶级域。在获得任何DGA域名之前,总是先对硬编码域名进行测试。对于分析的样本中,硬编码域名如下:

sustainabilityminolta.com
theories-prev.com
starringmarco.com
seekerpostcards.com
threadsmath.com
recall-pioneer.com
waste-neighborhood.com
usage-maternity.com
standings-descriptions.com
volumedatabase.com
summaries-heading.com
stoppedmeaningful.com
singles-october.com
scottish-fact.com
weblogcourage.com
troycyber.com
reply-phantom.com
wagon-crime.com
sharp-louisiana.com
suitedminerals.com
saskatchewan-funds.com
sites-experts.com
techrepublicexemption.com
serbia-harbor.com
super-ideas.com
translationdoor.com
wildhelmet.com
shoefalse.com
remainedoxide.com
wild-motels.com
staticlesbian.com
valentinequeensland.com
travelling-mechanics.com
solelypersonal.com
resulting-museum.com
towndayton.com
workedforest.com
yorkshire-engineer.com
stockholm-effect.com
reynoldshydrogen.com
sluts-persistent.com
satisfaction-granted.com
slut-hentai.com
territoriesprayers.com
thumbnailsfragrance.com
undergraduategraphical.com

名称服务器测试

Nymaim的一个显著特性是使用DNS查询名称服务器记录(NS)。Nymaim检查任何一个响应中是否包含它称为BlackNsWords的列表中的单词。这写单词通常与沉洞(Sinkhole)相关:

sinkhole
amazonaws
honeybot
honey
parking
domaincontrol
dynadot

如果Nymaim在NS资源记录中找到这些单词中的任何一个,它就不会使用这个域名。

首选DNS服务器

Nymaim使用一个称为PreferredDnsServers的DNS服务器列表,可能是因为这些服务器不太可能更改或阻塞DNS请求。

IP Company
8.8.8.8 Google
8.8.4.4 Google
156.154.70.1 Neustar Security
156.154.71.1 Neustar Security
208.67.222.222 OpenDNS
208.67.220.220 OpenDNS

IP转换

与Nymaim的早期版本一样,A资源记录并不是C2服务器的ip地址。真实的地址需要通过一系列可逆的异或和减法步骤对ip进行变换才能得到。Talos 威胁情报在2017年9月写了一份描述该算法的详细报告。

下图视图片段显示了IP的转换:

iptransformation

在这篇博客文章的末尾可以找到用于在两个方向上执行IP转换的Python脚本。

校验和测试

Nymaim仍然使用A资源记录的校验和测试。例如,下面是一个在编写本文时正在运行C2服务器域名的ip:

> dig @8.8.8.8 +short -t A sustainabilityminolta.com
127.33.12.14
127.33.12.15
192.202.176.55
126.56.117.50

下表列出了这四个IP(第一列)和转换后的真实IP(第二列)。第三列是整数表示:

IP IP’ value
127.33.12.14 127.0.0.0 0x0000007F
127.33.12.15 127.0.0.1 0x0100007F
192.202.176.55 192.42.116.41 0x29742AC0
126.56.117.50 190.43.116.42 0x2A742BBE

Nymaim将检查所有整数值,看看它们是否是其余值的和。在上面的例子中,加粗的IP190.43.116.42是A记录126.56.117.50的转换结果。用小端整数可以表示为0x2A742BBE。这对应于将剩余ip的整数表示形式相加得到的校验和,也就是0x2A742BBE = 0x0000007F + 0x0100007F + 0x29742AC0

匹配了校验和的IP将从列表中删除,它只作为其他IP的校验和。然后Nymaim将一个接一个地测试转换后的ip:

  1. DNS对sustainabilityminolta.com的NS资源发出请求,以检查是否为沉洞(Sinkhole)。响应dns100.ovh.net不包含BlackNsWords中任何单词, Nymaim继续查询A记录。
  2. 对A记录的DNS请求返回四个转换后的ip。因为第四个IP是其余三个IP的校验和,所以Nymaim继续按顺序联系IP。
  3. 第一个非本地IP地址192.42.116.41使用HTTP POST请求联系:http://192.42.116.41/index.php

request

请求

实际的C2服务器请求是HTTP post。内容使用会话密钥进行AES加密,会话密钥受非对称加密保护。第一个C2请求大约是900字节。URL文件名硬编码为index.php:

http://192.42.116.41/index.php

DGA特征

property value
类型 TDD (时间依赖的确定性)
生成模式 j基于伪随机数生成器的MD5
种子 生成密钥和当前日期
域名改变频率 每天,具有一个11天的滑动窗口
每天的域名数 46 个硬编码域名+64个新生成域名 + 640 之前生成的域名
序列 有序的
域名之间的等待时间
顶级域 69 个不同的顶级域, 更偏好于.com.net
二级域特征 两个单词表中的单词和一个连接符
二级域长度ength 8 (e.g., realrays.kr) – 34 (e.g., telecommunications-pharmaceuticals.name)

 

结果

在本节中,你将看到DGA的Python实现,以及用于Nymaim的IP转换的脚本。

DGA算法

DGA需要一个比较大的单词列表( words.json),将它放在与DGA脚本相同的目录中。你可以使用-d——date生成特定日期的域名,如:

> python dga.py -d 2018-04-27



import json
import argparse
from datetime import datetime
import hashlib


class Rand:

    def __init__(self, seed, year, yday, offset=0):
        m = self.md5(seed)
        s = "{}{}{}".format(m, year, yday + offset)
        self.hashstring = self.md5(s)

    @staticmethod
    def md5(s):
        return hashlib.md5(s.encode('ascii')).hexdigest()

    def getval(self):
        v = int(self.hashstring[:8], 16)
        self.hashstring = self.md5(self.hashstring[7:])
        return v

def dga(date):
    with open("words.json", "r") as r:
        wt = json.load(r)

    seed = "3138C81ED54AD5F8E905555A6623C9C9"
    daydelta = 10
    maxdomainsfortry = 64
    year = date.year % 100
    yday = date.timetuple().tm_yday - 1

    for dayoffset in range(daydelta + 1):
        r = Rand(seed, year, yday - dayoffset)
        for _ in range(maxdomainsfortry):
            domain = ""
            for s in ['firstword', 'separator', 'secondword', 'tld']:
                ss = wt[s]
                domain += ss[r.getval() % len(ss)]
            print(domain)

if __name__=="__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("-d", "--date", help="as YYYY-mm-dd")
    args = parser.parse_args()
    date_str = args.date
    if date_str:
        date = datetime.strptime(date_str, "%Y-%m-%d")
    else:
        date = datetime.now() 
    dga(date)

IP转换脚本

下面的Python脚本可用于在两个方向转换Nymaim IP地址,并查看IP地址列表是否满足校验和要求:

import argparse


def iptoval(ip):
    els = [int(_) for _ in ip.split(".")]
    v = 0
    for el in els[::-1]:
        v <<= 8
        v += el
    return v


def valtoip(v):
    els = []
    for i in range(4):
        els.append(str(v & 0xFF))
        v >>= 8
    return ".".join(els)


def step(ip, reverse=False):
    v = iptoval(ip)
    if reverse:
        v ^= 0x18482642
        v = (v + 0x78643587) & 0xFFFFFFFF
        v ^= 0x87568289
    else:
        v ^= 0x87568289
        v = (v - 0x78643587) & 0xFFFFFFFF
        v ^= 0x18482642
    return valtoip(v)


def transform(ip, iterations=16, reverse=False):
    for _ in range(iterations):
        ip = step(ip, reverse=reverse)
    return ip


def checksum(pairs, index):
    checksum = 0
    for i, p in enumerate(pairs):
        if i == index:
            continue
        checksum += iptoval(p[1])
    return checksum & 0xFFFFFFFF


def findip(pairs):
    for i, p in enumerate(pairs):
        c = checksum(pairs, i)
        if c == iptoval(p[1]):
            return p[0]


if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("ip", nargs="+")
    parser.add_argument("-r", "--reverse", help="reverse transformation",
                        action="store_true")
    parser.add_argument("-c", "--checksum", help="test checksum",
                        action="store_true")
    args = parser.parse_args()

    pairs = []
    for ip_src in args.ip:
        ip_dst = transform(ip_src, reverse=args.reverse)
        pair = (ip_src, ip_dst)
        d = "-->"
        if args.reverse:
            pair = pair[::-1]
            d = "<--"
        pairs.append(pair)
        if not args.checksum:
            print("{} {} {}".format(ip_src, d, ip_dst))

    fmt = "| {:4} | {:15} | {:15} | {:10} |"
    fmt2 = "| {:4} | {:15} | {:15} | 0x{:08X} |"
    if args.checksum:
        print(fmt.format("", "IP", "IP'", "value"))
        print(fmt.format(*4 * ["---"]))
        ok_ip = findip(pairs)

        for ip, ipp in pairs:
            if ip == ok_ip:
                continue
            print(fmt2.format("", ip, ipp, iptoval(ipp)))

        for ip, ipp in pairs:
            if ip != ok_ip:
                continue
            print(fmt2.format("x", ip, ipp, iptoval(ipp)))

        if not ok_ip:
            print("No IP matches checksum")
        else:
            print("The IP marked x matches the checksum of remaining IPs, "
                  "it is removed.")

例如,sustainabilityminolta.com的一个A记录值是192.202.176.55。可以得到真正的IP:

> python3 transform.py 192.202.176.55
192.202.176.55 --> 192.42.116.41

要反转转换,使用-r--reverse

> python3 transform.py 192.42.116.41 --reverse
192.42.116.41 <-- 192.202.176.55

要检查A资源记录是否满足校验和,添加所有ip为参数并添加-c——checksum

> python3 transform.py 127.33.12.14 127.33.12.15 192.202.176.55 126.56.117.50 --checksum
|      | IP              | IP'             | value      |
| ---  | ---             | ---             | ---        |
|      | 127.33.12.14    | 127.0.0.0       | 0x0000007F |
|      | 127.33.12.15    | 127.0.0.1       | 0x0100007F |
|      | 192.202.176.55  | 192.42.116.41   | 0x29742AC0 |
| x    | 126.56.117.50   | 190.43.116.42   | 0x2A742BBE |
The IP marked x matches the checksum of remaining IPs, it is removed.

如果IP与校验和匹配,则用x标记它。

 

DGArchive

本文中的DGA是由DGArchive项目实现的。

分享到: QQ空间 新浪微博 微信 QQ facebook twitter
|推荐阅读
|发表评论
|评论列表
加载更多