【问题标题】:How to hide the exported symbols name within a shared library如何在共享库中隐藏导出的符号名称
【发布时间】:2012-03-27 18:22:28
【问题描述】:

对于 VC,我可以编写一个 DEF 文件并使用“NONAME”指令在 dll 的导出表中只保留序号。

如何使用 gcc 和 ELF 格式的共享库做同样的事情?

或者,ELF 共享库中是否有类似 PE 格式 DLL 中的序号的东西?如果没有,我如何在共享库中隐藏导出符号的名称?

========================================

更新:一些附加说明:

在 Windows 中,您可以通过仅放置一个具有空名称的整数 ID(序数)来导出函数。

为了显示它,dll 导出表的正常布局如下所示:http://home.hiwaay.net/~georgech/WhitePapers/Exporting/HowTo22.gif

“NONAME”看起来像这样:http://home.hiwaay.net/~georgech/WhitePapers/Exporting/HowTo23.gif

请注意,第二张图片中的函数名称为“N/A”。这是一个完整的解释:hxxp://home.hiwaay.net/~georgech/WhitePapers/Exporting/Exp.htm。

========================================

更新:非常感谢所有给我建议的人。最后,我决定在 linux/posix 平台上继续使用静态库。 但是将小的“特殊部分”(它使用一些不适合静态库的功能,例如:TLS Slot 等)提取到普通的共享库。因为小型普通共享库只做很少的事情,而这些工作完全不敏感,所以没有必要隐藏/隐藏它的 API。

我认为这是解决我的问题的最简单方法:-D

【问题讨论】:

  • 如果我从 .so 文件中删除了一个动态符号,我可以使用序数之类的东西从另一个应用程序中再次调用它吗?
  • 关键是大多数 Linux 人不知道NONAME 指令在 Windows 世界中的含义(至少我不知道,因为我从未使用过或为 Windows 编写过代码)。所以你应该解释这意味着什么(以及什么是“PE中的序数”)。更一般地,解释你真正想要实现的目标,而不是参考 Windows 是如何做到的。
  • 您在各种答案中的 cmets 包含比问题更详细的信息。您应该考虑编辑您的问题以包含此信息。
  • 好的,你说得对,Basile,我会附加一些额外的解释。但简而言之:我需要导出一个函数但隐藏它的名称。
  • 但是,在 Linux 世界中,导出函数并隐藏其名称本身就是矛盾的。您将有一个用于库的头文件,并且该头文件提到通过 names 导出的函数(否则,该函数不会被导出,即使通过一些肮脏的技巧可以使其可访问)。

标签: c++ c gcc shared-libraries


【解决方案1】:

当您想长期维护代码时,关于 attribute ((visibility ("hidden"))) 的先前答案很好,但是如果您只有几个符号想要可见并且想要快速修复...在您想要导出使用的符号上,添加

__attribute__ ((visibility ("default"))) 

然后你可以将-fvisibility=hidden 传递给编译器

这里有详尽的解释:

http://gcc.gnu.org/wiki/Visibility

编辑:另一种方法是构建静态库/存档(使用ar -cru mylib.a *.o 制作.a 存档)或根据此combine two GCC compiled .o object files into a third .o file 将对象组合成单个对象文件

如果您问“为什么要组合目标文件而不是仅仅制作静态库?” ...因为链接器将 .o 文件与 .a 文件区别对待(我不知道为什么,就是这样),特别是它允许您将 .o 文件链接到共享库或二进制文件中,即使所有符号都被隐藏了(即使是您正在使用的符号)这具有减少启动时间(少一个 DSO 和要查找的符号少得多)和二进制大小(符号通常占尺寸的约 20%,剥离只处理其中的一半——只是外部可见的部分)

对于二进制文件strip --strip-all -R .note -R .comment mybinary

图书馆strip --strip-unneeded -R .note -R .comment mylib.so

在此处详细了解静态链接的好处:http://sta.li/faq,但他们没有讨论许可问题,这是使用静态库的主要原因,而且您想隐藏自己的 API ,这可能是个问题

现在我们知道有一个“符号干净”的对象,可以使用我们的组合对象通过链接 private.o 和 public.c 来构建 libpublic.so(它只别名/导出你想要的公共) 到共享库中。

此方法也很适合查找公共 API 中不需要的“额外代码”。如果您将 -fdata-sections -ffunction-sections 添加到您的对象构建中,当您与 -Wl,--gc-sections,--print-gc-sections 链接时,它将删除未使用的部分并打印已删除内容的输出。

编辑 2 - 或者您可以隐藏整个 API 并只为您要导出的函数设置别名

alias ("target")

alias 属性导致声明作为另一个符号的别名发出,必须指定。例如,

void __f () { /* Do something. */; }
void f () __attribute__ ((weak, alias ("__f")));

定义f' to be a weak alias for __f'。在 C++ 中,必须使用目标的错位名称。如果 `__f' 没有在同一个翻译单元中定义,则会出错。

并非所有目标机器都支持此属性。

【讨论】:

  • 让我再说一遍:我需要导出 API(所以不能“隐藏”)。我需要导出一个没有名称的 api!就像 VC 在 DEF 文件中的“NONAME”指令一样。 GCC 是否有任何“NONAME”等价物?
  • 这不能回答问题(请参阅更新的信息和说明)。此外,该链接没有提供“按序数”或通过名称以外的任何其他机制导出函数的示例。
  • 你想设置一个别名符号吗?这样你就可以导出它并隐藏真实的?
  • @technosaurus:是的,我也可以设置别名。 :-) 我可以使用版本脚本来设置别名吗?以及链接器(ld)是否可以自动将真实名称映射到别名?
  • 我用别名编辑了我的帖子,我总是发现版本脚本方法很麻烦并且难以跟踪......但是我再次使用 geany 作为我的“IDE”......不过我突然想到,我从来没有添加多个属性,所以我不确定一个函数是否可以是“别名”&&“默认”可见性。
【解决方案2】:

您可以考虑使用GCC function attribute 来提高可见性并将其隐藏,即在头文件的许多适当位置添加__attribute__((visibility ("hidden")))

然后您将隐藏无用的符号,并保留好的符号。

这是一个 GCC 扩展(可能被 Clang 或 Icc 等其他编译器支持)。

附录

在 Linux 世界中,共享库应按名称导出函数(或者可能是全局数据),如在头文件中发布的那样。否则,不要将这些函数称为“导出”——它们不是!

如果您绝对希望在共享库中拥有一个函数,该函数可访问但不能导出,您可以通过某种方式注册它(例如,将函数全局数据(例如数组)的某个槽中的指针),这意味着您拥有(或提供)一些函数注册机制。但这不再是导出函数了。

更具体地说,您可以在主程序中拥有一个全局函数指针数组

 // in a global header.h
  // signature of some functions
 typedef void signature_t(int, char*);
 #define MAX_NBFUN 100
 // global array of function pointers
 extern signature_t *funtab[MAX_NBFUN];

然后在您的程序的main.c 文件中

 signature_t *funtab[MAX_NBFUN];

然后在你的共享对象中(例如在myshared.c 文件中编译成libmyshared.so)一个构造函数:

 static my_constructor(void) __attribute__((constructor));

 static myfun(int, char*); // defined elsewhere is the same file
 static void 
 my_constructor(void) { // called at shared object initialization
    funtab[3] = myfun;
 }

稍后在您的主程序(或其他一些共享对象)上可能会调用

 funtab[3](124, "foo");

但我永远不会将这样的东西称为“导出”函数,只有 可访问 函数。

另见 C++ 软件,如 QtFLTKRefPerSysGCCGTKmmFOX-ToolkitClang 等......它们都可以通过 pluginscallbacksclosures(在内部,一个好的 C++ 编译器会发出并优化对 C++ lambda expressions 的闭包的调用)。还可以查看 PythonfishLuaGNU guile 等解释器内部,您可以使用 C++ 代码对其进行扩展。

考虑同时生成机器代码并在您的程序中使用它。 asmjitlibgccjitLLVMGNU lightning 之类的库可能会有所帮助。

在 Linux 上,您可能会在运行时生成一些 C++ 代码到 /tmp/generated.cc,通过分叉(可能使用 system(3)popen(3)...)一些命令,例如 g++ -Wall -O -fPIC -shared /tmp/generated.cc -o /tmp/generated-plugin.so,将该代码编译成 /tmp/generated-plugin.so 插件然后使用dlopen(3)dlsym(3)。然后使用extern "C" 函数,并查看C++ dlopen minihowto。您可能对__attribute__((constructor)) 感兴趣。

我的个人经验(在过去的项目中,我在这里不允许提及,但在我的网页上提及)是您可以在 Linux 上生成数十万个插件。我仍然敢于提及我的manydl.c 程序(其 GPLv3+ 许可证允许您将其改编为 C++)。

在概念层面,阅读GC handbook 可能会有所帮助。垃圾收集代码(或插件)存在一个微妙的问题。

另请阅读 Drepper 的论文 How to write shared libraries,参见 elf(5)ld(1)nm(1)readelf(1)ldd(1)execve(2)mmap(2)dlopen(3)execve(2),@98765434 @、Advanced Linux ProgrammingProgram Library HOWTOC++ dlopen mini-howto 和 Ian Taylor 的 libbacktrace

【讨论】:

  • 说,我有一个名为“my_func”的 API,我必须从另一个应用程序调用它。所以,我必须将它导出到 dll 的导出表(意味着它不能被隐藏)。我只是不想暴露 API 的名称。在 VC 和 PE DLL 中,我可以通过只导出序数来实现它,但是我怎么能用 gcc 和 elf .so 做同样的事情呢?
  • 所以,你“有一个 API”(我不确定你是否理解这个短语)。为什么不能在 API 的头文件中适当地添加适当的 GCC __attribute__((visibility ("hidden")))(您可以使用宏来帮助您)?或者“拥有 API”对您来说意味着什么?
  • 不,我相信你应该问你真正的问题是什么,并用普通的Linux方式解决它。您认为这是一种过于以 Windows 为中心的方式,并且在 Linux 上无法正常工作(反之亦然)。标准的 Linux 方法是在您的共享对象中拥有数以千计的导出函数(它们的名称对链接或动态加载的应用程序可见)dlopen 您的共享库。
  • 您是否考虑过相反的解决方案:使您的库成为免费软件(例如,在 GPLv3 许可下)?
  • 但是你的技巧,如果在一个库中实现,会让你的库的合法用户调试程序变得更加困难......
【解决方案3】:

要在 UNIX 上隐藏导出函数的含义,您可以使用#defines 简单地重命名来混淆它们的名称。像这样:

#define YourGoodFunction_CreateSomething              MeaninglessFunction1
#define YourGoodFunction_AddSomethingElseToSomething  lxstat__
#define YourGoodFunction_SaveSomething                GoAway_Cracker
#define YourGoodFunction_ReleaseSomething             Abracadabra

等等。

在一些功能的情况下,它可以手动完成。如果需要数千个,则应使用代码生成。

  1. 获取您的真实函数名称列表,使用 grep、awk、cut 等。
  2. 准备一本无意义名称的字典
  3. 编写一个脚本(或二进制)生成器,该生成器将输出带有 #defines 的 C 头文件,如上所示。

唯一的问题是如何获取字典。好吧,我在这里看到了几个选项:

  • 您可以让您的同事在他们的键盘上随机输入 ;-)
  • 生成一个随机字符串,如:read(/dev/urandom, 10-20 bytes) | base64
  • 使用一些真正的字典(通用英语,特定领域)
  • 收集真实的系统 API 名称并稍作更改:__lxstat -> lxstat__

这仅限于您的想象力。

【讨论】:

    【解决方案4】:

    您可以编写一个版本脚本并将其传递给链接器来执行此操作。

    一个简单的脚本如下所示:

    testfile.exp:

    {
    global:
      myExportedFunction1;
      myExportedFunction2;
    
    local: *;
    }
    

    然后使用以下选项链接您的可执行文件:

      -Wl,--version-script=testfile.exp
    

    当应用于共享库时,它仍会列出 .so 文件中的符号以进行调试,但无法从库外部访问它们。

    【讨论】:

    • 所以,myExportedFunction1 和 myExportedFunction2 仍然作为共享库的动态符号出现?如果我想在没有符号名称的情况下导出这两个 API,我该怎么办?在 Windows 中,我们只能通过其序号导出 API,是否有 ELF .so 文件的等价物?
    • ELF .so 文件中没有序号,只有符号名称。
    【解决方案5】:

    我正在寻找相同问题的解决方案。所以,到目前为止,我找不到一个强大的解决方案。但是,作为概念证明,我使用 objcopy 来达到预期的效果。基本上,在编译一个目标文件后,我重新定义了它的一些符号。然后使用翻译后的目标文件构建最终的共享目标文件或可执行文件。结果,可以用作对我的算法进行逆向工程的提示的类/方法名称完全被一些无意义的名称 m1、m2、m3 重命名。

    这是我用来确保这个想法有效的测试:

    生成文件:

    all: libshared_object.so executable.exe
    
    clean:
        rm *.o *.so *.exe
    
    libshared_object.so : shared_object.o
        g++ -fPIC --shared -O2 $< -o $@
        strip $@
    
    shared_object.o : shared_object.cpp interface.h
        g++ -fPIC -O2 $< -c -o $@
        objcopy --redefine-sym _ZN17MyVerySecretClass14secret_method1Ev=m1 \
                --redefine-sym _ZN17MyVerySecretClass14secret_method2Ev=m2 \
                --redefine-sym _ZN17MyVerySecretClass14secret_method3Ev=m3 $@
    
    
    executable.exe : executable.o libshared_object.so
        g++ -O2 -lshared_object -L. $< -o $@
        strip $@
    
    executable.o : executable.cpp interface.h
        g++ -O2 -lshared_object -L. $< -c -o $@
        objcopy --redefine-sym _ZN17MyVerySecretClass14secret_method1Ev=m1 \
                --redefine-sym _ZN17MyVerySecretClass14secret_method2Ev=m2 \
                --redefine-sym _ZN17MyVerySecretClass14secret_method3Ev=m3 $@
    
    run: all
        LD_LIBRARY_PATH=. ./executable.exe
    

    interface.h

    class MyVerySecretClass
    {
    private:
        int secret_var;
    public:
        MyVerySecretClass();
        ~MyVerySecretClass();
        void secret_method1();
        void secret_method2();
        void secret_method3();
    };
    

    shared_object.cpp

    #include <unistd.h>
    #include <stdlib.h>
    #include <stdio.h>
    
    #include "interface.h"
    
    MyVerySecretClass::MyVerySecretClass()
        : secret_var(0)
    {}
    
    MyVerySecretClass::~MyVerySecretClass()
    {
        secret_var = -1;
    }
    
    void MyVerySecretClass::secret_method1()
    {
        ++secret_var;
    }
    
    void MyVerySecretClass::secret_method2()
    {
        printf("The value of secret variable is %d\n", secret_var);
    }
    
    void MyVerySecretClass::secret_method3()
    {
        char cmdln[128];
        sprintf( cmdln, "pstack %d", getpid() );
        system( cmdln );
    }
    

    executable.cpp

    #include "interface.h"
    
    int main ( void )
    {
        MyVerySecretClass o;
        o.secret_method1();
        o.secret_method2();
        o.secret_method1();
        o.secret_method2();
        o.secret_method1();
        o.secret_method2();
        o.secret_method3();
        return 0;
    }
    

    【讨论】:

    • 这里有一个更简单的隐藏接口的方法: 1. 定义一个接口类; 2. 在 dll/so 模块上实现(派生)接口,但不导出它; 3.只在dll/so中导出一个api,比如:“void* CreateInstance()”,它返回一个指向你接口类的指针; 4.从你的主可执行图像中调用这个api。
    • 然而,我已经厌倦了一种更复杂但“优雅”的方式:使用具有执行权限的 MapViewOfFileEx / mmap 启动代码并自己运行它。这样,你需要自己初始化所有东西,并解决待处理的IAT条目等。我终于发现它对于一个可移植的多平台应用程序来说太复杂了,并且缺乏对某些平台的可移植性(例如:chrome NaCL):-(
    • 使用虚拟表的隐藏功能是可以的。但是,关键是如果产品已经实现并且您无法修改它该怎么办。只是因为它太大了。混淆看起来是静态链接所有内容的唯一选择。我一直在寻找一种工具,它可以自动混淆 C++ 名称,而无需额外的努力。然而,我一个也找不到。所以,我想出了一个可以做到的方法。
    • 这种方法的美妙之处在于您可以加密方法名称并将加密名称用作符号。例如,这将允许您解密堆栈跟踪和调试应用程序,同时保持堆栈跟踪对其他人完全不可读。
    • 我期待一个工具可以更改“.so”模块的导出符号表,将它们重命名为一些意义不大的东西,并调整“消费者应用程序”的导入表(谁正在使用它模块)自动。我们只需要这个工具可以处理 ELF 格式,因为在 Windows 上我们可以在 .DEF 文件中使用 NONAME 指令。
    猜你喜欢
    • 1970-01-01
    • 2014-04-01
    • 2021-11-04
    • 2020-10-05
    • 1970-01-01
    • 2012-11-27
    • 2011-10-03
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多