脚本语言(英语:Scripting language)是为了缩短传统的“编写、编译、链接、运行”(edit-compile-link-run)过程而创建的计算机编程语言。早期的脚本语言经常被称为批处理语言或工作控制语言。一个脚本通常是解释运行而非编译。
游戏脚本由于脚本语言的开发成本低,许多游戏引擎不约而同地使用了脚本语言作为主要编程语言,比如大名鼎鼎的《魔兽世界》,就是使用的Lua进行开发的。本文所描述的游戏脚本是Unity(游戏引擎)中使用的主要编程语言——C#,并且Unity提供C#中的主要脚本API ,其脚本具有简单、易学、易用的特性,目的就是希望能让开发人员快速完成游戏开发。
对于传统的第三方游戏插件开发而言,由于没有游戏厂商提供的官方接口,只能通过非常规的方式将插件的功能安装在游戏中。
通常来说,这些安装的方式与病毒的行为十分类似,更偏向于系统底层,比如利用Windows API修改目标进程中的数据,或者创建远程线程让目标进程执行汇编代码或加载第三方库(如DLL),更有甚者,直接在Windows内核中劫持目标进程网络通讯,或进行APC注入。
目前传统的应用层注入方式主要有以下几种:
静态注入类:
动态注入类:
使用上述方法安装的插件,其功能实现大部分要靠硬编码、钩子、内存修改来实现。这样一来,需要耗费大量的时间分析游戏的汇编代码与通过内存中的数据来构建结构体与游戏对象。
假如我们可以让游戏脚本在游戏中执行,通过脚本直接使用游戏引擎封装好的函数与对象,不就可以事半功倍了吗?
Unity3D是由Unity Technol
ogies开发的一个让玩家轻松创建诸如三维视频游戏、建筑可视化、实时三维动画等类型互动内容的多平台的综合型游戏开发工具,是一个全面整合的专业游戏引擎。Unity使用户能够以2D和3D方式创建游戏,并且引擎提供C#中的主要脚本API ,用于插件形式的Unity编辑器,游戏本身以及拖放功能。在 C#作为引擎使用的主要编程语言之前,Unity支持 Boo 和 JavaScript 版本的 UnityScript。
Mono是一个免费的开源项目。由微软的子公司Xamarin(前身为Novell,最初由Ximian)和.NET基金会领导。旨在构建符合ECMA(欧洲计算机制造商协会)标准的.NET Framework兼容工具集。包括 C#编译器和带有实时(JIT)编译的公共语言运行时。
简单来说,Mono是Unity3D的一个运行时,负责C/C++和C#/CIL的交互。
举个例子,随便打开一个Unity游戏的根目录,你会发现一个名为UnityPlayer.dll的动态链接库,这个动态链接库封装是Unity的底层C++代码。
在 游戏根目录\Mono\EmbedRuntime\
(某些低版本Unity游戏目录为游戏根目录\Mono\
,笔者的为 2018.2.0 Beta)下有一个名为mono.dll的动态链接库,这个便是负责C/C++和C#/CIL的交互的模块,网上有开源版本,但许多游戏公司为了防止游戏被恶意修改,通常都重新编译mono.dll,在其中加入加密游戏数据与检测恶意行为的代码。
在游戏根目录\游戏名_Data\Managed\
中名为Assembly-CSharp.dll的文件便是C#脚本代码。
三者的关系如下:
利用Mono平台实现游戏脚本注入的主要方法有两种,一种是静态修改,直接PatchAssembly-CSharp.dll的代码。
本文测试环境是在Windows下,主要是阐述思路,Android平台的注入同理。
另一种是调用mono.dll的API,动态加载C#代码(移动端同理),本文主要讨论后者。代码实现主要由三部分组成:
C#库文件(负责实现第三方功能)
动态链接库(用于在目标程序加载脚本)
主程序(负责注入DLL到目标程序)
负责实现第三方功能,代码需要因游戏而定,主要是通过获取或修改Unity原生组件(Transform、physics等)的数据实现第三方功能,也可以直接使用开发者定义的函数,但这种方法需要获取游戏源代码,需要解密脚本文件,在此不再赘述。
在目标进程中载入C#脚本需要以下API,具体参数查阅mono官方文档,上面都有详细解释。
通过文件名加载C#脚本文件镜像。
mono_image_open_from_data
从文件镜像中读取并将C#代码编译为IL代码,至此,C#代码被即时编译完毕。
mono_assembly_load_from_full
获取IL代码的镜像。
mono_assembly_get_image
通过类名获取类句柄,供mono_class_get_method_from_name调用。
mono_class_from_name
通过类句柄和函数名获取函数地址,供mono_runtime_invoke调用。
mono_class_get_method_from_name
通过函数地址运行IL代码,代码开始运行。
mono_runtime_invoke
在调用mono的API之前,我们需要获取API的函数地址,基本方法如下:
首先获取mono.dll的模块句柄
HMODULE hMono = GetModuleHandle(L"mono.dll");
再获取API地址
typedef void* (__cdecl *MONO_IMAGE_OPEN_FROM_DATA)(char *ImageName);MONO_IMAGE_OPEN_FROM_DATA mono_image_open_from_data;mono_image_open_from_data = (MONO_IMAGE_OPEN_FROM_DATA)GetProcAddress(hMono, "mono_image_open_from_data");
用这种方法获取所有需要调用的API
加载C#脚本代码
#define ClassName L"ClassName"#define MethodName L"MethodName"#define name_space L"name_space"intptr_t raw_image = ImageOpenFromDataFull(file_data);intptr_t assembly = AssemblyLoadFromFull(raw_image);intptr_t image = AssemblyGetImage(assembly);intptr_t class_id = GetClassFromName(image, name_space, ClassName);intptr_t method = GetMethodFromName(class_id, MethodName);RuntimeInvokeMethod(method);
还可以在DLL中实现C#热更新,方便调试。
主程序负责将动态链接库注入到目标程序。
通过上文提到的远程线程注入方法
使用 CreateRemoteThread
远程调用 LoadLibrary
即可。
对抗脚本注入,有以下几种、主要思路是在mono.dll上做手脚。
当前流行的方法通过修改mono源码,加密Assembly-CSharp.dll,并在mono_image_open_from_data里加入解密脚本的函数,这样既可以防止脚本被静态Patch,又可以防止动态注入。
Android平台下的进程保护
Windows平台下的进程保护(R0、R3)
另外一种是在其他mono函数加入检测代码。笔者所遇到便是在mono_class_from_name加入检测代码,非原生脚本的加载都会导致函数调用崩溃。
利用没有被修改的函数加载代码,如mono_assembly_foreach枚举IL代码镜像,绕过mono_class_from_name的检测。
重写mono_class_from_name函数。
加载另一个纯净的mono.dll,使用纯净模块里面的函数来加载代码。
利用mono平台注入C#代码大大提高了开发效率,如果是单纯用原生Unity组件开发出来的代码基本上适用于所有用Unity引擎开发的游戏。
未来Unity可能会放弃mono平台,转用LICPP平台,但是短时间内是不会放弃mono这个成熟的平台的,所以,本方法在未来一段时间内都有利用价值。
]]>LM Hash是一种较古老的Hash,在LAN Manager协议中使用,非常容易通过暴力破解获取明文凭据。Vista以前的Windows OS使用它,Vista之后的版本默认禁用了LM协议,但某些情况下还是可以使用。
Vista以上现代操作系统使用的Hash。通常意义上的NTLM Hash指存储在SAM数据库及NTDS数据库中对密码进行摘要计算后的结果,这类Hash可以直接用于PtH,并且通常存在于lsass进程中,便于SSP使用
Net-NTLM Hash用于网络身份认证(例如ntlm认证中),目前分为两个版本:
通常我们使用Responder等工具获取到的就是NetNTLM,这类hash并不能直接用来PtH,但有可能通过暴力破解来获取明文密码
Net-NTLM出现在NTLM认证过程中,其计算过程依赖NTLM Hash
参考资料:
https://medium.com/@petergombos/lm-ntlm-net-ntlmv2-oh-my-a9b235c58ed4
本文提到的NTLM认证特指Microsoft NTLM Protocol。
NTLM认证是一种在网络上进行认证的安全协议,其主要过程粗略地分为三步:
上面的过程发生在工作组环境中,在域环境中使用NTLM Pass-Through认证,核心过程与工作组没有太大区别:
主要区别在于Server会将认证信息使用netlogon协议发送给域控制器,由域控制器完成检验并返回认证结果
在网络登录过程中不使用凭证输入对话框来收集数据,而会使用到预先生成的凭据。
分为网络明文登陆和交互式登陆等。这些登录类型在认证期间将用户明文密码发送到服务器。 服务器可以在LSASS中缓存这个密码或其处理后的值,并使用它来验证其他资源。
NTLM认证出现在网络登录中,而远程服务器不缓存用户凭据,这也与ntlm认证的特性相吻合。 Double-Hop的问题也与网络/非网络登录的性质有关。
注意:本文所指均为狭义上的Pass-the-Hash,即一种在NTLM认证中使用NTLM Hash进行认证的手段
在上面的三步过程中,我们发现并没有使用到用户提供的明文密码,而是使用NTLM Hash来计算NetNTLM Hash。Hash传递攻击就发生在NTLM认证的第三步,我们能使用获取到的NTLM Hash来完成一次完整的认证。
靶机ip:
攻击机ip:
以mimikatz为例,在攻击机上以管理员身份运行mimikatz:
在攻击之前我们需要知道目标账户名以及hash值,这里我们假设获取到了ntlm hash:
我们使用mimikatz来pth:
这里需要domain,user,ntlm以及打开的程序四个参数。默认情况下打开cmd。
我们知道,pth只是一种无明文认证的手段,我们还需要针对具体的服务进行攻击,这里以smb服务为例。通常我们访问一个unc路径时,不考虑double-hop的情况下,如果没有指定windows会自动用当前用户的凭据进行ntlm认证,例如命令dir \\Target\aaa
。由于windows某些ssp会在lsass中缓存hash值,并使用它们来ntlm认证,那么理论上我们如果在lsass中添加包含目标账户的hash的合法数据结构,就可以在使用类似dir这些命令时用目标账户进行认证,这也是mimikatz中pth功能的原理。
上一步里面我们已经用mimikatz修改了内存中的hash值,那么我们就能利用弹出的cmd来测试一下是否能网络登录目标系统,访问c\$共享。
这里使用的exist用户是非内置管理员用户,尽管c$共享允许管理员组成员访问,但由于uac对网络登录的限制,导致直接访问会被Access Died:
出于演示的需要,我们把靶机上的UAC级别设置为从不通知
并重启,再次pth:
能成功访问c\$共享。
试图缓解PtH的危害可以从两个方面入手(当然不仅仅是这两方面):
- 禁止NTLM认证
- 防止NTLM Hash被获取到
第一种方式当然是一劳永逸地阻止了PtH,但是NTLM认证已经被微软使用了很长时间,完全禁止可能会破坏已部署的一些环境,在实际应用中阻力较大。
由于NTLM认证的天然缺陷,微软似乎很难改变其行为,因此更多地把防御放在了阻止Hash被获取到这种思路。下面我们来介绍微软发布的一些补丁以及它们可能的绕过方式。
kb2871997在缓解PtH上做出了不少努力,其为windows增加的特性值得深入研究。它在win server 2012 R2及以上版本已默认集成。
根据微软公告,安装kb2871997后会有这么些行为:
我们前面提到过,lsass进程会缓存用户凭据,有明文有Hash,这取决于使用的ssp类型以及补丁情况。
上图中msv、tspkg、wdigest、kerberos这几个ssp都获取到了凭据,它们分别用于Terminal Server认证(RDSH),ntlm认证,摘要认证以及kerberos认证。我们能看到只有msv没有明文密码。从前文中对ntlm认证过程的描述,我们可以发现除了第一步客户端要求用户输入明文密码,之后的认证过程再也没有出现过明文密码。这也就是说,ssp只需要计算ntlm hash并放在内存中就行了。
我们看到摘要认证wdigest在windows 2008 中缓存了明文密码。摘要认证类似于ntlm认证,也使用了挑战-响应机制,但它与ntlm认证较大的区别在于客户端计算响应时,需要使用到明文密码,为了实现SSO,需要将明文密码放在内存中方便进行计算,因此客户端一方的ssp理论上必须在内存中缓存明文密码,这也是为什么我们能抓到明文密码的原因;与之相比,NTLM SSP使用的单向函数如NTOWFv2只需要计算一次并放置在内存中,之后计算响应只需要这个hash值即可。
kb2871997会删除除了wdigest ssp以外其他ssp的明文凭据,但对于wdigest ssp只能选择禁用。用户可以选择将HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\SecurityProviders\WDigest\UseLogonCredential
更改为0来禁用。
安装kb2871997之后我们使用mimikatz抓取内存中的密码:
wdigest ssp默认仍然在内存中存储明文密码,更改注册表后注销,重新登录,再次抓取:
wdigest已经抓不到明文密码以及密码hash。
这仅仅是增大了从内存中获取ntlm hash的难度,但并不能阻止从注册表等其他地方获取。
下图中,工作组环境可以在本地安全策略->用户权限分配->拒绝从网络访问这台计算机
配置是否允许用户或用户组网络登录:
理论上UAC无法限制RID为500的用户网络登录,但打补丁并配置好禁止administrator(RID==500)网络登录,测试结果如下:
处理方式太过粗暴,可能会影响用户正常需求。
受限管理员模式能阻止主机缓存内存中远程验证用户的凭据。
我们来做两个测试
mstsc /restrictedadmin
,也即受限管理员模式启动,服务端支持受限管理员模式:\
)登录,几乎都会登陆失败。从上面两种模式对比可以看出,客户端直接发送明文密码到服务端,可能会被mimikatz从内存中获取到;而受限管理员模式则能避免发送明文,服务端内存中也不会缓存用户凭据。
“受限管理员模式”的初衷是为了保护远程桌面服务端,即当服务端被攻陷时,使用受限管理员模式可以在一定程度上保护客户端用户的凭据不被mimikatz等工具获取到。因此,这个保护措施并不是直接针对PtH,效果也是有限的。
实际上受限管理员模式本身就会增加新的攻击路径,即可以以PtH的方式向远程桌面服务器发起认证。也就是说,在实际应用中,我们可以利用mimikatz的sekurlsa::pth
来启动mstsc,使用受限管理员模式来无明文密码登录,当然也有些其他的RDP客户端可以实现利用密码Hash来认证,例如FreeRDP。
受保护用户是一个新的域全局安全组,对于该组的成员,Windows 8.1设备或Windows Server 2012 R2主机不会缓存受保护用户不支持的凭据。如果这些组的成员登录到运行早于Windows 8.1的Windows版本的设备,则该组的成员没有其他保护。
登录到Windows 8.1设备和Windows Server 2012 R2主机的受保护用户组的成员不能再使用:
如果域功能级别是Windows Server 2012 R2,则该组的成员不能再:
更新用户票证(TGT)超过最初的4小时生命周期
在域账户未加入Protected Users组时,我们使用mimikatz获取内存中的凭据:
我们可以看到ntlmssp缓存了emanon账户的密码hash。我们将此用户加入Protected Users:
注销,重新登录,再次使用mimikatz抓取密码:
emanon用户的ntlm hash已经无法获取到,但机器账户DM1$以及本地用户的hash仍然能获取到,这也说明Protected Users保护范围是有限的。
网上有些文章提到了Pass-the-Key(Overpass-the-hash),Protected Users对这种攻击方式也有一定缓解功能。PtK是在域中攻击kerberos认证的一种方式,据称可以在NTLM认证被禁止的情况下用来实现类似PtH的功能(毕竟是针对kerberos认证,其实与NTLM认证没什么关系)。关于这种攻击方式可以查看本节的参考,这里不再赘述。我们比较关心使用mimikatz来Pass-the-Key所需要的信息,其中必须有的是请求tgt所需的用户hash,这可以使用mimikatz中的sekurlsa::ekeys
来获取:
经测试,域功能级别为windows server 2012 R2(未测试低等级),将域用户加入Protected Users用户组后无法通过ekeys命令获取Hash。当然,机器账户还是能够获取到。
Protected Users是一个域安全组,仅能在域中保护用户,并且阻止抓取用户Hash并没有直面PtH这种攻击方式,域用户凭据仍可能通过注册表Cache、钓鱼攻击等方式获取到。
LSA(包括本地安全机构服务器服务(LSASS)进程)验证用户是否进行本地和远程登录,并实施本地安全策略。Windows 8.1操作系统为LSA提供额外保护,以防止未受保护的进程读取内存和代码注入。启用此功能后无法把debugger attach到进程上。在win8.1及2012 r2以上有效,启用的方法是reg add HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Lsa /v RunAsPPL /t REG_DWORD /d 1
之后重启。
重启之后,使用mimikatz抓取内存中的密码:
使用lsadump::lsa /inject
:
同样无法获取到密码hash
mimikatz能够通过加载其驱动程序来绕过LSA Protection。在实际使用中需要注意mimikatz同目录下需要有驱动程序mimidrv.sys
命令:1
2
3privilege::Debug
!+
!processprotect /process:lsass.exe /remove
测试:
LAPS是用于管理计算机本地用户密码的一个客户端扩展(CSE),在域中依赖比较少,核心在于组策略支持。
LAPS一个重要功能是随机化所有域成员本地管理员密码,密码按照较强的密码策略生成,定期修改。这使得用相同凭据横向渗透变得很困难。
本地管理员密码明文存储在AD中,仅有管理员及获得管理员委派的账户能访问到。
LAPS和仅仅增大了横向渗透的难度,并且配置上出错也可能导致前功尽弃。LAPS配置上可能出现的一个问题是向非授权用户授予“All Extended Rights”权限,这导致如果我们获取该账户,就能访问AD中的密码。
Credentials Guard是win10中引入的新功能,据称能保护NTLM密码哈希值,Kerberos票证授予票证和应用程序存储的凭据。该进程是唯一能使用明文凭据的进程,它的原理大概是这样:
当NTLM认证过程中需要用到例如ntlm hash这类凭证的时候(第三步),将Credentials Guard视为黑箱,由lsass等进程输入生成NetNTLM所需的信息(第二步收到的challenge等等),由CG处理并输出结果,而CG本身内存禁止读取,使得mimikatz这一类工具无从下手:
对付Credentials Guard有一些曲线救国的方法:
SSP的二进制形式是DLL,提供用来处理身份认证的接口(SSPI)。如果我们无法从内存中直接获取凭据,那么通过注册一个ssp来处理用户登录时输入的凭据也是一种办法。mimikatz直接在内存中加载自定义的ssp dll,能够在用户登录时获取到明文凭据。
演示:
mimikatz 内存注入ssp
锁屏等待用户再次登录后,查看system32下的文件
NetNTLM有两个版本——v1和v2。v1相比v2更加脆弱,因此如果我们能将NetNTLMv2降级为v1,破解的效率会更高;如果能降级到NetLM,那么爆破成功率就变得极高。
在这篇文章中提到了一种迂回获取凭据的方式,核心思想是修改注册表使Windows允许在网络认证中发送算法较弱的NetHash例如NetNTLMv1,而实际上我们可以直接与NTLM SSP交互而不必产生网络流量,并能获取到NetNTLMv1用于降低破解的难度。
Credentials Guard运行时,我们虽然不能直接从内存获取到NTLM Hash,但利用上面提到的思路获取NetNTLM Hash是有可能的。
Internal-Monologue.ps1使用演示:
参考资料:
https://blog.nviso.be/2018/01/09/windows-credential-guard-mimikatz/
https://github.com/eladshamir/Internal-Monologue
https://technet.microsoft.com/en-us/library/2006.08.securitywatch.aspx
由于NTLM认证本身具有缺陷性,导致攻击者可以在不知道明文密码,只知道密码Hash的情况下完成认证。这个缺陷就目前来看无法修复,微软也只能建议使用更安全的kerberos协议来代替ntlm协议,而其发布的补丁也并没有触及PtH这种攻击思想的本质,仅仅是增大了攻击的难度或者粗暴地禁止NTLM认证,因此依然存在绕过的可能。
我在学习及研究过程中受到@360无线电安全研究院及@SycloverSecurity的大力支持与帮助,在此一并表示衷心的感谢。
]]>