运行时修补ELF二进制文件

use*_*031 1 c linux assembly

我有一个程序的共享对象,我需要将指令从'jne/je'更改为jmp.我试图环顾四周,例如关于如何使用LD_PRELOAD进行此操作或无济于事.

Fra*_*kH. 5

这在技术上是否可行取决于以下几点:

  1. jne/ je说明有不同的长度:

    • 单字节操作码(0x74/ 0x75)加上有符号的8位偏移量(即总共两个字节)
    • 双字节操作码(0xf00x84/ 0x85,)加上有符号的32位偏移量(即总共6个字节)

    对于jmp指令是相同的(在64位模式下,总共有五个不同的编码,介于2和9个字节之间,具体取决于jmp是相对还是绝对).
    这意味着如果您的目标"足够接近"以选择与大小兼容的操作码,则只能替换jne/ .对于32位相对/ 您通常可以替换,但是对于近(8位)通常不可能,因为在+/- 128字节内不会有"自由指令空间".jejmpjmpjneje

  2. 如果你有一个"兼容的机会"wrt.根据指令的大小,你已经完成了两个你已经提到的任务:

    • 拦截共享对象加载,以便在使用库之前进行替换.
    • 找到你想修补的功能的地址(假设它改变了).

    如何做到这一点取决于您的操作系统; 在UN*X状,你可以尝试使用LD_PRELOAD加载自定义库你自己的什么都不做比dlopen()你要修改的库中,然后dlsym()找到程序,然后替换代码,但永远不会调用dlclose(),以确保修改后的副本当稍后(通过动态链接)再次访问lib时,保持在(即未卸载)而不是原始.

系统管理员通常有办法减轻(甚至禁用)LD_PRELOAD(对于以root身份运行的setuid可执行文件,它总是被忽略),因为它对系统完整性/安全性有明显的影响.因此,是否可以使用该技术取决于您具有的具体设置.

如果满足以上所有条件,那么类似于:

int patch_je_jne(void *instr, void *tgt_address)
{
    char *je_jne = (char*)instr;
    char *iaddr = je_jne;

    switch (je_jne[0]) {
    case JE_8BIT_OP:
    case JNE_8BIT_OP:
        iaddr += 2;
        tgt_address -= iaddr;        /* adjust pc-relative */
        if ((char*)tgt_address > (char*)0xFF)
            return CANNOT_PATCH_LARGE_OFFSETS;
        je_jne[1] = (char)tgt_address;
        je_jne[0] = JMP_OP;
        return PATCH_SUCCESS;

    case 0xF0:                       /* marker for a 32bit-relative JE/JNE */
        if (je_jne[1] != JE_32BIT_OP && je_jne[1] != JNE_32BIT_OP))
            break;
        iaddr += 6;
        tgt_address -= iaddr;        /* adjust pc-relative */
        *((char**)(je_jne + 2)) = tgt_address;
        je_jne[1] = JMP_32BIT_OP;
        return PATCH_SUCCESS;
    }
    return CANNOT_PATCH_THIS_INSTR;
}
Run Code Online (Sandbox Code Playgroud)

可能会这样做.如上所述,除了检查要修补的地址上的指令操作是否兼容之外,此代码中没有验证/验证.

如上所述,主要工作是安全地确定需要应用钩子的位置/方式.

正如有人建议的那样,修补二进制库而不是运行(加载)库的注释:关键问题是:目标是什么jmp,你能提前知道吗?
在二进制修补中,您需要确定在修改二进制文件时,这与操作系统行为(如ASLR(地址空间布局随机化))一起难以实现 - 您需要一个预先知道的,不变的目标地址你的可执行文件,但通常你只有一个亲戚 jmp可用,即不知道由于ASLR最终会导致哪个代码位置.这一切都取决于在那里你想要jmp去.