首页
社区
课程
招聘
[原创]一种直接调用C++程序so库导出函数的方法
2023-3-6 00:20 6928

[原创]一种直接调用C++程序so库导出函数的方法

2023-3-6 00:20
6928

前言

最近分析了一个C++写的程序,从程序设计的角度来看,非常棒,各种设计模式让人看的赏心悦目。不过对于逆向分析来说,各种代理类设计、各种类的继承是种很烦的事情,IDA分析起来跳来跳去,有时候就不想分析了,想直接调用so库现成的导出函数。可是这时候发现了一个问题,导出函数的返回值与参数往往含有类指针,而我又没有可以定义这些类的头文件,该如何处理?
通过实践,本文提出了一种有效的直接调用C++程序so库导出函数的方法,这种方法对于C写的程序、dll库是同样有效的。核心原理其实也简单,没有头文件,不知道类的定义其实不打紧,因为程序调用关心的只有压参、出参,保持栈平衡就行,而类指针其实也就是个long类型的值,因此直接用long替换就好了。当然,调用时,类对象的内存分配和初始化也要处理好,这个看IDA的反编译结果就好了。

实验过程

已知:test、libdemo.so
目标:手动调用A::hello函数
测试环境:kali-linux-2022.3-vmware-amd64.vmwarevm

反编译结果

图片描述
图1 test的main函数逆向结果

 

图片描述
图2 libdemo.so的A::hello函数逆向结果

测试代码1

首先,可以根据IDA反编译分析出各类的成员变量都有哪些,定义出结构体A和结构体B。
其次,C++函数函数调用在底层也是C函数调用的那套逻辑,本例中,A::hello就是_ZN1A5helloER1B,第一个参数是this指针,其实就是结构体A的首地址。
最后,参照test的反编译结果,完成结构体A与B的初始化,调用A::hello。
图片描述
图3 A::hello对应C函数_ZN1A5helloER1B

 

caller.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include<stdio.h>
#include<dlfcn.h>
 
typedef int (*fn_B_setName)(long pthis, char *name);
typedef void (*fn_A_A)(long pthis, char *name, int age);
typedef void (*fn_A_hello)(long pthis, long a2);
 
struct A{
    char name[32];
    int age;
};
 
struct B{
    char name[32];
};
 
int main()
{
    //加载 libdemo.so
    printf("load libdemo.so\n");
    void* libdemo_handle = dlopen("./libdemo.so", RTLD_NOW );
    printf("%016lx\n", libdemo_handle);
    fprintf(stderr, "%s\n", dlerror());
 
    fn_B_setName B_setName = (fn_B_setName)dlsym(libdemo_handle, "_ZN1B7setNameEPc"); //不严谨的写法,应该判断 libdemo_handle 的返回值;...
    printf("%016lx\n", B_setName);
    fprintf(stderr, "%s\n", dlerror());
 
    fn_A_A A_A = (fn_A_A)dlsym(libdemo_handle, "_ZN1AC2EPci");
    printf("%016lx\n", A_A);
 
    fn_A_hello A_hello = (fn_A_hello)dlsym(libdemo_handle, "_ZN1A5helloER1B");
    printf("%016lx\n", A_hello);
 
    struct A a={0};
    struct B b={0};
 
    B_setName((long)&b, "xiaoB2");
    A_A((long)&a, "xiaoA2", 19);
    A_hello((long)&a, (long)&b);
 
    return 0;
}

效果1
图片描述

测试代码2

其实就算不知道结构体A与B的定义也是可以调用A::hello的,只要申请两块足够大的内存即可。

 

caller2.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include<stdio.h>
#include<dlfcn.h>
 
typedef int (*fn_B_setName)(long pthis, char *name);
typedef void (*fn_A_A)(long pthis, char *name, int age);
typedef void (*fn_A_hello)(long pthis, long a2);
 
int main()
{
    //加载 libdemo.so
    printf("load libdemo.so\n");
    void* libdemo_handle = dlopen("./libdemo.so", RTLD_NOW );
    printf("%016lx\n", libdemo_handle);
    fprintf(stderr, "%s\n", dlerror());
 
    fn_B_setName B_setName = (fn_B_setName)dlsym(libdemo_handle, "_ZN1B7setNameEPc"); //不严谨的写法,应该判断 libdemo_handle 的返回值;...
    printf("%016lx\n", B_setName);
    fprintf(stderr, "%s\n", dlerror());
 
    fn_A_A A_A = (fn_A_A)dlsym(libdemo_handle, "_ZN1AC2EPci");
    printf("%016lx\n", A_A);
 
    fn_A_hello A_hello = (fn_A_hello)dlsym(libdemo_handle, "_ZN1A5helloER1B");
    printf("%016lx\n", A_hello);
 
    char a[1024]={0};
    char b[1024]={0};
    B_setName((long)b, "xiaoB3");
    A_A((long)a, "xiaoA3", 20);
    A_hello((long)a, (long)b);
 
    return 0;
}

效果2
图片描述

测试代码3

(这段代码是补充的,感谢mudebug师傅的提醒)
使用 基址+偏移 的方式不仅可以获得导出函数的地址,还可以获得未导出函数的地址,更加通用一些。
caller3.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#include<stdio.h>
#include<dlfcn.h>
 
typedef int (*fn_B_setName)(long pthis, char *name);
typedef void (*fn_A_A)(long pthis, char *name, int age);
typedef void (*fn_A_hello)(long pthis, long a2);
 
int get_baseaddr(char* filename, unsigned long* baseaddr)
{
    unsigned long start=0;
    unsigned long end;
    char line[4096]={0};
    char modulefile[1024]={0};
    char flags[32]={0};
    FILE *fp=NULL;
    int ret=-1;
 
    fp = fopen("/proc/self/maps", "r");
    if (fp == NULL) {
        return ret;
    }
 
    while (fgets(line, sizeof(line), fp) != NULL) {
        //printf("%s",line);
        sscanf(line, "%lx-%lx %s %*Lx %*x:%*x %*Lu %s", &start, &end, flags, modulefile); //%*x里的*表示不获取,即 %*Lx %*x:%*x %*Lu 都不要
 
        if (strstr(modulefile, filename)!=NULL) {
            *baseaddr=start;
            ret = 0;
            //printf("%s", line);
            //printf("baseaddr=%lx", *baseaddr);
            break;
        }
    }
 
    fclose(fp);
 
    return ret;
}
 
int main()
{
    //加载 libdemo.so
    printf("load libdemo.so\n");
    void* libdemo_handle = dlopen("./libdemo.so", RTLD_NOW );
    printf("%016lx\n", libdemo_handle);
    fprintf(stderr, "%s\n", dlerror());
 
    unsigned long baseaddr=0;
    get_baseaddr("libdemo.so", &baseaddr);  //不严谨的写法,应该判断返回值;
    printf("baseaddr=0x%016lx\n", baseaddr);
 
    fn_B_setName B_setName = (fn_B_setName)(baseaddr+0x12F8);
    fn_A_A A_A = (fn_A_A)(baseaddr+0x115A);
    fn_A_hello A_hello = (fn_A_hello)(baseaddr+0x11CA);
 
    char a[1024]={0};
    char b[1024]={0};
    B_setName((long)b, "MM");
    A_A((long)a, "GG", 21);
    A_hello((long)a, (long)b);
 
    return 0;
}

效果3
图片描述

其他说明

1.如果依赖的so库在当前路径却找不到

1
2
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:.
./test

[培训]《安卓高级研修班(网课)》月薪三万计划,掌握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法

最后于 2023-3-6 23:19 被Jtian编辑 ,原因:
上传的附件:
收藏
点赞1
打赏
分享
最新回复 (12)
雪    币: 29
活跃值: (5080)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
不吃早饭 2023-3-6 00:37
2
0
然鹅不同编译器,不同版本下,c++函数命名规则是不一样的
雪    币: 1724
活跃值: (3564)
能力值: ( LV6,RANK:90 )
在线值:
发帖
回帖
粉丝
Jtian 1 2023-3-6 09:59
3
0
不吃早饭 然鹅不同编译器,不同版本下,c++函数命名规则是不一样的
只管调用,不关心命名规则呀,命名直接看IDA结果好了
雪    币: 7960
活跃值: (5165)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
mudebug 2023-3-6 10:20
4
0
遍历maps 获取到模块首地址。然后加偏移就行。你这个太复杂,而且不能调用sub开头的函数
雪    币: 20
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
龙乎 2023-3-6 10:53
5
0
感觉作用不是很大,既然能写成类成员函数,那么this大部分情况下都是用到的,这种直接初始化一个this,往往得不到你想要的结果
雪    币: 1724
活跃值: (3564)
能力值: ( LV6,RANK:90 )
在线值:
发帖
回帖
粉丝
Jtian 1 2023-3-6 13:03
6
0
龙乎 感觉作用不是很大,既然能写成类成员函数,那么this大部分情况下都是用到的,这种直接初始化一个this,往往得不到你想要的结果
“调用时,类对象的内存分配和初始化也要处理好,这个看IDA的反编译结果就好了。”
雪    币: 1724
活跃值: (3564)
能力值: ( LV6,RANK:90 )
在线值:
发帖
回帖
粉丝
Jtian 1 2023-3-6 13:10
7
0
mudebug 遍历maps 获取到模块首地址。然后加偏移就行。你这个太复杂,而且不能调用sub开头的函数
获取模块首地址,然后加偏移的获取函数地址的方式确实是可以的,可以调用到未导出的函数,更通用一些。在某些情况下,没有对抗,最容易看到的就是IDA的反编译结果,也就是说函数符号是比较容易获得的,上面的方案还是比较方便的。
雪    币: 1724
活跃值: (3564)
能力值: ( LV6,RANK:90 )
在线值:
发帖
回帖
粉丝
Jtian 1 2023-3-6 13:12
8
0
相对于调用so里的未导出函数,我在想是否有方法调用elf文件的内部函数,模拟执行?自己写加载器?不知道有没有成熟方案
雪    币: 1724
活跃值: (3564)
能力值: ( LV6,RANK:90 )
在线值:
发帖
回帖
粉丝
Jtian 1 2023-3-6 13:55
9
1
Jtian 相对于调用so里的未导出函数,我在想是否有方法调用elf文件的内部函数,模拟执行?自己写加载器?不知道有没有成熟方案

chatGPT给了一个方案如下:

ELF(Executable and Linkable Format)是一种可执行文件格式,用于在Unix和Linux系统上运行的程序。在ELF文件中调用内部函数的方法与调用动态链接库中的内部函数类似,可以使用函数指针来执行该函数。需要注意的是,调用内部函数可能会破坏代码的稳定性和安全性,因此建议只在有充分理由的情况下进行。

下面是调用ELF文件内部函数的基本步骤:

  1. 使用mmap函数将ELF文件的内容映射到内存中,该函数返回一个指向映射区域的指针。例如:

    int fd = open("example.elf", O_RDONLY);struct stat st;fstat(fd, &st);void* base = mmap(NULL, st.st_size, PROT_READ|PROT_EXEC, MAP_PRIVATE, fd, 0);
  2. 使用dlsym函数获取函数在ELF文件中的地址。需要注意的是,该函数只能获取ELF文件中导出的函数或变量,因此如果需要调用非导出函数,则需要明确知道该函数在ELF文件中的地址。例如:

    // 获取导出函数的指针int (*myfunction)(int) = (int (*)(int))dlsym(RTLD_DEFAULT, "function_name");// 获取内部函数的指针int (*myinternalfunction)(int) = (int (*)(int))(base + offset);

    需要将dlsym的第一个参数指定为RTLD_DEFAULT,表示使用全局符号表来查找函数,而不是使用动态链接器的符号解析器。

  3. 调用获取的函数指针即可执行该函数。例如:

    int result = (*myfunction)(arg);int internal_result = (*myinternalfunction)(arg);
  4. 最后使用munmap函数释放内存。例如:

    munmap(base, st.st_size);

需要注意的是,在使用mmap映射文件时,需要提供正确的文件名和路径,否则会导致无法打开文件的错误。在使用dlsym获取函数地址时,需要提供正确的函数名或内部地址,否则会导致无法获取函数地址的错误。因此,在进行这些操作时,需要仔细检查参数并处理可能的错误情况。


最后于 2023-3-6 13:55 被Jtian编辑 ,原因:
雪    币: 262
活跃值: (350)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
singhost 2023-3-7 09:04
10
0
Jtian Jtian 相对于调用so里的未导出函数,我在想是否有方法调用elf文件的内部函数,模拟执行?自己写加载器?不知道有没有成熟方案 chatGPT ...
怎么可能用mmap呢,肯定要用dlopen加载啊,如果dlopen只能加载so的话,那就得写一个加载器加载到内存中,因为ELF在内存中的布局和在磁盘中是不一样的,不能直接使用mmap映射上去
雪    币: 1724
活跃值: (3564)
能力值: ( LV6,RANK:90 )
在线值:
发帖
回帖
粉丝
Jtian 1 2023-3-7 14:35
11
0
singhost 怎么可能用mmap呢,肯定要用dlopen加载啊,如果dlopen只能加载so的话,那就得写一个加载器加载到内存中,因为ELF在内存中的布局和在磁盘中是不一样的,不能直接使用mmap映射上去

我实验了一下,在一些简单的情况下,用mmap确实可以。

下面的主体代码也是chatgpt给的,我做了简单修改。


caller.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/mman.h>
#include <fcntl.h>

typedef int (*fn_sum)(int a, int b);

int main() {
    const char* filename = "./hello";
    unsigned char* base = NULL;
    off_t size = 0;
    int fd = open(filename, O_RDONLY);

    if (fd == -1) {
        printf("Failed to open ELF file!\n");
        return -1;
    }

    size = lseek(fd, 0, SEEK_END);
    base = mmap(NULL, size, PROT_READ|PROT_EXEC, MAP_PRIVATE, fd, 0);

    if (base == MAP_FAILED) {
        printf("Failed to mmap ELF file!\n");
        return -1;
    }

    // 获取指定函数的地址
    fn_sum sum = (fn_sum)(base + 0x1149);

    // 调用函数
    int a=2;
    int b=3;
    int c=sum(2, 3);
    printf("%d+%d=%d\n", a, b, c);

    munmap(base, size);
    close(fd);

    return 0;
}


效果


最后于 2023-3-7 14:55 被Jtian编辑 ,原因:
上传的附件:
雪    币: 1724
活跃值: (3564)
能力值: ( LV6,RANK:90 )
在线值:
发帖
回帖
粉丝
Jtian 1 2023-3-7 14:37
12
0

但被调用函数如果稍微复杂点会失败,和地址偏移有关

最后于 2023-3-7 14:37 被Jtian编辑 ,原因:
雪    币: 262
活跃值: (350)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
singhost 2023-3-20 19:28
13
1
Jtian 但被调用函数如果稍微复杂点会失败,和地址偏移有关
是的,要自己做重定位,我之前自己写过一个简单的内存loader,主要就是做符号的重定位
游客
登录 | 注册 方可回帖
返回