译者:华为未然实验室
预估稿费:200RMB
投稿方式:发送邮件至linwei#360.cn,或登陆网页版在线投稿
前言
在上一篇文章中,我们在无需Office或升级到Windows 10 Pro的情况下实现了在Win10S中执行任意.NET代码。但这并没有实现我们的最终目标——在UMCI执行时运行我们想要运行的任何应用程序。我们可以利用任意代码执行来运行一些分析工具,以更好地了解Win10S,以便于对系统进行进一步的修改。
本文主要介绍如何在无需运行powershell.exe(被列入黑名单的众多应用程序之一)的情况下以尽可能简单的方式加载更复杂的.NET内容,包括恢复一个完整的PowerShell环境(在合理范围内)。
处理程序集加载
我们在上一篇文章中使用的简单的.NET程序集仅与系统内置程序集有依赖关系。这些系统程序集是操作系统附带的,使用Microsoft Windows Publisher证书进行签名。这意味系统完整性策略允许系统程序集作为镜像文件加载。当然,我们自己构建的任何东西都不会被允许从文件加载。
因为我们是从字节数组加载我们的程序集,所以SI策略不适用。对于仅具有系统依赖关系的简单程序集,这可能没有问题。但是,如果我们要加载引用其他不受信任的程序集的更复杂的程序集,则难度更大。由于.NET使用后期绑定,所以可能不会立即出现程序集加载问题,只有当尝试访问该程序集中的方法或类时,框架才尝试加载该程序集,这时才会出现异常。
当程序集加载时,框架会解析程序集名称。我们能不能从字节数组预加载一个依赖程序集,然后让加载程序在需要时加载它?我们不妨一试——先从一个字节数组加载一个程序集,然后按名称重新加载。如果预加载有效,则按名称加载应该会成功。编译以下简单的C#应用程序,其将从字节数组加载程序集,然后会尝试按全名再次加载程序集:
using System;
using System.IO;
using System.Reflection;
class Program
{
static void Main(string[] args)
{
try
{
Assembly asm = Assembly.Load(File.ReadAllBytes(args[0]));
Console.WriteLine(asm.FullName);
Console.WriteLine(Assembly.Load(asm.FullName));
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
}
现在运行该应用程序并将其路径传递给要加载的程序集(确保程序集在你编译上述代码的目录之外)。你应该会看到如下输出:
C:buildLoadAssemblyTest> LoadAssemblyTest.exe ..test.dll
test, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
Could not load file or assembly 'test, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' or one of its dependencies. The system cannot find the file specified.
我认为这样行不通,因为按名称加载程序集出现了异常。这是.NET从字节数组加载程序集这一方式的一个限制。被加载的程序集的名称未在任何全局程序集表中注册。这好坏参半,好处是这允许拥有相同名称的多个程序集共存在同一个进程中,坏处是这意味着如果我们不直接引用程序集实例,我们就不能访问该程序集中的任何东西。引用的程序集总是按名称加载,所以这意味着预先加载程序集无济于事,无助于使更复杂的程序集起作用。
.NET框架为该问题提供了一个解决方案,你可以指定一个Assembly Resolver事件处理程序。每当运行时无法从加载的程序集列表或磁盘上的文件找到程序集时,Assembly Resolver事件便被调用。程序集位于应用程序的基本目录之外时通常会发生这种情况。但是要注意,如果运行时找到符合其标准的磁盘上的文件,其将尝试加载该文件。如果该文件不在SI策略允许之列,则加载将失败,但是从解析角度而言,运行时不认为这是失败,因此在这种情况下运行时不会调用我们的事件处理程序。
名称传递给事件处理程序进行解析。该名称可以是部分名称,也可以是具有附加信息(如PublicKeyToken和版本)的完整程序集名称。因此,我们只将名称字符串传递给AssemblyName类,并使用它来提取程序集的名称,无需其他任何操作。然后,我们可以使用该名称来搜索具有该名称和DLL或EXE扩展名的文件。在我放在Github上的引导程序示例中,默认是在用户文档中的assembly目录中或在ASSEMBLY_PATH环境变量中指定的任意路径列表中进行搜索。最后,如果我们找到程序集文件,我们将从字节数组加载该文件,并将其返回给事件的调用者,确保缓存程序集文件以供以后查询。
AssemblyName name = new AssemblyName(args.Name);
string path = FindAssemblyPath(name.Name, ".exe") ??
FindAssemblyPath(name.Name, ".dll");
if (path != null) {
Assembly asm = Assembly.Load(File.ReadAllBytes(path));
_resolved_asms[args.Name] = asm;
_resolved_asms[asm.FullName] = asm;
}
else {
_resolved_asms[args.Name] = null;
}
引导程序代码中的最后一步是使用ExecuteAssemblyByName方法加载入口程序集。该入口程序集应包含一个称为startasm.exe并放置在搜索路径中的主入口点。你可以将所有分析代码放在一个引导程序程序集中,但其很快会变大,而且将序列化数据发送到AddInProcess命名管道效率不高。此外,通过启动一个新的可执行文件,可以在每次更改程序集时快速替换要运行的功能,而无需重新生成scriptlet。
注意,如果要加载的任何程序集包含原生代码(比如混合模式CIL和C++),则这样行不通。要运行原生代码,需要让程序集作为镜像加载,从字节数组加载行不通。当然,纯托管CIL可以很好地完成原生代码可以完成的一切,所以一定要以.NET编写你的工具。
引导PowerShell控制台
现在已经可以按名称执行任何.NET程序集(包括任何依赖程序集),接下来是要获得交互式运行环境。有什么交互式运行环境比PowerShell更好?PowerShell是用.NET编写的,因此powershell.exe也是一个.NET程序集。
我不这样认为,但值得注意的是Powershell ISE是一个完整的.NET程序集,所以我们可以加载它。但大多数情况下我更喜欢命令行版本。有不使用powershell.exe获得PowerShell的研究,但至少在我知道的例子中,例如https://github.com/p3nt4/PowerShdll,这样做并不容易。通常的方法是实现其自己的shell并将PS脚本从命令行传递到PowerShell运行空间。幸运的是,我们不必猜测powershell.exe的工作原理,我们可以对二进制文件进行逆向工程,可执行文件的核心现在是开源的。启动控制台的示例非托管代码请见此处。
将其简化为最简单的代码,本机入口点创建一个UnmanagedPSEntry类的实例,并调用Start方法。只要存在该进程的控制台,调用Start就会提供一个完全正常的PowerShell交互式环境。虽然AddInProcess是一个控制台应用程序,但必要时可以调用AllocConsole或AttachConsole创建一个新的控制台或附加到现有的控制台。我们甚至可以设置控制台标题和图标,这样我们会感觉是在运行完整的PowerShell。
AllocConsole();
SetConsoleTitle("Windows Powershell");
UnmanagedPSEntry ps = new UnmanagedPSEntry();
ps.Start(null, new string[0]);
我们已实现PowerShell运行,至少在开始使用控制台之前一切良好,开始使用控制台时可能遇到错误:
看来虽然我们成功绕过了镜像加载的UMCI检查,但PowerShell仍然尝试执行受限语言模式。这没有问题,我们所做的只是切断加载powershell.exe,不涉及PowerShell的其他UMCI锁定策略。检查要运行的模式是通过SystemPolicy类中的GetSystemLockdownPolicy方法进行的。这将调用Windows锁定策略DLL(WLDP)中的WldpGetLockdownPolicy函数来查询PowerShell的操作。通过传递null作为源路径,该函数返回一般系统策略。该函数也是检查单个文件的策略的入口点,通过将路径传递给签名脚本,可为脚本选择性执行策略。这是签名的Microsoft模块以Full Language模式运行而主shell可能以受限语言模式运行的方式。很明显,SystemPolicy类在缓存在私有systemLockdownPolicy静态字段中的策略查找的结果。因此,如果我们在调用任何其他PS代码之前使用反射将此值设置为SystemEnforcementMode.None,我们将禁用该锁定。
var fi = typeof(SystemPolicy).GetField("systemLockdownPolicy",
BindingFlags.NonPublic | BindingFlags.Static);
fi.SetValue(null, SystemEnforcementMode.None);
这样做可以获得我们所期望的没有锁定限制的PowerShell。
我将RunPowershell实现上传到了Github。构建可执行文件并将其复制到%USERPROFILEDocumentsassemblystartasm.exe,然后使用之前的DG绕过方式执行引导程序。
探索系统
PowerShell现已启动运行,我们现在可以对系统进行一些检测了。我确信我能做的第一件事是安装我的NtObjectManager模块。Install-Module cmdlet在尝试安装NuGet模块(在锁定策略下无法加载)时无法正常工作。不过你可以下载模块的文件,如果在引导程序程序集路径的列表中指定了模块的目录,你就可以导入PSD1文件,其应该会成功加载。
现在你可以自由发挥了。我在NtApiDotNet程序集中添加了几种方法来转储关于SI策略的系统信息。比如NtSystemInfo.CodeIntegrityOptions可转储当前启用了CI的flag,NtSystemInfo.CodeIntegrityFullPolicy是Windows Creator Update(大概是为支持Win10S)的新选项,其可以转储所有已配置的CI策略。在Win10S上运行时,有趣的是实际上有两个策略被执行,SI策略和某种撤销策略。通过以这种方式提取策略,我们应该能够确保我们获得系统正在执行的正确的策略信息,而不仅仅是我们认为是策略的文件。
最后,我添加了一个PowerShell cmdlet New-NtKernelCrashDump来创建一个内核崩溃转储(不要担心,它不会使系统崩溃),前提是你获得了SeDebugPrivilege,你可以通过以管理员身份运行AddInProcess获得。虽然这不允许你修改系统,但至少可让你探索内部数据结构来了解相关信息。当然,你需要将内核转储复制到另一个系统才能运行WinDBG。
总结
本文简要介绍了如何获得在Win10S中运行的更复杂的.NET内容。我主张尽可能以.NET编写你的分析工具,这样的话在锁定系统中运行要容易很多。当然,你可以使用反射性DLL加载程序,但既然.NET已经为你编写好了,又何必大费周章呢。