前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >调用.NET9 JIT未导出函数

调用.NET9 JIT未导出函数

作者头像
江湖评谈
发布2024-03-25 14:29:07
530
发布2024-03-25 14:29:07
举报
文章被收录于专栏:天下风云天下风云

前言

非托管动态库的导出函数,一般是可以直接加载调用的函数。但是如果未导出的呢?比如,想调用.NET9 JIT非托管DLL里面的一个未导出(No extern)的函数。如何做呢?本篇看下

例子

一个简单的示例说明:

代码语言:javascript
复制
//文件DaoChu.cpp
#include <stdio.h>
#include<Windows.h>
int  max1(int a, int b){
    return a > b ? a : b;
}
extern "C" __declspec(dllexport) void ABCD(){
    printf("call ABCD");
}
int  max2(int a, int b){
    return a > b ? a : b;
}
int main(){
    printf("%d\n", max1(1, 2));
    printf("%d\n", max2(1, 2));
    getchar();
    return 0;
}

以上生成DaoChu.dll非托管。这里有三个函数:max1,max2,ABCD。只有ABCD函数标记了(extern "C" __declspec(dllexport))导出。所以通过非托管DLL调用的话也只能ABCD才能调用得到。

代码语言:javascript
复制
HMODULE h = LoadLibraryExA((LPCSTR)"DaoChu.dll", NULL, 0);
pFunc pc = (pFunc)GetProcAddress(h, "ABCD");

有没有可能调用max1,max2函数呢?当然有,可以借助导出函数的偏移来计算未导出的函数地址。MSCV编译器有一个规律,当一个函数调用的时候,比如max1函数,帧栈如下:

代码语言:javascript
复制
00007FF7FD1119E5 E8 FB F7 FF FF  call max1 (07FF7FD1111E5h)

它这里的地址07FF7FD1111E5h会跳到一个都是jmp的中转地址,下面代码最后一行是ABCD函数的帧栈中转,第一行是max1函数帧栈中转。

代码语言:javascript
复制
00007FF7FD1111E5 E9 E6 05 00 00       jmp         max1 (07FF7FD1117D0h)  
00007FF7FD1111EA E9 F1 49 00 00       jmp         __scrt_stub_for_is_c_termination_complete (07FF7FD115BE0h)  
00007FF7FD1111EF E9 3C 2C 00 00       jmp         _get_startup_argv_mode (07FF7FD113E30h)  
00007FF7FD1111F4 E9 67 30 00 00       jmp         __scrt_initialize_winrt (07FF7FD114260h)  
00007FF7FD1111F9 E9 22 1D 00 00       jmp         __raise_securityfailure (07FF7FD112F20h)  
00007FF7FD1111FE E9 2D 32 00 00       jmp         _RTC_Initialize (07FF7FD114430h)  
00007FF7FD111203 E9 14 48 00 00       jmp         __stdio_common_vfprintf (07FF7FD115A1Ch)  
00007FF7FD111208 E9 57 48 00 00       jmp         _exit (07FF7FD115A64h)  
00007FF7FD11120D E9 1E 1C 00 00       jmp         sprintf_s (07FF7FD112E30h)  
00007FF7FD111212 E9 25 49 00 00       jmp         QueryPerformanceCounter (07FF7FD115B3Ch)  
00007FF7FD111217 E9 08 49 00 00       jmp         SetUnhandledExceptionFilter (07FF7FD115B24h)  
00007FF7FD11121C E9 5F 0B 00 00       jmp         _CRT_RTC_INIT (07FF7FD111D80h)  
00007FF7FD111221 E9 6A 11 00 00       jmp         __scrt_narrow_argv_policy::configure_argv (07FF7FD112390h)  
00007FF7FD111226 E9 0F 48 00 00       jmp         __setusermatherr (07FF7FD115A3Ah)  
00007FF7FD11122B E9 60 32 00 00       jmp         _RTC_Terminate (07FF7FD114490h)  
00007FF7FD111230 E9 ED 47 00 00       jmp         _CrtDbgReport (07FF7FD115A22h)  
00007FF7FD111235 E9 26 2D 00 00       jmp         __scrt_is_user_matherr_present (07FF7FD113F60h)  
00007FF7FD11123A E9 01 31 00 00       jmp         __scrt_set_unhandled_exception_filter (07FF7FD114340h)  
00007FF7FD11123F E9 B4 47 00 00       jmp         __current_exception_context (07FF7FD1159F8h)  
00007FF7FD111244 E9 75 48 00 00       jmp         _register_onexit_function (07FF7FD115ABEh)  
00007FF7FD111249 E9 62 49 00 00       jmp         __scrt_stub_for_acrt_thread_detach (07FF7FD115BB0h)  
00007FF7FD11124E E9 71 48 00 00       jmp         _execute_onexit_table (07FF7FD115AC4h)  
00007FF7FD111253 E9 68 49 00 00       jmp         __scrt_stub_for_acrt_uninitialize (07FF7FD115BC0h)  
00007FF7FD111258 E9 15 49 00 00       jmp         GetProcessHeap (07FF7FD115B72h)  
00007FF7FD11125D E9 4E 06 00 00       jmp         ABCD (07FF7FD1118B0h)

这个帧栈中转在MSVC里面是固定的,我们只需要知道导出函数ABCD的帧栈中转地址距离max1函数帧栈中转地址的偏移,即可调用max1函数。很明显本例是0x78.把它改造成函数指针:

代码语言:javascript
复制
    //函数指针
    typedef int  (*compare)(int a, int b);
    typedef void (*pFunc)();
    extern "C" int ZW(int u1, int u2)
    //实际代码
    HMODULE h = LoadLibraryExA((LPCSTR)"DaoChu.dll", NULL, 0);
    pFunc pc = (pFunc)GetProcAddress(h, "ABCD");
    char* p = (char*)pc;
    p = p - 0x78;
    compare ce = (compare)p;
    ZW(2,3);
    ce(1, 2);

原理呢,也非常简单。先获取到导出函数的函数地址,然后通过这个地址计算出未导出函数的地址。因为帧栈中转调用,ZW函数返回有无问题?实际上根据本例,栈的扩展(rsp-0x20)在被调用的函数里面,所以这里是没有问题的。

但是其它问题呢?比如帧栈中转实际上是已经传参过了,为实际使用参数的中间过程。获取到的这个地址,参数是已经固定住了。为了让这个参数用到我们自己的传参,所这里还需,嵌入汇编代码,即是上面的ZW调用:

代码语言:javascript
复制
//文件x64asm.asm
.CODE ;
ZW PROC ;
add rcx,rcx;
mov rdx,rdx;
ret ;
ZW ENDP ;
END ;

右击x64asm.asm-》属性-》从生成项目中排除选择否

右击x64asm.asm-》属性-》项类型-》Microsoft Macro Assembler .

JIT

JIT一般的在如下路径:

代码语言:javascript
复制
C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\clrjit.dll

clrjit.dll里面包含了所有JIT操控,C++代码高达几十万行。但是它只导出区区五个函数

代码语言:javascript
复制
getJit
getLikelyClasses
getLikelyMethods
jitBuildString
jitStartup

这里面有成百上千的函数,如果要调用其它的未导出如何做呢?JIT的C++里面引入了Cmake代码,所以它的生成跟MSVC又有不同之处(这里需要着重注意MSVC与Cmake的不同,但是原理相通),JIT没有帧栈中转。根据上面的例子的原理,只需要知道一个导出函数的地址,以及在实际运行的时候距离另外一个函数偏移。这里以导出函数jitStartup计算未导出函数dumpILRange的函数地址。

代码语言:javascript
复制
//jitStartup函数指针声明和原型
typedef void (*jitStartup)(void* jitHost);
extern "C" DLLEXPORT void jitStartup(ICorJitHost* jitHost)
{
    if (g_jitInitialized)
    {
        if (jitHost != g_jitHost)
        {
            JitConfig.destroy(g_jitHost);
            JitConfig.initialize(jitHost);
            g_jitHost = jitHost;
        }
        return;
    }
#ifdef HOST_UNIX
    int err = PAL_InitializeDLL();
    if (err != 0)
    {
        return;
    }
#endif
    g_jitHost = jitHost;
    assert(!JitConfig.isInitialized());
    JitConfig.initialize(jitHost);
    Compiler::compStartup();
    g_jitInitialized = true;
}
//dumpILRange函数指针声明和原型
typedef void (*dumpILRange)(const BYTE* const codeAddr, unsigned codeSize);
void dumpILRange(const BYTE* const codeAddr, unsigned codeSize) // in bytes
{
    for (IL_OFFSET offs = 0; offs < codeSize;)
    {
        char prefix[100];
        sprintf_s(prefix, ArrLen(prefix), "IL_%04x ", offs);
        unsigned codeBytesDumped = dumpSingleInstr(codeAddr, offs, prefix);
        offs += codeBytesDumped;
    }
}
//实际代码
HMODULE h1 = LoadLibraryExA((LPCSTR)"C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\clrjit.dll
", NULL, 0);
char* p1 = (char*)(jitStartup)GetProcAddress(h1, "jitStartup");
p1 = p1 + 0x2B6380;
dumpILRange dumpil = (dumpILRange)p1;

但是实际上我们可能需要做更多,比如JIT环境变量的事先设置,否则即使获取到函数地址,也依然会有报错的情况。也就是说尽量满足未导出函数的函数里面所有调用情况的可能,才可能顺利调用未导出函数。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2024-03-21,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 江湖评谈 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 例子
  • 原理呢,也非常简单。先获取到导出函数的函数地址,然后通过这个地址计算出未导出函数的地址。因为帧栈中转调用,ZW函数返回有无问题?实际上根据本例,栈的扩展(rsp-0x20)在被调用的函数里面,所以这里是没有问题的。
  • JIT
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档