【问题标题】:Far call into __USER32_CS from 64-bit code on Linux在 Linux 上从 64 位代码远调用 __USER32_CS
【发布时间】:2013-08-18 19:19:26
【问题描述】:

最近我意识到你可以在 64 位代码中做到这一点:

  const size_t kLowStackSize = 1024UL * 1024UL * 4UL;
  void *low_stack = mmap(NULL, kLowStackSize, PROT_READ | PROT_WRITE,
      MAP_PRIVATE | MAP_ANONYMOUS | MAP_32BIT, -1, 0);
  struct __attribute__((packed, aligned(16))) {
    int32_t address;
    int16_t segment;
  } target = {(uint32_t) (uint64_t) code, 0x23};
  asm volatile(
      "mov %%rsp, %%r8\n"
      "mov %[stack], %%rsp\n"
      "push %%r8\n"
      "lcall *(%[target])\n"
      "pop %%rsp"
      :
      : [stack] "r" (low_stack + kLowStackSize), [target] "r" (&target)
      : "r8");

其中code 指向位于地址空间低4GiB 中的可执行页面上的一段32 位代码,0x23 是Linux x86 标头中__USER32_CS 段选择器的值。我不知道跳跃目标是否需要属性,但我添加了for good measure。当然,要使远程返回成为可能,这个调用代码本身必须位于虚拟地址空间的低 4 GiB 中的某个位置。我发现把它放到main 就足够了。

我知道这几乎是无用的(没有加载 32 位库,调用约定不同等)并且容易损坏(__USER32_CS 的值不是 Linux 面向用户空间的 API 的一部分)。

我的问题:有没有一种简单的方法来证明调用的目标确实是在 32 位模式下执行的?这种调用是否有任何实际用途(现有的库软件利用它,或者至少不是那么不切实际的可能性)?

【问题讨论】:

  • 只需将一些代码放入您的目标中,在 64 位和 32 位模式下执行不同。我会为 64 位序列 XOR EAX, EAX; DEC RAX; RAX; LRET 使用二进制代码,因为在 32 位模式下,由于 REX 前缀被解释为 DEC EAX,因此计算为 XOR EAX, EAX; DEC EAX; DEC EAX; LRET(第一个 DEC 来自前缀,第二个 @987654331 @实际的两字节操作码),因此给出不同的结果。
  • 这是一个非常棒的想法(实际上我可以手工组装)。也许您应该将其发布为答案。如果没有人提到一些在用户空间中在__USER_CS__USER32_CS 之间进行远调用的实际软件,我想我会接受你的。

标签: linux x86-64 inline-assembly memory-segmentation


【解决方案1】:

在 x86 中,32 位和 64 位指令编码基本相同

最大的例外是 16 个单字节 INCDEC 指令操作码。在 64 位模式下,这 16 个字节已重新用于 REX 前缀系列,它允许指定 64 位操作数大小以及在 64 位模式下使用新寄存器。

这意味着 64 位代码,如:

    xorl %eax, %eax
    .byte 0x48, 0xff, 0xc8
; this is the same as:
;   decq %rax         ; opcode: 0x48 0xff 0xc8
    lret $0

是有效的 32 位代码,但会被执行为:

    xorl %eax, %eax
    decl %eax         ; opcode: 0x48
    decl %eax         ; opcode: 0xff 0xc8
    lret $0

所以你可以ljmp到这段代码,并测试(32bit)返回值;如果在 64 位模式下执行,它将是 -1,如果在 32 位模式下执行,它将是 -2

不知道从32bit到64bit模式远返回的前提条件是什么。我怀疑您可能必须同时设置一个“低内存”64 位堆栈指针以及一个低内存 64 位代码地址“蹦床”(以便远调用帧中的返回 EIP 和返回 ESP 都是 32 位值)。

【讨论】:

  • 感谢您的详细解释!阅读您的评论后,我得出了大致相同的结论。需要注意的一件事——对于其他想在未来进行实验的人来说——虽然设置一个“低内存”堆栈是必要的,至少在我的机器上,不需要单独的“蹦床” ”。我相信如果需要,可以使用链接描述文件将.code 的加载地址更改为“低内存”。但是,至少在我的设置中,它默认加载在那里。
  • 在 64 位模式下,main executable 映射到地址 0x400000(检查默认链接器脚本)及以上,而 共享库 是映射在地址 >>2GB。如果您创建 lcall 的函数在您的主要可执行文件的文本段中,则几乎可以保证
  • 哦,我现在明白了。 (尽管在任何“严重”使用中,都需要生成“thunk”以在调用约定之间进行转换,例如通过编写 dlsym 的版本,该版本需要函数签名并变成位于低内存中的 thunk。不过,这样的“严重”使用还需要特殊的动态链接器和 libc,因此“不切实际”可能是轻描淡写。)
  • @KristófMarussy:“不切实际”取决于您的需求 ;-) 但同意一点也不简单,尤其是如果您想让它与动态链接一起使用,则更是如此。更容易的互操作性(不需要直接转换将是使用 x32 ABI (sites.google.com/site/x32abi),但这需要重新编译,而且它仍在进行中。我的直觉是多进程、共享内存 IPC基于 /RPC 的“thunking”解决方案会更容易实现(是的,您可以 shmat() 将相同的 SHM 段连接到 64 位和 32 位应用程序)。
猜你喜欢
  • 2015-06-20
  • 2010-09-12
  • 2012-01-19
  • 2020-08-16
  • 1970-01-01
  • 2021-11-29
相关资源
最近更新 更多