吾爱破解 - LCG - LSG |安卓破解|病毒分析|破解软件|www.52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 2456|回复: 36

[.NET逆向] 【原理与实践篇】利用Mono.Cecil的Bug来Anti .NET Decompiler by Wwh / NCK

  [复制链接]
发表于 2018-8-6 17:17 | 显示全部楼层
之前发的那个帖子只算是讨论了一种现象,还没涉及到本质,也就是CLR加载过程。
所以我们现在来讨论一下CLR是如何处理压缩的元数据与未压缩的元数据的。

我们先来说说原理,这样才能理解接下来的实践。
首先我们需要CoreCLR源码一份,CLR底层架构是不会变的,所以研究元数据时用CoreCLR是比用SSCLI20好的。
如果没有源码也没关系,帖子用会把关键部分都贴出来。

几十万行的代码我们应该如何定位到我们需要的位置?
我们需要思考一下,CoreCLR源码几乎不会在传递参数的时候直接使用常量,而是使用了宏(#define)
所以我们利用这一点,先在解决方案中搜索"#Strings" "#US",找到了mdcommon.h这个文件
Snipaste_2018-08-06_15-54-39.png
接着查找所有引用,可以找到2处读取元数据流的地方,和1处判断元数据类型的地方

我们先分析下CLR如何判断元数据类型
Snipaste_2018-08-06_15-59-35.png
Snipaste_2018-08-06_16-00-16.png
我把代码化简了一下
[C++] 纯文本查看 复制代码
*pFormat = MDFormat_Invalid;
pStream = MDFormat::GetFirstStream_Verify(&sHdr, pData, &cbStreamBuffer);
// Loop through each stream and pick off the ones we need.
for (i = 0; i < sHdr.GetiStreams(); i++)
{
    // Get next stream.
    PSTORAGESTREAM pNext = pStream->NextStream_Verify();
    
    if (strcmp(pStream->GetName(), "#~") == 0)
    {
        // Validate that only one of compressed/uncompressed is present.
        if (*pFormat != MDFormat_Invalid)
            // Already found a good stream.
            goto ErrExit;
        // Found the compressed meta data stream.
        *pFormat = MDFormat_ReadOnly;
    }
    else if (strcmp(pStream->GetName(), "#-") == 0)
    {
        // Validate that only one of compressed/uncompressed is present.
        if (*pFormat != MDFormat_Invalid)
            // Already found a good stream.
            goto ErrExit;
        // Found the ENC meta data stream.
        *pFormat = MDFormat_ReadWrite;
    }
    else if (strcmp(pStream->GetName(), "#Schema") == 0)
    {
        // Found the uncompressed format
        *pFormat = MDFormat_ICR;
    }
    
    // Pick off the next stream if there is one.
    pStream = pNext;
}

这段代码是什么意思呢?
CLR会遍历所有元数据流头,
如果出现"#~",设置元数据格式为MDFormat_ReadOnly,
如果出现"#-",设置元数据格式为MDFormat_ReadWrite,
如果出现"#Schema",设置元数据格式为MDFormat_ICR,
同时还会检查"#~"和"#-"有没有重复出现过。

接下来,CLR判断元数据格式
Snipaste_2018-08-06_16-25-30.png
我化简的代码
[C++] 纯文本查看 复制代码
if ( format == MDFormat_ReadOnly )
{
    // Found a fully-compressed, read-only format.
    pInternalRO = new (nothrow) MDInternalRO;
    IfFailGo( pInternalRO->Init(const_cast<void*>(pData), cbData) );
}
else
{
    // Found a not-fully-compressed, ENC format.
    IfFailGo( GetInternalWithRWFormat( pData, cbData, flags, riid, ppIUnk ) );
}

意思是如果只存在"#~",那就是全压缩的元数据,
如果存在"#-"或"#Schema",那就是未完全压缩的元数据

接下来我们看看CLR如何加载压缩的元数据
Snipaste_2018-08-06_16-33-13.png
我化简的代码
[C++] 纯文本查看 复制代码
pStream = MDFormat::GetFirstStream_Verify(&sHdr, pData, &cbStreamBuffer);

// Loop through each stream and pick off the ones we need.
for (i = 0; i < sHdr.GetiStreams(); i++)
{
    void *pvCurrentData = (void *)((BYTE *)pData + pStream->GetOffset());
    ULONG cbCurrentData = pStream->GetSize();
    
    // Get next stream.
    PSTORAGESTREAM pNext = pStream->NextStream_Verify();
    
    // String pool.
    if (strcmp(pStream->GetName(), "#Strings") == 0)
    {
        // Initialize string heap with null-terminated block of data
        IfFailGo(m_MiniMd.m_StringHeap.Initialize(
            MetaData::DataBlob((BYTE *)pvCurrentData, cbCurrentData), 
            FALSE));        // fCopyData
    }
    
    // Literal String Blob pool.
    else if (strcmp(pStream->GetName(), "#US") == 0)
    {
        METADATATRACKER_ONLY(MetaDataTracker::NoteSection(TBL_COUNT + MDPoolUSBlobs, pvCurrentData, cbCurrentData, 1));
        // Initialize user string heap with block of data
        IfFailGo(m_MiniMd.m_UserStringHeap.Initialize(
            MetaData::DataBlob((BYTE *)pvCurrentData, cbCurrentData), 
            FALSE));        // fCopyData
    }
    
    // GUID pool.
    else if (strcmp(pStream->GetName(), "#GUID") == 0)
    {
        METADATATRACKER_ONLY(MetaDataTracker::NoteSection(TBL_COUNT + MDPoolGuids, pvCurrentData, cbCurrentData, 1));
        // Initialize guid heap with block of data
        IfFailGo(m_MiniMd.m_GuidHeap.Initialize(
            MetaData::DataBlob((BYTE *)pvCurrentData, cbCurrentData), 
            FALSE));        // fCopyData
    }
    
    // Blob pool.
    else if (strcmp(pStream->GetName(), "#Blob") == 0)
    {
        METADATATRACKER_ONLY(MetaDataTracker::NoteSection(TBL_COUNT + MDPoolBlobs, pvCurrentData, cbCurrentData, 1));
        // Initialize blob heap with block of data
        IfFailGo(m_MiniMd.m_BlobHeap.Initialize(
            MetaData::DataBlob((BYTE *)pvCurrentData, cbCurrentData), 
            FALSE));        // fCopyData
    }
    
    // Found the compressed meta data stream.
    else if (strcmp(pStream->GetName(), "#~") == 0)
    {
        IfFailGo( m_MiniMd.InitOnMem(pvCurrentData, cbCurrentData) );
        bFoundMd = true;
    }
    
    // Pick off the next stream if there is one.
    pStream = pNext;
    cbStreamBuffer = (ULONG)((LPBYTE)pData + cbData - (LPBYTE)pNext);}

意思是一切以最后一次发现的表和堆为准

然后我们看看CLR如何加载未压缩的元数据
Snipaste_2018-08-06_16-39-59.png
Snipaste_2018-08-06_16-49-32.png
我化简的代码
[C++] 纯文本查看 复制代码
// Load the string pool.
if (SUCCEEDED(hr = pStorage->OpenStream(L"#Strings", &cbData, &pvData)))
{
    IfFailGo(m_MiniMd.InitPoolOnMem(MDPoolStrings, pvData, cbData, bReadOnly));
}
else
{
    if (hr != STG_E_FILENOTFOUND)
    {
        IfFailGo(hr);
    }
    IfFailGo(m_MiniMd.InitPoolOnMem(MDPoolStrings, NULL, 0, bReadOnly));
}

// Load the user string blob pool.
if (SUCCEEDED(hr = pStorage->OpenStream(L"#US", &cbData, &pvData)))
{
    IfFailGo(m_MiniMd.InitPoolOnMem(MDPoolUSBlobs, pvData, cbData, bReadOnly));
}
else
{
    if (hr != STG_E_FILENOTFOUND)
    {
        IfFailGo(hr);
    }
    IfFailGo(m_MiniMd.InitPoolOnMem(MDPoolUSBlobs, NULL, 0, bReadOnly));
}

// Load the guid pool.
if (SUCCEEDED(hr = pStorage->OpenStream(L"#GUID", &cbData, &pvData)))
{
    IfFailGo(m_MiniMd.InitPoolOnMem(MDPoolGuids, pvData, cbData, bReadOnly));
}
else
{
    if (hr != STG_E_FILENOTFOUND)
    {
        IfFailGo(hr);
    }
    IfFailGo(m_MiniMd.InitPoolOnMem(MDPoolGuids, NULL, 0, bReadOnly));
}

// Load the blob pool.
if (SUCCEEDED(hr = pStorage->OpenStream(L"#Blob", &cbData, &pvData)))
{
    IfFailGo(m_MiniMd.InitPoolOnMem(MDPoolBlobs, pvData, cbData, bReadOnly));
}
else
{
    if (hr != STG_E_FILENOTFOUND)
    {
        IfFailGo(hr);
    }
    IfFailGo(m_MiniMd.InitPoolOnMem(MDPoolBlobs, NULL, 0, bReadOnly));
}

// Open the metadata.
hr = pStorage->OpenStream(L"#~", &cbData, &pvData);
if (hr == STG_E_FILENOTFOUND)
{
    IfFailGo(pStorage->OpenStream(ENC_MODEL_STREAM, &cbData, &pvData));
}

意思是从开始找到结束,以第一次出现的表和堆为准。
这里的OpenStream,判断字符串是忽略大小写的,可以看我上面发的图。
用的是stricmp。
dnlib写得和这些完全一样,该判断大小写不该判断大小写都写对了,不得不佩服0xd4d这位大神。

我们了解了CLR内部流程,再来看看如何自己为.NET程序集添加无效元数据让Mono.Cecil这个不严谨的类库出错
这里我们就需要dnlib了,因为dnlib可以正确区分。
Snipaste_2018-08-06_17-03-57.png
这个程序将作为本次演示的受害者
Snipaste_2018-08-06_17-06-12.png
[C#] 纯文本查看 复制代码
using dnlib.DotNet;
using dnlib.DotNet.Writer;

namespace ConsoleApp1 {
    internal unsafe class Program {
        private static void Main(string[] args) {
            using (ModuleDef moduleDef = ModuleDefMD.Load(@"E:\Projects\UnpackMe.Net\UnpackMe.Net\bin\Release\UnpackMe.Net.exe")) {
                ModuleWriter writer;

                writer = new ModuleWriter(moduleDef, new ModuleWriterOptions(moduleDef));
                writer.TheOptions.WriterEvent += TheOptions_WriterEvent;
                writer.Write("x.exe");
            }
        }

        private static void TheOptions_WriterEvent(object sender, ModuleWriterEventArgs e) {
            ModuleWriterBase writer = (ModuleWriterBase)sender;
            if (e.Event != ModuleWriterEvent.MDEndCreateTables)
                return;
            writer.TheOptions.MetadataOptions.CustomHeaps.Add(new InvalidHeap("#Strings"));
            writer.TheOptions.MetadataOptions.CustomHeaps.Add(new InvalidHeap("#US"));
            writer.TheOptions.MetadataOptions.CustomHeaps.Add(new InvalidHeap("#GUID"));
            writer.TheOptions.MetadataOptions.CustomHeaps.Add(new InvalidHeap("#Blob"));
            writer.TheOptions.MetadataOptions.CustomHeaps.Add(new InvalidHeap("#Schema"));
        }

        private sealed class InvalidHeap : HeapBase {
            private readonly string _name;

            public override string Name => _name;

            public InvalidHeap(string name) => _name = name;

            public override uint GetRawLength() => 1;

            protected override void WriteToImpl(DataWriter writer) => writer.WriteByte(0);
        }
    }
}

dnlib的设计有些奇怪,我们需要订阅模块写入事件,在写入事件中判断是否刚刚完成ModuleWriterEvent.MDEndCreateTables
如果是,我们添加上混淆用的堆,但不能是表(CLR有检测,上文已经说了),这里堆的名字可以是任意,不一定要是#Strings #US,比如#Wwh #NCK也是可以的,然后记得在这些堆里面再加上#Schema,这样CLR会认为这是未压缩的元数据。
我们用dnSpy反编译制作好的Anti Mono.Cecil的程序集看看
Snipaste_2018-08-06_17-11-36.png
正常反编译并且可以运行
而ILSpy,.NET Reflector,JustDecompiler就不一样了,完全无法反编译
Snipaste_2018-08-06_17-17-25.png

免费评分

参与人数 12吾爱币 +14 热心值 +11 收起 理由
ghost_kl + 1 + 1 我很赞同!
AngelEyes145 + 1 + 1 用心讨论,共获提升!
sunnylds7 + 1 + 1 热心回复!
qgy123 + 1 + 1 我很赞同!
zz0147 + 1 + 1 我很赞同!
jnez112358 + 1 + 1 谢谢@Thanks!
RoB1n_Ho0d + 1 我很赞同!
Ravey + 1 谢谢@Thanks!
limao555 + 1 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
ymb123 + 1 + 1 谢谢@Thanks!
liucq + 2 + 1 用心讨论,共获提升!
无痕软件 + 3 + 1 年少有为

查看全部评分

发帖求助前要善用论坛搜索功能,那里可能会有你要找的答案;

如果你在论坛求助问题,并且已经从坛友或者管理的回复中解决了问题,请把帖子分类或者标题加上【已解决】

如何回报帮助你解决问题的坛友,一个好办法就是给对方加【热心】,加分不会扣除自己的积分,做一个热心并受欢迎的人!

发表于 2018-8-6 17:28 | 显示全部楼层
怪不得我想汉化.NET Reflector,反编译了就打不开,还好找到方法汉化了,正在进行中

发帖求助前要善用论坛搜索功能,那里可能会有你要找的答案;

如果你在论坛求助问题,并且已经从坛友或者管理的回复中解决了问题,请把帖子分类或者标题加上【已解决】

如何回报帮助你解决问题的坛友,一个好办法就是给对方加【热心】,加分不会扣除自己的积分,做一个热心并受欢迎的人!

发表于 2018-8-6 17:33 | 显示全部楼层

发帖求助前要善用论坛搜索功能,那里可能会有你要找的答案;

如果你在论坛求助问题,并且已经从坛友或者管理的回复中解决了问题,请把帖子分类或者标题加上【已解决】

如何回报帮助你解决问题的坛友,一个好办法就是给对方加【热心】,加分不会扣除自己的积分,做一个热心并受欢迎的人!

发表于 2018-8-6 20:07 | 显示全部楼层

发帖求助前要善用论坛搜索功能,那里可能会有你要找的答案;

如果你在论坛求助问题,并且已经从坛友或者管理的回复中解决了问题,请把帖子分类或者标题加上【已解决】

如何回报帮助你解决问题的坛友,一个好办法就是给对方加【热心】,加分不会扣除自己的积分,做一个热心并受欢迎的人!

发表于 2018-8-6 23:54 | 显示全部楼层
支持大神分享。。。。。。。。。。

发帖求助前要善用论坛搜索功能,那里可能会有你要找的答案;

如果你在论坛求助问题,并且已经从坛友或者管理的回复中解决了问题,请把帖子分类或者标题加上【已解决】

如何回报帮助你解决问题的坛友,一个好办法就是给对方加【热心】,加分不会扣除自己的积分,做一个热心并受欢迎的人!

发表于 2018-8-7 08:58 | 显示全部楼层
感谢分享,学习了。

发帖求助前要善用论坛搜索功能,那里可能会有你要找的答案;

如果你在论坛求助问题,并且已经从坛友或者管理的回复中解决了问题,请把帖子分类或者标题加上【已解决】

如何回报帮助你解决问题的坛友,一个好办法就是给对方加【热心】,加分不会扣除自己的积分,做一个热心并受欢迎的人!

发表于 2018-8-7 09:43 | 显示全部楼层
感谢分享,学习了。

发帖求助前要善用论坛搜索功能,那里可能会有你要找的答案;

如果你在论坛求助问题,并且已经从坛友或者管理的回复中解决了问题,请把帖子分类或者标题加上【已解决】

如何回报帮助你解决问题的坛友,一个好办法就是给对方加【热心】,加分不会扣除自己的积分,做一个热心并受欢迎的人!

发表于 2018-8-7 11:04 | 显示全部楼层
学习学习,谢谢分享!

发帖求助前要善用论坛搜索功能,那里可能会有你要找的答案;

如果你在论坛求助问题,并且已经从坛友或者管理的回复中解决了问题,请把帖子分类或者标题加上【已解决】

如何回报帮助你解决问题的坛友,一个好办法就是给对方加【热心】,加分不会扣除自己的积分,做一个热心并受欢迎的人!

发表于 2018-8-7 11:15 | 显示全部楼层
多谢分享心得!

发帖求助前要善用论坛搜索功能,那里可能会有你要找的答案;

如果你在论坛求助问题,并且已经从坛友或者管理的回复中解决了问题,请把帖子分类或者标题加上【已解决】

如何回报帮助你解决问题的坛友,一个好办法就是给对方加【热心】,加分不会扣除自己的积分,做一个热心并受欢迎的人!

发表于 2018-8-7 13:22 | 显示全部楼层

感谢分享,学习了

发帖求助前要善用论坛搜索功能,那里可能会有你要找的答案;

如果你在论坛求助问题,并且已经从坛友或者管理的回复中解决了问题,请把帖子分类或者标题加上【已解决】

如何回报帮助你解决问题的坛友,一个好办法就是给对方加【热心】,加分不会扣除自己的积分,做一个热心并受欢迎的人!

您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则


免责声明:
吾爱破解所发布的一切破解补丁、注册机和注册信息及软件的解密分析文章仅限用于学习和研究目的;不得将上述内容用于商业或者非法用途,否则,一切后果请用户自负。本站信息来自网络,版权争议与本站无关。您必须在下载后的24个小时之内,从您的电脑中彻底删除上述内容。如果您喜欢该程序,请支持正版软件,购买注册,得到更好的正版服务。如有侵权请邮件与我们联系处理。

Mail To:Service@52PoJie.Cn

快速回复 收藏帖子 返回列表 搜索

RSS订阅|手机版|小黑屋|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2018-8-17 07:23

Powered by Discuz!

© 2001-2017 Comsenz Inc.

快速回复 返回顶部 返回列表