Linux内核实现PE加载器——Longene源码分析

Longene是一个源自中国的自由、开源的操作系统项目,目的是要把Linux的内核扩充成一个既支持Linux应用、也支持Windows应用,既支持Linux设备驱动、也支持Windows设备驱动的兼容内核;使用户可以直接在Linux操作系统上高效运行Windows应用。2006年发布了其第一个版本,但是自2014年以来,项目开发已停止,2018年5月其官网无法访问。这里就不多做介绍了,直接开始从源码角度分析Longene是怎么在Linux内核中实现PE加载器的。

Longene中PE可执行文件的加载以及动态链接的过程主要在/module/ke/文件夹中的代码实现。

Longene采用内核模块的形式,在内核中注册了pe格式的文件。(之前有分析过了如何扩展可支持的文件格式),该结构体为pe_format。

Linux内核实现PE加载器——Longene源码分析

当执行某exe文件,例如在命令行里面敲入 ./notepad.exe的时候,首先linux内核调用execve系统调用最终走到会走到longene兼容模块的load_pe_bianry函数中(这也是前面分析过的)。

进入load_pe_binary函数,首先判断父进程是否是win32程序。

Linux内核实现PE加载器——Longene源码分析

接下来就是一堆常规的操作,比如预留进程地址空间,将二进制文件映射至内存,调整堆栈大小,为解释器预留空间等。

Linux内核实现PE加载器——Longene源码分析

然后是一个重要的步骤,搜索ntdll.dll.so,也就是wine代码生成的builtin dll。在$ PATH中搜索ntdll.dll.so,默认是/usr/local/lib/wine/ntdll.dll.so。找到后才可以执行下面的代码。

Linux内核实现PE加载器——Longene源码分析

search_ntdll函数如下图所示:

Linux内核实现PE加载器——Longene源码分析

调用函数map_system_dll映射ntdll.dll.so。该函数在sysdll.c中定义,先通过load_elf_interp函数将ntdll.dll.so装入,该函数主体部分基本是参考linux内核源码中ELF文件加载启动的那部分代码(binfmt_elf.c)

Linux内核实现PE加载器——Longene源码分析

接着回到map_system_dll函数,装入ntdll.dll.so后,通过多次调用uk_find_symbol函数来获得ntdll.dll.so中各个函数的入口地址,其中,将ntdll_entry设置为LdrInitializeThunk函数的地址,该函数的作用之前有分析过,就是用来为PE可执行文件加载dll的;将pe_entry设置为ProcessStartForward函数的地址,该函数是一个转发函数,相当于直接调用kernel32.dll.so中的BaseProcessStart函数。

Linux内核实现PE加载器——Longene源码分析

回到map_system_dll函数继续执行。再次调用load_elf_interp,这次是将解释器也就是ld-linux.so装入,并将地址赋值给interp_entry

Linux内核实现PE加载器——Longene源码分析

到这里,map_system_dll执行完,回到load_pe_binary函数。下面先创建EPROCESS结构体,初始化eprocess和kprocess,创建PEB,PPB等,设置TEB,初始化KThreaad等。

Linux内核实现PE加载器——Longene源码分析

如果父进程是win32程序,也就是is_win32为true(之前有判断),先等待父进程执行完。

Linux内核实现PE加载器——Longene源码分析

然后是很关键的一步。将ntdll.dll.so中的函数LdrInitializeThunk()作为APC函数执行完成PE格式DLL映像的装入和动态连接。

Longene在Linux内核实现了WindowsAPC机制,APC就是“异步过程调用(AsyncroneusProcedure Call)”的缩写,在module/ke/apc.c中实现,看了下,这里的实现参考了reactOS的代码。简单来说,通过在Linux系统上使用Signal机制,可以实现进程创建过程中的异步过程调用(APC),从而可以模拟APC机制来实现装入与动态连接WindowsDLL的过程,为进程运行做好准备。这里先不对APC机制的具体实现进行分析了。

接着看代码。首先调用apc_init对APC进行初始化,将ntdll_entry作为参数传递,也就是将LdrInitializeThunk()函数(前面有分析,ntdll_entry赋值为该函数的地址了)作为APC的处理函数。然后调用insert_queue_apc将APC请求加入APC队列,将interp_entry作为参数传递。

Linux内核实现PE加载器——Longene源码分析

最后就是调用start_thread并从系统调用中返回用户空间,当然这时进程还没有真正开始执行,因为APC机制,还需要完成动态链接的过程后,应用程序代码才开始执行。

Linux内核实现PE加载器——Longene源码分析

在execve系统调用返回的时候会从win32syscall_exit返回,这是一段汇编代码,在module/ke/win32entry.S中

Linux内核实现PE加载器——Longene源码分析

Linux内核实现PE加载器——Longene源码分析

这里代码没有贴全,因为全是汇编,就不具体介绍了,只需要知道经过一些跳转,最终会执行win32apc这段汇编代码中,并调用do_apc函数,如下图所示:

Linux内核实现PE加载器——Longene源码分析

也就是说,其实在execve系统调用返回的时候会调用do_apc,该函数在module/ke/apc.c中。

Linux内核实现PE加载器——Longene源码分析

do_apc函数调用了deliver_apc函数,该函数的作用就是取出APC队列中的APC处理函数,并逐个调用。首先处理内核APC,然后再处理用户空间APC

比如,之前讲到LdrInitializeThunk()作为用户APC函数添加进队列了。

 

首先处理内核APC

Linux内核实现PE加载器——Longene源码分析

 

然后处理用户空间APC

Linux内核实现PE加载器——Longene源码分析

处理用户空间的APC首先是调用了init_user_apc函数

Linux内核实现PE加载器——Longene源码分析

该函数倒数第二行代码 TrapFrame->ip =get_apc_dispatcher();

作用就是当返回用户空间时,首先执行的第一个函数将会是 KiUserApcDispatcher,每个APC都会通过此函数来调用。该函数在wine/dlls/ntdll/init.c中。

这里需要特别关注的是这个函数的参数,这些参数都在load_pe_binary中设定的。

其中,ApcRoutine设定为ntdll中的LdrInitializeThunk;ApcContext为linux进程堆栈指针,不包括为PE以及APC附加上去的堆栈;Iosb为解释器 libld-linux.so的入口地址;Reserved为一个空闲页面内存的首地址,此空闲内存用来临时保存为PE和APC附加上去的堆栈内容;Context为NULL。

该函数首先调用了StartInterp函数

Linux内核实现PE加载器——Longene源码分析

StartInterp函数也在init.c中,是由汇编代码组成。主要作用是进入Linux解释器中,也就是ld-linux.so。

Linux内核实现PE加载器——Longene源码分析

"mov 0x2c(%esp), %esi\n\t" 该句将 esi设为解释器/lib/ld-linux.so的入口。

之后就进入了解释器ld-linux.so,ELF解释器完成链接ntdll的工作后,会去搜索AT_ENTRY项值,它的跳转目标地址就是AT_ENTRY,这个值在load_pe_binary中设置为StartThunk函数地址,所以会运行到StartThunk。

那AT_ENTRY是什么时候设置的呢?回顾一下,前面讲到搜索ntdll.dll.so的路径之后,会调用map_system_dll函数,该函数在sysdll.c中,将StartThunk函数的地址已经赋值给start_thunk了,之后create_elf_tables记录了下来。

Linux内核实现PE加载器——Longene源码分析

create_elf_tables最后一个参数为entry,在该函数里将entry赋值给AT_ENTRY了。

Linux内核实现PE加载器——Longene源码分析

Linux内核实现PE加载器——Longene源码分析

StartThunk函数也是由汇编组成的,如下图所示:

Linux内核实现PE加载器——Longene源码分析

这个函数主要是参考了libc中的_start函数,主要目的是调用PrepareThunk,为调用真正的APC函数 LdrInitializeThunk做好准备。之后,就需要恢复为PE和APC准备的堆栈,否则,将无法从APC调用中返回,更无法进入PE文件的入口。由于popa恢复了在StartInterp时pusha压入的环境,所以最后一条ret,将返回到KiUserApcDispatcher,之后就进入了LdrInitializeThunk,进行PE文件的链接。

PrepareThunk函数如下图所示:

Linux内核实现PE加载器——Longene源码分析

执行完之后,就回到了 KiUserApcDispatcher函数开始执行LdrInitializeThunk,这里就不分析该函数代码了。因为之前由分析过wine代码,Longene这里的代码就是参考wine中的该函数实现。

KiUserApcDispatcher函数的最后一步是调用NtContinue函数。

Linux内核实现PE加载器——Longene源码分析

该函数的作用是恢复PE上下文,并回到内核空间,如果没有其他的APC函数要处理的话,w32syscall_exit就会开始执行start_thread(regs,pe_entry, bprm->p)

之前有提到过pe_entry被设置为ProcessStartForward函数的地址,该函数再wine/dls/ntdll/init.c中,是一个转发函数,调用了BaseProcessStart函数。

Linux内核实现PE加载器——Longene源码分析

BaseProcessStart函数通过BaseProcessStartEntry得到函数地址并进行调用,BaseProcessStartEntry得赋值是在LdrInitializeThunk中。前面提到过,APC机制会调用到LdrInitializeThunk函数,该函数中多次调用find_builtin_symbol函数,得到kernel32.dll中的例如BaseProcessStart函数地址

Linux内核实现PE加载器——Longene源码分析

进入BaseProcessStart函数,通过entry进入到PE入口执行,这时PE文件才正式开始执行。

Linux内核实现PE加载器——Longene源码分析



有了前面对代码的分析,我们从宏观角度来看看整个PE文件执行的过程,如图所示:

Linux内核实现PE加载器——Longene源码分析