纯C#实现注入任意DLL(托管/非托管),无需额外DLL,AnyCPU编译支持x64/x86 win10/win8/win7/xp

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/wwh1004/article/details/79190596

FastWin32项目:github,欢迎fork欢迎star

  • 由来

网上流传的注入托管DLL说是纯C#,实际上还是调用了C++编写的DLL用于在远程进程中启动CLR。这样做会让被注入进程多一个已加载DLL,发布的程序也会带一个启动CLR的DLL(比如CLRInjection还带着LoadCLR2.dll和LoadCLR4.dll),看着很不舒服。所以我看了下MegaDumper.exe的注入方式,发现是手写机器码,所以我也手写机器码实现了,并且支持32位/64位,无CLR环境/有CLR环境,不同CLR版本的注入器(MegaDumper.exe是只支持32位且被注入进程必须存在CLRv2环境)。

  • 注入过程

注入器是这样注入托管程序集的:
先注入对应的mscoree.dll
再注入托管程序集

  • 过程分析

这2步都是难点,第一步的难点是如果判断DLL是否为托管程序集并且获取到DLL的目标CLR版本。也许有人会说用Assembly.Load*系列方法,但是很遗憾,这种方法稳定性极差,如果同时判断同名不同目标CLR版本程序集,第二次判断的结果将与第一次相同。举个例子,有2个同名程序集A和B,A的目标CLR版本是v2,B的目标CLR版本是v4,先用Assembly.Load*系列方法判断了A,获取了A的目标CLR版本,我再用Assembly.Load*系列方法判断B并获取B的目标CLR版本,返回的将是A的目标CLR版本。这是一个藏的很深的BUG,我也是很久才发现有这个问题。
我本打算用dnlib.dll直接读取程序集的目标CLR版本,但是dnlib.dll太大了,1MB有多,于是自己解析PE头

private struct Section
{
    public uint VirtualSize;

    public uint VirtualAddress;

    public uint SizeOfRawData;

    public uint PointerToRawData;

    public Section(uint virtualSize, uint virtualAddress, uint sizeOfRawData, uint pointerToRawData)
    {
        VirtualSize = virtualSize;
        VirtualAddress = virtualAddress;
        SizeOfRawData = sizeOfRawData;
        PointerToRawData = pointerToRawData;
    }
}

/// <summary>
/// 获取CLR版本
/// </summary>
/// <param name="binaryReader"></param>
/// <returns></returns>
private static string GetVersionString(BinaryReader binaryReader)
{
    uint ntHeaderOffset;
    bool is64;
    Section[] sections;
    uint rva;
    Section? section;

    GetPEInfo(binaryReader, out ntHeaderOffset, out is64);
    binaryReader.BaseStream.Position = ntHeaderOffset + (is64 ? 0xF8 : 0xE8);
    rva = binaryReader.ReadUInt32();
    //.Net MetaData Directory RVA
    if (rva == 0)
        throw new BadImageFormatException("文件不是程序集");
    sections = GetSections(binaryReader);
    section = GetSection(rva, sections);
    if (section == null)
        throw new InvalidDataException("未知格式的二进制文件");
    binaryReader.BaseStream.Position = section.Value.PointerToRawData + rva - section.Value.VirtualAddress + 0x8;
    //.Net MetaData Directory FileOffset
    rva = binaryReader.ReadUInt32();
    //.Net MetaData Header RVA
    if (rva == 0)
        throw new BadImageFormatException("文件不是程序集");
    section = GetSection(rva, sections);
    if (section == null)
        throw new InvalidDataException("未知格式的二进制文件");
    binaryReader.BaseStream.Position = section.Value.PointerToRawData + rva - section.Value.VirtualAddress + 0xC;
    //.Net MetaData Header FileOffset
    return Encoding.UTF8.GetString(binaryReader.ReadBytes(binaryReader.ReadInt32() - 2));
}

/// <summary>
/// 获取PE信息
/// </summary>
/// <param name="binaryReader"></param>
/// <param name="ntHeaderOffset"></param>
/// <param name="is64"></param>
private static void GetPEInfo(BinaryReader binaryReader, out uint ntHeaderOffset, out bool is64)
{
    ushort machine;

    binaryReader.BaseStream.Position = 0x3C;
    ntHeaderOffset = binaryReader.ReadUInt32();
    binaryReader.BaseStream.Position = ntHeaderOffset + 0x4;
    machine = binaryReader.ReadUInt16();
    if (machine != 0x14C && machine != 0x8664)
        throw new InvalidDataException("未知格式的二进制文件");
    is64 = machine == 0x8664;
}

/// <summary>
/// 获取节
/// </summary>
/// <param name="binaryReader"></param>
/// <returns></returns>
private static Section[] GetSections(BinaryReader binaryReader)
{
    uint ntHeaderOffset;
    bool is64;
    ushort numberOfSections;
    Section[] sections;

    GetPEInfo(binaryReader, out ntHeaderOffset, out is64);
    numberOfSections = binaryReader.ReadUInt16();
    binaryReader.BaseStream.Position = ntHeaderOffset + (is64 ? 0x108 : 0xF8);
    sections = new Section[numberOfSections];
    for (int i = 0; i < numberOfSections; i++)
    {
        binaryReader.BaseStream.Position += 0x8;
        sections[i] = new Section(binaryReader.ReadUInt32(), binaryReader.ReadUInt32(), binaryReader.ReadUInt32(), binaryReader.ReadUInt32());
        binaryReader.BaseStream.Position += 0x10;
    }
    return sections;
}

/// <summary>
/// 获取RVA对应节
/// </summary>
/// <param name="rva"></param>
/// <param name="sections"></param>
/// <returns></returns>
private static Section? GetSection(uint rva, Section[] sections)
{
    foreach (Section section in sections)
        if (rva >= section.VirtualAddress && rva < section.VirtualAddress + Math.Max(section.VirtualSize, section.SizeOfRawData))
            return section;
    return null;
}

第一个难点解决了,接下来看第二个难点,如何注入?全手写机器码意味着极大的工作量,为什么不用c++写好函数,提取出编译好的机器码呢?提取出来的机器码是固定了变量地址的,所以还要自己改。手改机器码肯定难,于是写了一段新代码用来帮助修改

public static class AsmCodeHelper
{
    public static bool Is64;

    public static void SetCode()
    {
        string codes;

        Assembler.NasmPath = "nasm.exe";
        Assembler.NdisasmPath = "ndisasm.exe";
        codes = string.Join(Environment.NewLine, BytesToCSharpCode(File.ReadAllText("strbytes.txt")));
        Clipboard.SetText(codes);
    }

    private static List<string> BytesToCSharpCode(string strBytes)
    {
        byte[] bytes;

        bytes = strBytes.Split(' ').Select(s => Convert.ToByte(s, 16)).ToArray();
        return BytesToCSharpCode(bytes);
    }

    private static List<string> BytesToCSharpCode(byte[] bytes)
    {
        IList<AsmData> asmDataList;
        List<string> codeList;

        File.WriteAllLines("opcodes.txt", Assembler.BytesToOpcodes(bytes, Is64).GetAllOpcodes());
        Process.Start(@"C:\Software\Notepad++\notepad++.exe", Path.GetFullPath("opcodes.txt"));
        Console.WriteLine("反编译完成,按任意键继续......");
        Console.ReadKey();
        asmDataList = Assembler.OpcodesToBytes(File.ReadAllLines("opcodes.txt"), Is64);
        codeList = new List<string>(asmDataList.Count * 8);
        foreach (AsmData asmData in asmDataList)
            codeList.AddRange(BytesToCSharpCodeSub(asmData.Bytes, asmData.Opcode));
        return codeList;
    }

    private static List<string> BytesToCSharpCodeSub(byte[] bytes, string opcode)
    {
        List<string> codeList;

        codeList = new List<string>(bytes.Length + 2);
        for (int i = 0; i < bytes.Length; i++)
            codeList.Add(string.Format("p[{0}] = 0x{1};", i.ToString(), bytes[i].ToString("X2")));
        codeList.Add(string.Format("p += {0};", bytes.Length.ToString()));
        codeList.Add(string.Format("//{0}", opcode));
        return codeList;
    }
}

利用ndisasm和nasm轻松反编译与编译,修改好后直接生成c#写机器码的代码。在改的时候会发现,CorBindToRuntimeEx和CLRCreateInstance函数的地址是不确定的,这又要自己获取。GetProcAddress只能获取当前进程的,并不能获取远程进程,所以这又需要我们熟悉PE结构。利用DLL的导出表,可以自己获取,代码如下:

/// <summary>
/// 获取模块句柄,获取失败时返回 <see cref="IntPtr.Zero"/>
/// </summary>
/// <param name="processHandle">进程句柄</param>
/// <param name="first">是否返回第一个模块句柄</param>
/// <param name="moduleName">模块名</param>
/// <param name="flag">过滤标识</param>
/// <returns></returns>
internal static unsafe IntPtr GetHandleInternal(IntPtr processHandle, bool first, string moduleName, uint flag)
{
    if (!first && string.IsNullOrEmpty(moduleName))
        throw new ArgumentNullException("first为false时moduleName不能为空");

    bool is64;
    bool isXP;
    IntPtr moduleHandle;
    uint size;
    IntPtr[] moduleHandles;
    StringBuilder moduleNameBuffer;

    if (processHandle == IntPtr.Zero)
        return IntPtr.Zero;
    if (!Process32.Is64ProcessInternal(processHandle, out is64))
        return IntPtr.Zero;
    if (is64 && !Environment.Is64BitProcess)
        throw new NotSupportedException("目标进程为64位但当前进程为32位");
    moduleHandle = IntPtr.Zero;
    isXP = Environment.OSVersion.Version.Major == 5;
    if (isXP)
    {
        //XP兼容
        if (!EnumProcessModules(processHandle, &moduleHandle, (uint)IntPtr.Size, out size))
            return IntPtr.Zero;
    }
    else
    {
        if (!EnumProcessModulesEx(processHandle, &moduleHandle, (uint)IntPtr.Size, out size, flag))
            //先获取储存所有模块句柄所需的字节数
            return IntPtr.Zero;
    }
    if (first)
        //返回第一个模块句柄
        return moduleHandle;
    moduleHandles = new IntPtr[size / IntPtr.Size];
    fixed (IntPtr* p = &moduleHandles[0])
    {
        if (isXP)
        {
            //XP兼容
            if (!EnumProcessModules(processHandle, p, size, out size))
                return IntPtr.Zero;
        }
        else
        {
            if (!EnumProcessModulesEx(processHandle, p, size, out size, flag))
                //获取所有模块句柄
                return IntPtr.Zero;
        }
    }
    moduleNameBuffer = new StringBuilder((int)MODULENAME_MAX_LENGTH);
    for (int i = 0; i < moduleHandles.Length; i++)
    {
        if (!GetModuleBaseName(processHandle, moduleHandles[i], moduleNameBuffer, MODULENAME_MAX_LENGTH))
            return IntPtr.Zero;
        if (moduleNameBuffer.ToString().Equals(moduleName, StringComparison.OrdinalIgnoreCase))
            return moduleHandles[i];
    }
    return IntPtr.Zero;
}

/// <summary>
/// 获取远程进程函数地址
/// </summary>
/// <param name="processHandle">进程句柄</param>
/// <param name="moduleName">模块名</param>
/// <param name="functionName">函数名</param>
/// <returns></returns>
internal static unsafe IntPtr GetProcAddressInternal(IntPtr processHandle, string moduleName, string functionName)
{
    IntPtr moduleHandle;
    int ntHeaderOffset;
    bool is64;
    int iedRVA;
    IMAGE_EXPORT_DIRECTORY ied;
    int[] nameOffsets;
    string name;
    short ordinal;
    int addressOffset;

    moduleHandle = GetHandleInternal(processHandle, false, moduleName, EnumModulesFilterFlag.ALL);
    if (moduleHandle == IntPtr.Zero)
        return IntPtr.Zero;
    if (!MemoryIO.ReadInt32Internal(processHandle, moduleHandle + 0x3C, out ntHeaderOffset))
        return IntPtr.Zero;
    if (!Process32.Is64ProcessInternal(processHandle, out is64))
        return IntPtr.Zero;
    if (is64)
    {
        if (!MemoryIO.ReadInt32Internal(processHandle, moduleHandle + ntHeaderOffset + 0x88, out iedRVA))
            return IntPtr.Zero;
    }
    else
    {
        if (!MemoryIO.ReadInt32Internal(processHandle, moduleHandle + ntHeaderOffset + 0x78, out iedRVA))
            return IntPtr.Zero;
    }
    if (!ReadProcessMemory(processHandle, moduleHandle + iedRVA, &ied, 40, null))
        return IntPtr.Zero;
    nameOffsets = new int[ied.NumberOfNames];
    fixed (void* p = &nameOffsets[0])
        if (!ReadProcessMemory(processHandle, moduleHandle + (int)ied.AddressOfNames, p, ied.NumberOfNames * 4, null))
            return IntPtr.Zero;
    for (int i = 0; i < ied.NumberOfNames; i++)
    {
        if (!MemoryIO.ReadStringInternal(processHandle, moduleHandle + nameOffsets[i], out name, 40, false, Encoding.ASCII))
            return IntPtr.Zero;
        if (name == functionName)
        {
            if (!MemoryIO.ReadInt16Internal(processHandle, moduleHandle + (int)ied.AddressOfNameOrdinals + i * 2, out ordinal))
                return IntPtr.Zero;
            if (!MemoryIO.ReadInt32Internal(processHandle, moduleHandle + (int)ied.AddressOfFunctions + ordinal * 4, out addressOffset))
                return IntPtr.Zero;
            return moduleHandle + addressOffset;
        }
    }
    return IntPtr.Zero;
}

这样我们解决了大部分难题。具体的代码可以回到文章开头点击FastWin32项目的github找到master/FastWin32/FastWin32/Diagnostics/Injector.cs。
注入进程的Demo可以在这里下载:http://download.csdn.net/download/wwh1004/10244677

展开阅读全文

没有更多推荐了,返回首页