.NET 7 的 AOT 到底能不能扛反编译?
一:背景
1.讲故事
在B站,公众号上发了一篇 AOT 的文章后,没想到反响还是挺大的,都称赞这个东西能抗反编译,可以让破解难度极大提高,可能有很多朋友对逆向不了解,以为用 ILSpy
,Reflector
,DnSpy
这些工具打不开就觉得很安全,其实不然,在 OllyDbg
,IDA
,WinDBG
这些逆向工具面前一样是裸奔。
既然大家都很感兴趣,那这篇就和大家聊一聊。
二:几个例子
1. 动态修改程序数据
修改程序数据在逆向中再正常不过了,由于目前的 AOT 只能发布成 x64 ,这里就用 WinDbg
做下演示,首先看下例子。
internal class Program
{
static void Main(string[] args)
{
while (true)
{
Console.WriteLine("hello world!");
Thread.Sleep(1000);
}
}
}
程序在不断的输出,接下来我们将 hello world
中的 world
给去掉,操作手法非常简单,先内存搜索找到 hello world
,然后修改 length=5 即可。
0:005> lm
start end module name
00007ff7`95b70000 00007ff7`95e5d000 ConsoleApp1 C (private pdb symbols)
0:005> s-u 00007ff7`95b70000 L?0x00007ff7`95e5d000 hello
00007ff7`95e1c41c 0068 0065 006c 006c 006f 0020 0077 006f h.e.l.l.o. .w.o.
0:000> dp 00007ff795e1c41c-0x4 L1
00007ff7`95e1c418 00650068`0000000c
0:000> eq 00007ff7`95e1c418 00650068`00000005
0:000> g
2. 获取程序托管堆
AOT 再怎么牛,它终还是个托管程序,既然是托管程序自然就有托管堆,托管堆中就有所有的托管数据,玩过 SOS.dll
朋友应该知道,用 !eeheap -gc
就能把托管堆给显示出来,比如下面这样。
0:022> !eeheap -gc
Number of GC Heaps: 1
generation 0 starts at 0x000002414D891030
generation 1 starts at 0x000002414D891018
generation 2 starts at 0x000002414D891000
ephemeral segment allocation context: none
segment begin allocated committed allocated size committed size
000002414D890000 000002414D891000 000002414D8D1FE8 000002414D8D2000 0x40fe8(266216) 0x41000(266240)
Large object heap starts at 0x000002415D891000
segment begin allocated committed allocated size committed size
000002415D890000 000002415D891000 000002415D891018 000002415D892000 0x18(24) 0x1000(4096)
Pinned object heap starts at 0x0000024165891000
0000024165890000 0000024165891000 0000024165899C10 00000241658A2000 0x8c10(35856) 0x11000(69632)
Total Allocated Size: Size: 0x49c10 (302096) bytes.
Total Committed Size: Size: 0x42000 (270336) bytes.
------------------------------
GC Allocated Heap Size: Size: 0x49c10 (302096) bytes.
GC Committed Heap Size: Size: 0x42000 (270336) bytes.
虽然目前的 AOT 不支持 SOS 扩展,无法显示出托管堆,但一点关系都没有,SOS 是通过 DataAccess 去挖的,大不来我手工挖一下就好了哈,接下来就是怎么挖的问题了,熟悉 CLR 的朋友应该知道所谓的托管堆在内部用的是 generation_table[]
一维数据来维护的,以 代
的方式来划分,代的落地是用 heap_segment
来表示的, 参考代码如下:
generation gc_heap::generation_table [total_generation_count];
enum gc_generation_num
{
// small object heap includes generations [0-2], which are "generations" in the general sense.
soh_gen0 = 0,
soh_gen1 = 1,
soh_gen2 = 2,
max_generation = soh_gen2,
// large object heap, technically not a generation, but it is convenient to represent it as such
loh_generation = 3,
// pinned heap, a separate generation for the same reasons as loh
poh_generation = 4,
uoh_start_generation = loh_generation,
// number of ephemeral generations
ephemeral_generation_count = max_generation,
// number of all generations
total_generation_count = poh_generation + 1
};
接下来用 x 命令看下数组内容,代码如下:
0:000> x ConsoleApp1!WKS::gc_heap::generation_table
00007ff7`95e25010 ConsoleApp1!WKS::gc_heap::generation_table = class WKS::generation [5]
0:000> dx -r1 (*((ConsoleApp1!WKS::generation (*)[5])0x7ff795e25010))
(*((ConsoleApp1!WKS::generation (*)[5])0x7ff795e25010)) [Type: WKS::generation [5]]
[0] [Type: WKS::generation]
...
[4] [Type: WKS::generation]
0:000> dx -r1 (*((ConsoleApp1!WKS::generation *)0x7ff795e25010))
(*((ConsoleApp1!WKS::generation *)0x7ff795e25010)) [Type: WKS::generation]
[+0x038] start_segment : 0x25100000000 [Type: WKS::heap_segment *]
[+0x040] allocation_start : 0x25100001030 : 0x38 [Type: unsigned char *]
[+0x048] allocation_segment : 0x25100000000 [Type: WKS::heap_segment *]
[+0x0d0] allocation_size : 0x0 [Type: unsigned __int64]
[+0x100] gen_num : 0 [Type: int]
...
0:000> dx -r1 ((ConsoleApp1!WKS::heap_segment *)0x25100000000)
((ConsoleApp1!WKS::heap_segment *)0x25100000000) : 0x25100000000 [Type: WKS::heap_segment *]
[+0x000] allocated : 0x25100001048 : 0x90 [Type: unsigned char *]
[+0x008] committed : 0x25100012000 : Unable to read memory at Address 0x25100012000 [Type: unsigned char *]
[+0x010] reserved : 0x25110000000 : 0x18 [Type: unsigned char *]
[+0x018] used : 0x25100009fe0 : 0x0 [Type: unsigned char *]
[+0x020] mem : 0x25100001000 : 0x38 [Type: unsigned char *]
[+0x028] flags : 0x0 [Type: unsigned __int64]
[+0x030] next : 0x0 [Type: WKS::heap_segment *]
...
上面的这些字段就描述出了 !eeheap -gc
的结果,接下来想挖什么,提取什么我就不过多介绍了。
3. 提取托管线程列表
提取 托管线程
列表也是非常重要的, 它能指示出很多信息,一般用 !t
命令就能显示,输出如下:
0:022> !t
ThreadCount: 17
UnstartedThread: 0
BackgroundThread: 6
PendingThread: 0
DeadThread: 0
Hosted Runtime: no
Lock
DBG ID OSID ThreadOBJ State GC Mode GC Alloc Context Domain Count Apt Exception
0 1 4128 000002414BDB8C70 2a020 Preemptive 000002414D8C6108:000002414D8C8000 000002414bdaf8f0 -00001 MTA
6 2 4458 000002414BDE5EB0 2b220 Preemptive 0000000000000000:0000000000000000 000002414bdaf8f0 -00001 MTA (Finalizer)
7 4 23e8 000002416DDB15C0 102b220 Preemptive 000002414D8C9250:000002414D8CA000 000002414bdaf8f0 -00001 MTA (Threadpool Worker)
...
20 17 50a8 000002416DE43DD0 102b220 Preemptive 000002414D8BC2D0:000002414D8BDFD0 000002414bdaf8f0 -00001 MTA (Threadpool Worker)
21 18 57d4 000002416DE628E0 8029220 Preemptive 000002414D8CC2A8:000002414D8CE000 000002414bdaf8f0 -00001 MTA (Threadpool Completion Port)
既然目前的 SOS 不支持,同样可以手工到 CLR 中去挖,熟悉的朋友应该知道 !t
的数据源来自于 ThreadStore::s_pThreadStore
下的 m_ThreadList
集合,它以链表的形式串联了每个线程的 LinkPtr
字段,但可惜的是,在 AOT 中,这一块已经重写了,由 g_pTheRuntimeInstance
全局变量下的 m_ThreadList
来维护了。
为了方便观察,多生成几个 Thread。
static void Main(string[] args)
{
Debugger.Break();
var tasks = Enumerable.Range(0, 10).Select(m => new Thread(() =>
{
Console.WriteLine($"tid={Thread.CurrentThread.ManagedThreadId} 已执行!");
Console.ReadLine();
}));
foreach (var item in tasks)
{
item.Start();
}
Console.ReadLine();
}
程序跑起来后,深挖 g_pTheRuntimeInstance
全局变量即可。
0:015> x ConsoleApp1!g_pTheRuntimeInstance
00007ff7`0155ee20 ConsoleApp1!g_pTheRuntimeInstance = 0x00000291`cb5b9300
0:015> dx -r1 ((ConsoleApp1!RuntimeInstance *)0x291cb5b9300)
((ConsoleApp1!RuntimeInstance *)0x291cb5b9300) : 0x291cb5b9300 [Type: RuntimeInstance *]
[+0x000] m_pThreadStore : 0x291cb5b9390 [Type: ThreadStore *]
...
0:015> dx -r1 ((ConsoleApp1!ThreadStore *)0x291cb5b9390)
((ConsoleApp1!ThreadStore *)0x291cb5b9390) : 0x291cb5b9390 [Type: ThreadStore *]
[+0x000] m_ThreadList [Type: SList<Thread,DefaultSListTraits<Thread,DoNothingFailFastPolicy> >]
[+0x008] m_pRuntimeInstance : 0x291cb5b9300 [Type: RuntimeInstance *]
[+0x010] m_Lock [Type: ReaderWriterLock]
0:015> dx -r1 (*((ConsoleApp1!SList<Thread,DefaultSListTraits<Thread,DoNothingFailFastPolicy> > *)0x291cb5b9390))
(*((ConsoleApp1!SList<Thread,DefaultSListTraits<Thread,DoNothingFailFastPolicy> > *)0x291cb5b9390)) [Type: SList<Thread,DefaultSListTraits<Thread,DoNothingFailFastPolicy> >]
[+0x000] m_pHead : 0x291ed366240 [Type: Thread *]
0:015> dx -r1 ((ConsoleApp1!Thread *)0x291ed366240)
((ConsoleApp1!Thread *)0x291ed366240) : 0x291ed366240 [Type: Thread *]
...
[+0x058] m_pNext : 0x291cb6aeb60 [Type: Thread *]
[+0x060] m_hPalThread : 0x204 [Type: void *]
[+0x068] m_ppvHijackedReturnAddressLocation : 0x0 [Type: void * *]
[+0x070] m_pvHijackedReturnAddress : 0x0 [Type: void *]
[+0x078] m_uHijackedReturnValueFlags : 0x0 [Type: unsigned __int64]
[+0x080] m_pExInfoStackHead : 0x0 [Type: ExInfo *]
[+0x088] m_threadAbortException : 0x0 [Type: Object *]
[+0x090] m_pThreadLocalModuleStatics : 0x291cb6aee90 [Type: void * *]
[+0x098] m_numThreadLocalModuleStatics : 0x1 [Type: unsigned int]
[+0x0a0] m_pGCFrameRegistrations : 0x0 [Type: GCFrameRegistration *]
[+0x0a8] m_pStackLow : 0xf754100000 [Type: void *]
[+0x0b0] m_pStackHigh : 0xf754200000 [Type: void *]
[+0x0b8] m_pTEB : 0xf7533ba000 : 0x0 [Type: unsigned char *]
[+0x0c0] m_uPalThreadIdForLogging : 0x2044 [Type: unsigned __int64]
[+0x0c8] m_threadId [Type: EEThreadId]
[+0x0d0] m_pThreadStressLog : 0x0 [Type: void *]
[+0x0d8] m_interruptedContext : 0x0 [Type: _CONTEXT *]
[+0x0e0] m_redirectionContextBuffer : 0x0 [Type: unsigned char *]
0:015> dx -r1 (*((ConsoleApp1!EEThreadId *)0x291ed366308))
(*((ConsoleApp1!EEThreadId *)0x291ed366308)) [Type: EEThreadId]
[+0x000] m_uiId : 0x2044 [Type: unsigned __int64]
从CLR 的 Thread 维护的信息来看,这个结构体已经很小了,也说明 AOT 在Thread信息维护上做了很多的精简。
三:总结
总的来说,AOT 确实能加速程序的初始启动,一体化的打包机制也非常方便部署,但怎么变终究还是一个托管程序,需要底层的 C++ 托着它,扛 反编译 无从谈起,所以防小人的话,该加壳的加壳,该混淆的混淆。
.NET 7 的 AOT 到底能不能扛反编译?的更多相关文章
- 反编译C#代码来看看闭包到底是什么
原文地址:https://zhuanlan.zhihu.com/p/3161634 C#的闭包,是一个语法糖. 它实质上是将匿名函数转换成一个类,函数作为其中的类方法,并调整外部调用代码来实现的.既然 ...
- sqlserver 存储过程中使用临时表到底会不会导致重编译
曾经在网络上看到过一种说法,SqlServer的存储过程中使用临时表,会导致重编译,以至于执行计划无法重用, 运行时候会导致重编译的这么一个说法,自己私底下去做测试的时候,根据profile的跟踪结果 ...
- Integer类的装箱和拆箱到底是怎样实现的?
先解释一下装箱和拆箱: 装箱就是 自动将基本数据类型转换为包装器类型:拆箱就是 自动将包装器类型转换为基本数据类型. 下表是基本数据类型对应的包装器类型: int(4字节) Integer byt ...
- Java finally语句到底是在return之前还是之后执行(JVM字节码分析及内部体系结构)?
之前看了一篇关于"Java finally语句到底是在return之前还是之后执行?"这样的博客,看到兴致处,突然博客里的一个测试用例让我产生了疑惑. 测试用例如下: public ...
- Unity的JIT和AOT实现
https://myhloli.com/about-il2cpp.html JIT方式: Unity的跨平台技术是通过一个Mono虚拟机实现的.而这个虚拟机更新太慢,不能很好地适应众多的平台. And ...
- [你必须知道的.NET]第二十七回:interface到底继承于object吗?
发布日期:2009.03.05 作者:Anytao © 2009 Anytao.com ,Anytao原创作品,转贴请注明作者和出处. 说在,开篇之前 在.NET世界里,我们常常听到的一句话莫过于“S ...
- JIL 编译与 AOT 编译
JIT:Just-in-time compilation,即时编译:AOT:Ahead-of-time compilation,事前编译. JVM即时编译(JIT) 1. 动态编译与静态编译 动态编译 ...
- ng build --aot 与 ng build --prod
angluar的编译有以下几种方式: ng build 常规的压缩操作 代码体积最大 ng build --aot angular预编译 代码体积较小 ng build --pr ...
- async/await到底该怎么用?如何理解多线程与异步之间的关系?
前言 如标题所诉,本文主要是解决是什么,怎么用的问题,然后会说明为什么这么用.因为我发现很多萌新都会对之类的问题产生疑惑,包括我最初的我,网络上的博客大多知识零散,刚开始看相关博文的时候,就这样.然后 ...
- String s="a"+"b"+"c",到底创建了几个对象?
首先看一下这道常见的面试题,下面代码中,会创建几个字符串对象? String s="a"+"b"+"c"; 如果你比较一下Java源代码和反 ...
随机推荐
- Win32简单图形界面程序逆向
Win32简单图形界面程序逆向 前言 为了了解与学习底层知识,从 汇编开始 -> C语言 -> C++ -> PE文件 ,直至今天的Win32 API,着实学的令我头皮发麻(笑哭). ...
- thinkphp5.1中适配百度富文本编辑器ueditor
百度富文本编辑器ueditor虽然很老,但是功能齐全,我近期需要能批量粘贴图片的功能,但是找不到,很无奈.然后现在就分享一下如何把ueditor适配到thinkphp5.1,有知道如何批量上传图片的艾 ...
- 官方文档采用Docker方式安装
官方文档地址:https://github.com/grafana/loki/tree/master/production The Docker images for Loki and Promtai ...
- js基础知识--BOM
之前说过,在js的 运行环境为浏览器时,js就主要有三部分组成: ECMAScript核心语法.BOM.DOM.今天就和大家详细说一下BOM的一些基础知识. BOM BOM通常被称为浏览器对象模型,主 ...
- vue项目使用.env文件配置全局环境变量
一.env文件的认识: (1).env 文件主要的作用是存储环境变量,也就是会随着环境变化的东西,比如数据库的用户名.密码.缓存驱动.时区,还有静态文件的存储路径之类的.因为这些信息应该是和环境绑定的 ...
- 聊聊Linux中CPU上下文切换
目录 什么是CPU上下文 CPU上下文切换 上一任务的CPU上下文保存在哪? 进程上下文切换 内核空间和用户空间 top命令查看CPU资源 系统调用 进程上下文切换 和 系统调用的区别? 进程切换的常 ...
- 洛谷P4630 [APIO2018] Duathlon 铁人两项 (圆方树)
圆方树大致理解:将每个点双看做一个新建的点(方点),该点双内的所有点(圆点)都向新建的点连边,最后形成一棵树,可以给点赋予点权,用以解决相关路径问题. 在本题中,方点点权赋值为该点双的大小,因为两个点 ...
- 洛谷P2827 [NOIP2016 提高组] 蚯蚓 (二叉堆/队列)
容易想到的是用二叉堆来解决,切断一条蚯蚓,其他的都要加上一个值,不妨用一个表示偏移量的delta. 1.取出最大的x,x+=delta: 2.算出切断后的两个新长度,都减去delta和q: 3.del ...
- Sun 的 BASE64Encoder替代
可以使用 org.apache.commons.codec.binary.Base64替代 Maven依赖如下 <dependency> <groupId>commons-co ...
- ifram父页面、子页面元素及方法的获取调用
page1 父页面 <div id="ifram" class="parent1"> <iframe frameborder="0& ...