【问题标题】:How can I manually increment an instruction pointer from a context?如何从上下文中手动增加指令指针?
【发布时间】:2014-01-21 09:26:06
【问题描述】:

首先,让我说我在这里做的事情大多数人都没有正当理由去做。 99.99...% 的所有段错误应该导致明确的终止,并且在除了最简单的情况之外的任何情况下愉快地处理它们将导致非常糟糕的行为和损坏的堆栈。如果您来这里是为了解决段错误,请查看以下链接:https://www.securecoding.cert.org/confluence/display/seccode/SIG35-C.+Do+not+return+from+a+computational+exception+signal+handler

也就是说,我正在从外部标准实现一个环境,该标准已将计算逻辑错误的信号处理程序返回的行为定义为向前跳过一条指令。我知道这很糟糕,但是我无法控制它;我不能只对定义进行核对,因为它适用于已经编写了其他软件元素的嵌入式系统,这些软件元素依赖于定义的行为(它们通常是安全关键的,并且需要能够优雅地退出,即使它们不优雅或糟糕的事情;此外,我没有来源,所以我不能只修复段错误,并且任何现有的错误段错误/崩溃行为实际上都是期望的,因为我正在模拟现有系统的行为)。

虽然系统本身要在具有固定指令长度的 PowerPC 上运行,但我们的开发是在并行 x86/x64 环境中进行的,其中指令不是固定长度的。我知道下面的代码可以工作,尽管对 x86 来说很糟糕:

#define _GNU_SOURCE
#include <signal.h>
#include <stdio.h>
#include <ucontext.h>
#include <sys/mman.h>

#define CRASHME *((int*)NULL) = 0 
//for x86
#ifdef REG_EIP
#define INCREMENT(x) (x)->uc_mcontext.gregs[REG_EIP]++
//for x64
#elif defined REG_RIP
#define INCREMENT(x) (x)->uc_mcontext.gregs[REG_RIP]++
//for PPC arch
#elif defined PT_NIP
#define INCREMENT(x) (x)->uc_mcontext.uc_regs->gregs[PT_NIP]+=4
#endif

static void handler(int sig, siginfo_t *si, void *vcontext)
{
    ucontext_t *context = (ucontext_t *)vcontext;
    INCREMENT(context);
}

void crashme_function(void)
{
    printf("entered new context, segfaulting!\n");
    CRASHME;
    printf("SEGFAULT handled!\n");
}

int main (int argc, char* args)
{
    struct sigaction sa;
    printf("Printing a thing\n");
    sa.sa_flags = SA_SIGINFO;
    sigemptyset(&sa.sa_mask);
    sa.sa_sigaction = handler;
    sigaction(SIGSEGV, &sa, NULL);
    printf("Entering new context...\n");
    crashme_function();
    printf("context exited successfully\n");
    return(0);

}

执行此代码的结果将在运行 Linux 内核 3.11.X 的基于 intel 的架构上将指令指针推进 1,并最终退出指令。我知道这可能不适用于所有说明。事实上,当在我的测试环境中执行时,处理程序进入了 6 次(对于指令的 6 个字节),然后继续执行超过 CRASHME。

在给定现有指令的情况下,仅将给定指令指针前进到下一条指令似乎是一项微不足道的任务;处理器每个周期都会这样做。在其他设置中,有人说“查看指令表并构建自己的”或“实现反汇编程序”。这些对于任务来说既不合适也没有必要,因为两者都已经由其他人完成并且仅发布在(几乎?)我的工作计算机无法访问的网络位置,并且我不相信提交我的家个人电脑。但是我在哪里可以找到这样的表或库来完成只进行指令计算,而不查看我已经知道我无法访问的站点?

【问题讨论】:

  • 如果你甚至不知道那里有哪种指令,你会用它实现什么。您还应该知道指令可以重叠,因此如果您一个接一个地增加 1 个字节,您只会遇到可以解释为指令流的“一些”字节组合。如果出现异常,您甚至都不知道指令指针在哪里,因为如果最后一条指令是 ret,堆栈上有一些损坏的值,那么它可以在您的地址空间中的任何位置。
  • 这就是我问这个问题的原因:我知道我编写的代码在任何 x86/x64 架构上都非常不可信。这就是为什么我需要知道如何推进任意 IP。我希望提高 x86/x64 的 INCREMENT(x) 的复杂性。另外,这不是我的问题。我不是这里的用户,我是系统。用户已经知道这是行为,他们有责任从用户实现的错误处理程序中终止进程。
  • 你应该反汇编EIP指向的位置。不。我会跳过有问题的例程中的所有代码,只需遍历堆栈到调用函数(跳转到执行返回的已知位置)。
  • 完全没有建设性的评论:说一个人不应该回到段错误代码,因为 CERT 不建议这样做,就像说一个人不应该放火烧房子,因为吸烟会导致癌症。并不是说 CERT 不应该将其与“不要写if (x) …”和“不要写if (x==0)”列表一起列出。
  • 这就是我写整个第二段的原因。我不能穿过堆栈到达调用者函数,也不能跳转。我必须转到下一条指令,因为这是系统参考文档的现有定义,也是根据 CERT,这是专门编写的,因为一大群早期程序员发现 SEGFAULTS 可以用信号处理程序处理并着手编写代码处理而不是崩溃,而不是解决实际的段错误(这几乎总是正确的解决方案)。在大多数操作系统中,从 SIGSEGV 返回是未定义的,这是有原因的。

标签: c assembly x86 segmentation-fault


【解决方案1】:

您可以使用像 libdisasm 这样的库,它应该可以为您提供所需的信息。我也做过类似的挂钩,编写自己的反汇编程序,但我想使用现成的库要容易得多。

无论如何,正如我在评论中所写的,我不确定您认为您从中获得了什么。你真的不能指望以这种方式自动分析程序,如果你想让它用于崩溃恢复,我已经说过这是非常不可靠的原因,并且可能导致比解决更多的问题。

也许你真正需要的是一些虚拟机。

为了执行代码,您还需要一些汇编程序,它将寄存器恢复到异常发生时的状态,然后跳转到您指向的任何位置。

【讨论】:

  • 问题在于它是用于嵌入式系统的模拟,其代码已经开发出来。事实上,如果嵌入式系统因为依赖这种行为而崩溃,我们的系统应该会显示同样的行为。关于为什么 ARINC 作者在他们的软件中实施如此糟糕的行为,我问了你同样的问题;但我们的只是做和死。从字面上看,我不允许将其保留为未定义。它已被定义,必须尊重定义。不过感谢您的回答。
  • @user2149140 您是否通过为 x86 编译代码来模拟通过转到 x86 处理器上的下一条指令来转到 PowerPC 处理器上的下一条指令的效果?如果是这样,那是行不通的,两个编译版本的执行方式可能完全不同。
  • @GavinSmith 我真的不知道你在问什么。在 powerPC 上,转到 context->IP+4 将转到下一条指令。对于英特尔来说,这并不简单。我也知道执行会有所不同;以下指令可能还有另一个 SIGSEGV。这并不是说行为是确定性的,而是定义的。如前所述,如果某些用户执行此操作,这也不是我的问题,只是为了确保它稍后会返回一条指令。
  • 我不明白当您开发的系统是 PowerPC 时,您为什么要在 x86 上执行此操作。在 x86 上出现分段错误后正在运行的程序的行为不会告诉您任何关于真实系统上会发生什么的事情。因此,您将无法测试它是否正常工作。
  • 在任何情况下,使用反汇编程序库都可以做到这一点——例如,在当前 IP 上反汇编一条指令会给出指令的大小。你没有解释为什么你不能这样做。
【解决方案2】:

Linux 内核源具有 X86 操作码映射的编码,然后由 Awk 脚本解析以生成一组可用于读取指令的表。它有足够的信息为您提供准确的指令大小,尽管您可能需要对其进行扩展以包含浮点指令和一些较新的 Intel 扩展(如 AVX)的信息。

如果您有权访问 linux 内核源代码树,请查看 arch/x86/lib/x85-opcode-map.txt。

其中包含确定指令大小所需的所有数据。

有一个 AWK 脚本 @arch/x86/tools/gen-insn-attr-x86.awk 将读取操作码文件并生成一系列对操作码映射中的信息进行编码的表格。

最后,如果您查看 arch/x86/lib/insn.c,其中有一个函数 insn_get_length(...) 将使用从操作码映射生成的表为您提供指令的长度。这应该足以让您回答您的特定问题“这条指令有多大”。

该代码没有什么特别“内核”的地方。您无需做任何特别的事情就可以适应用户模式。

我假设访问 Linux 内核源代码对您来说不应该是一个安全问题,并且没有什么会妨碍您阅读/采用 GPL 代码。

【讨论】:

    猜你喜欢
    • 2014-03-25
    • 2013-07-30
    • 2014-05-21
    • 2012-01-02
    • 2018-05-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多