在运行时使用C++和汇编来分配和创建新函数

8

我一直在做一个(C++)项目,需要完全动态分配函数,这意味着使用malloc/new和mprotect,然后手动修改缓冲区以使其成为汇编代码。因此,我想知道我的"缓冲区"需要什么条件,才能成为另一个_cdecl函数的副本。例如:

int ImAcDeclFunc(int a, int b)
{
     return a + b;
}

如果我想要完全动态地创建一个这个函数的副本,那需要什么(记住它是C++带有内联汇编)?首先,我猜我需要做类似于这样的事情:

// My main....
byte * ImAcDeclFunc = new byte[memory];
mprotect(Align(ImAcDeclFunc), pageSize, PROT_EXEC | PROT_READ | PROT_WRITE);

接下来,我需要找出ImAcDeclFunc(int a, int b);的汇编代码。由于我的汇编语言水平还不够好,那么这个函数在AT&T语法中应该怎么写呢?以下是我大胆的尝试:

push %ebp
movl %%ebp, %%esp
movl 8(%ebp), %%eax
movl 12(%ebp), %%edx
addl edx, eax
pop ebp
ret

现在,如果这段代码是正确的(我非常怀疑,请纠正我),那么我只需要找到这些代码的十六进制值(例如,“jmp”是0xE9,“inc”是0xFE),并直接在C++中使用这些值吗?如果我继续我的先前的C ++代码:
*ImAcDeclFunc = 'hex value for push'; // This is 'push' from the first line
*(uint)(ImAcDeclFunc + 1) = 'address to push'; // This is %ebp from the first line
*(ImAcDeclFunc + 5) = 'hex value for movl' // This is movl from the second line
// and so on...

完成整个代码/缓冲区的这个步骤后,是否足以实现完全动态的_cdecl函数呢?(即,我是否可以将其转换为函数指针并执行int result = ((int (*)(int, int))ImAcDeclFunc)(firstArg, secondArg)?)。我不想使用boost::function或类似的东西,我需要函数完全动态化,因此我很感兴趣:)
注意:这个问题是我之前一个问题的延续,但更具体。

为什么需要复制一个函数?原始函数已经很好了。您是否想从某个更高级别的表示中生成全新的函数? - n. m.
@n.m. 是的,这只是一个例子,让我更好地理解并为您轻松呈现所有内容。我大约需要二十个这样的例子。如果您阅读我的链接(到我的其他问题),您会完全理解为什么 :) - Elliott Darfink
我第一次尝试理解那个问题,但完全没有成功。 - n. m.
我认为你将会在这条路上遭受很多痛苦...你可以通过使用脚本语言(如SigTerm所推荐的)来避免麻烦,或者如果你必须使用C/C++作为语言,也许可以让你的程序将C/C++源代码文本写入文件,然后运行g++(或其他编译器)将该源代码转换为共享库,然后动态链接到它。 - Jeremy Friesner
3个回答

6
如果您拿到这个 lala.c 文件:
int ImAcDeclFunc(int a, int b)
{
    return a + b;
}

int main(void)
{
    return 0;
}

您可以使用gcc -Wall lala.c -o lala编译它。然后,您可以使用objdump -Dslx lala >> lala.txt反汇编可执行文件。您将发现ImAcDeclFunc被汇编为:
00000000004004c4 <ImAcDeclFunc>:
ImAcDeclFunc():
  4004c4:   55                      push   %rbp
  4004c5:   48 89 e5                mov    %rsp,%rbp
  4004c8:   89 7d fc                mov    %edi,-0x4(%rbp)
  4004cb:   89 75 f8                mov    %esi,-0x8(%rbp)
  4004ce:   8b 45 f8                mov    -0x8(%rbp),%eax
  4004d1:   8b 55 fc                mov    -0x4(%rbp),%edx
  4004d4:   8d 04 02                lea    (%rdx,%rax,1),%eax
  4004d7:   c9                      leaveq 
  4004d8:   c3                      retq   

实际上,这个功能相对容易复制到其他地方。在这种情况下,如果你复制了字节,它就可以正常工作。

当你开始使用包含操作码相对偏移量的指令时,问题就会出现。例如,相对跳转或相对调用。在这些情况下,你需要适当地重定位指令,除非你能够将其复制到与原始地址相同的地址。

简而言之,为了重定位,你需要找到它最初所基于的位置,并计算到你将要基于的位置的差异,并根据此偏移量重定位每个相对指令。这本身是可行的。你真正的困难在于处理对其他函数的调用,特别是对库函数的函数调用。在这种情况下,你需要确保库已链接,然后按照你正在针对的可执行文件格式定义的方式调用它。这是非常不容易的。如果你仍然感兴趣,我可以告诉你应该阅读哪些内容。


在你上面的简单例子中,你可以这样做:

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

int main(void)
{
    char func[] = {0x55, 0x48, 0x89, 0xe5, 0x89, 0x7d, 0xfc,
    0x89, 0x75, 0xf8, 0x8b, 0x45, 0xf8,
    0x8b, 0x55, 0xfc, 0x8d, 0x04, 0x02,
    0xc9, 0xc3};

    int (* func_copy)(int,int) = mmap(NULL, sizeof(func),
        PROT_WRITE | PROT_READ | PROT_EXEC,
        MAP_PRIVATE | MAP_ANONYMOUS, 0, 0);

    memcpy(func_copy, func, sizeof(func));
    printf("1 + 2 = %d\n", func_copy(1,2));

    munmap(func_copy, sizeof(func));
    return EXIT_SUCCESS;
}

这在 x86-64 上运行良好。它会打印出以下内容:
1 + 2 = 3

2
如果您能提供一个可行的示例,我会非常感激!那对我来说就是金子一样宝贵!关于相对调用,从我的了解来看,就像这样; targetAddress - currentAddress -/+ 任何偏移量?关于“处理库调用”,如果我只调用成员函数,那会有什么问题吗?由于我使用的是GCC,它与cdecl调用完全相同,但有一个额外的指针('this'指针)。如果我从成员函数中再调用库函数,比如使用_stdcall,会创建问题吗?例如dynamic_func->member_func->library_func? - Elliott Darfink
顺便问一下,mprotect失败是因为你没有对齐内存吗?我会尝试一下 :) - Elliott Darfink
@ElliottDarfink:是的,我也注意到了对齐问题。虽然已经更改了对齐方式,但仍会导致分段错误,因此需要继续尝试一些方法。是的,相对偏移量通常可以通过目标地址和当前地址之间的差值来实现。 - Mike Kwan
@ElliottDarfink:啊,当然!我的代码是64位的。对于32位版本,您需要重新编译并在您的机器上转储。不幸的是,我无法在我正在SSH连接的框中使用“-m32”开关。另外,我刚刚意识到我们可以使用mmap。像这样:http://pastebin.com/ArwyPW0R - Mike Kwan
哈哈,太酷了!在C++中动态执行函数。虽然我将“lala.c”反编译为ASM并复制了所有字节,但我无法使用您的示例(mmap)使其正常工作?从代码来看,您的示例似乎更流畅,是一种更好的方法,所以我真的很喜欢它。无论如何,如果我使用您的代码(当然还有我的ASM代码),它会在“func_copy”函数调用时崩溃并出现分段错误。这是我的代码(有效的内存返回“mmap”调用),所以必须还有其他问题:http://pastebin.com/JxUchcqN。 - Elliott Darfink
显示剩余10条评论

1

1
是的,我已经了解过它,但我还没有完全理解它的工作原理。更不用说文档有多么简略了。你不知道是否有任何文档资源可以提供帮助吗?它似乎是我想要的,只是我不知道“如何”。 - Elliott Darfink

1

我认为将一些脚本语言嵌入到您的项目中,而不是编写自修改程序,会是更好的想法。这样做需要的时间更少,而且您将获得更大的灵活性。

如果我想要完全动态地创建此函数的副本,那需要什么(记住它是带有内联汇编的C++)?

这需要使用反汇编器的人类。从技术上讲,函数应该从一个地址开始,并在返回语句结束。但是,在优化阶段编译器对函数进行了什么处理是未知的。如果函数入口点位于某种奇怪的位置(例如在函数末尾,在返回语句之后),或者如果函数被分成多个部分并与其他函数共享,则我不会感到惊讶。


“需要人类使用反汇编器”这种说法是不正确的。有一些自动化工具可以执行静态分析,例如Dyninst,这与此相矛盾。 - Mike Kwan
@MikeKwan:没有矛盾,我是正确的。虽然有自动化工具,但它们并不是100%可靠的,可能需要人类协助,并且它们经常从调试信息中提取辅助数据。像IDA Pro这样的工具需要几分钟才能将文件分成例程,但仍然可能会错过其中的一些。如果您尝试分析已混淆以混淆反汇编器的软件,那么情况会更加有趣。 - SigTerm
你认为一个使用反汇编器的人在这种情况下可以做得更好吗?大多数静态分析在间接分支方面都会失败。在这些情况下,人类分析也不会好到哪里去。你的回答中也有更多的不准确之处。至少在 ELF 上,函数的大小实际上可以通过符号信息来确定。 - Mike Kwan
@MikeKwan:显然,一个足够决心的人最终将不可避免地解密程序或编写替代例程以产生相同的结果(即逆向工程)。这是不可避免的,唯一的问题是需要多少时间。自动工具无法做到这一点。它将无法处理例程,并且情况不会随着时间的推移而改善。 - SigTerm
@MikeKwan:有多种逆向工程技术,你应该知道。你想争辩吗?我现在没心情,也不会改变我的答案。记住墨菲定律。“如果有什么事情可能出错,它就会出错。”这是编程风格的一个可能的基础思路之一。而且根据墨菲定律的统计分析,在遇到第一个例程时就会失败。OP需要的不是低级别的*exe黑客技巧,而是脚本语言的灵活性。编写内存编译器只会是重复造轮子... - SigTerm
显示剩余7条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接