GUN/Linux通用Glibc库是如何操控.Net 7的CLR

楔子

今天是北方小年,祝北方的朋友小年快乐。

最新的.Net 7 是如何被Linux(Ubuntu22.04)加载的,运行微软程序的呢?本篇看下它的Glibc库里面的运作模式。

Glibc

Glibc是套C语言运行库,GUN/Linux系统最底层的库,几乎所有的Linux函数运行都需要靠它---来自百科。
本篇主要用到,libc_start_call_main.h和libc-start.c两个库文件。.

原理

CLR在Ubuntu上运行的最重要的一点就是通过Glibc来加载./corerun命令的参数abc.dll操纵的。它通过调用Glibc的入口_start函数,一系列运作调用CLR的main函数开始进入运行时。

调看

先上lldb,同样在PreStubWorker处打打个断点
GUN/Linux通用Glibc库是如何操控.Net 7的CLR
运行到此函数
GUN/Linux通用Glibc库是如何操控.Net 7的CLR
查看下堆栈
GUN/Linux通用Glibc库是如何操控.Net 7的CLR
注意12,11,10三个栈,此三栈是调用CLR的前奏函数
12帧:_start
11帧:__libc_start_main_impl(libc-start.c里面的函数)
10帧:__libc_start_call_main(libc_start_call_main.h里的函数)
9帧及其以后都是CLR范畴了。

问题

b在_start上下个断点
GUN/Linux通用Glibc库是如何操控.Net 7的CLR
把PreStubWorker断点删掉,因为会干扰后面的调试
GUN/Linux通用Glibc库是如何操控.Net 7的CLR
c让整个程序运行完成
GUN/Linux通用Glibc库是如何操控.Net 7的CLR
然后r再把程序运行一遍,断点会停在最首先调用CLR的地方,也就是上面的_start函数
GUN/Linux通用Glibc库是如何操控.Net 7的CLR
可以看到它是一个汇编代码写成的函数,查看下它的函数体
GUN/Linux通用Glibc库是如何操控.Net 7的CLR
在地址0x55555556bc8f处call了一个地址,在此处下一个断点。C运行到此处。
GUN/Linux通用Glibc库是如何操控.Net 7的CLR
查看下call到底调用了哪个地址
GUN/Linux通用Glibc库是如何操控.Net 7的CLR
可以看到call的地址是0x0000555555583f1a,查看下这个地址的内存和汇编
GUN/Linux通用Glibc库是如何操控.Net 7的CLR
可以看到内存不是一个地址,而汇编则是未被编译的机器码。这到底咋回事呢?

分析

si单步进入上面的call指令
GUN/Linux通用Glibc库是如何操控.Net 7的CLR
发现call指令跳转的地址是0x00007ffff7a7bdc0。
而上面0x0000555555583f1a地址内存如下
GUN/Linux通用Glibc库是如何操控.Net 7的CLR
对照下可以发现需要把0x0000555555583f1a+6=0x0000555555583f20这个地址才能完整的跳转到0x00007ffff7a7bdc0这个地址,才能准确的调到堆栈的__libc_start_main_impl函数。

那么这个多余的6个字节哪里来的呢?

这里需要注意下__libc_start_main_impl这个函数有7个参数,所以在Ubuntu22.04上面传参的顺序是:rdi, rsi, rdx, rcx, r8, r9。
这里可以一一对照下。
GUN/Linux通用Glibc库是如何操控.Net 7的CLR
rdi就是__libc_start_main_impl函数的一个参数,rsi就是第二个argc=2以此类推。

返回问题所在,重新运行到call指令处,查看下这个_start函数原型
GUN/Linux通用Glibc库是如何操控.Net 7的CLR
call的机器码是ff 15,后面的01828b是相对位置,call的下一条指令地址是0x55555556bc95两者相加,0x55555556bc95+0x01828b=0x555555583F20,查看下这个地址内存
GUN/Linux通用Glibc库是如何操控.Net 7的CLR
刚好是0x00007ffff7a7bdc0,也就是__libc_start_main_impl函数的地址。

最后看下另外两个函数调用,在PreStubWorker下断点。重新运行,查看堆栈。
GUN/Linux通用Glibc库是如何操控.Net 7的CLR
当运行到了Glibc里面的__libc_start_main_impl函数之后,反汇编__libc_start_main_impl函数
GUN/Linux通用Glibc库是如何操控.Net 7的CLR
查看当前堆栈
GUN/Linux通用Glibc库是如何操控.Net 7的CLR
可以看到帧11也就是__libc_start_main_impl函数所在的帧,的地址是0x00007ffff7a7be40,转到这个地址
GUN/Linux通用Glibc库是如何操控.Net 7的CLR
这个地址的上一个地址里面所包含的指令调用了一个call,这个call跳转的刚好是__libc_start_call_main。继续看下__libc_start_call_main函数
GUN/Linux通用Glibc库是如何操控.Net 7的CLR
由于__libc_start_call_main在帧10,它的地址是0x00007ffff7a7bd90
GUN/Linux通用Glibc库是如何操控.Net 7的CLR
此处看下它的地址处汇编
GUN/Linux通用Glibc库是如何操控.Net 7的CLR
可以看到帧10地址的上一条地址调用了call rax,那么rax应该就是corerun.main也就是CLR的入口地址。如何证明呢?
在帧10的上一条地址0x7ffff7a7bd8e处下个断点。然后删掉其它断点避免干扰,r命令再次运行到0x7ffff7a7bd8e处。
GUN/Linux通用Glibc库是如何操控.Net 7的CLR
读取下rax的值
GUN/Linux通用Glibc库是如何操控.Net 7的CLR
可以看到它是corerun.main函数的地址无疑了

那么至此为止,Glibc操控CLR的所有关节已打通。如果说缺点什么,那么就是如ELF这个文件结构了。

总结

一:_start函数
看下_start的第一条指令

0x55555556bc70 <+0>:  f3 0f 1e fa           endbr64

这条指令是预测指令,防止恶意修改。
_start最后一条指令

0x55555556bc95 <+37>: f4                    hlt 

这条指令是当前机器静默,也就是啥都不做,等待苏醒。
那么问题其实很清楚了,call的跳转不是后面带的立即数的地址,而是相对地址加上下一条指令的地址。才是跳转的真正地址。所以才造成了差6个字节的假象。

二:frame帧
它的地址主要存储了call指令下一地址。帧上附带有参数,以及函数调用所在的行列。

三:注意命令:
di -f -mb 反汇编当前断点所在的函数
di -bn __libc_start_main_impl 通过函数名称反汇编
di -s 0x0001 -c 24 通过指定的地址-x0001反汇编
di -l 反汇编当前行