【问题标题】:Design a function in C from assembly code从汇编代码用 C 语言设计一个函数
【发布时间】:2020-03-06 16:09:32
【问题描述】:

我需要用C语言设计一个函数来实现机器码写的东西。我分步进行组装操作,但据说我的功能实现错误。我很迷惑。

这是函数的反汇编代码。

(Hand transcribed from an image, typos are possible
 especially in the machine-code.  See revision history for the image)
0000000000000000 <ex3>:
   0:   b9 00 00 00 00       mov    0x0,%ecx
   5:   eb 1b                jmp    L2  // 22 <ex3+0x22>
   7:   48 63 c1     L1:     movslq %ecx,%rax
   a:   4c 8d 04 07          lea    (%rdi,%rax,1),%r8
   e:   45 0f b6 08          movzbl (%r8),%r9d
  12:   48 01 f0             add    %rsi,%rax
  15:   44 0f b6 10          movzbl (%rax),%r10d
  19:   45 88 10             mov    %r10b,(%r8)
  1c:   44 88 08             mov    %r9b,(%rax)
  1f:   83 c1 01             add    $0x1,%ecx
  22:   39 d1        L2:     cmp    %edx,%ecx
  24:   7c e1                jl     L1   // 7 <ex3+0x7>
  26:   f3 c3                repz retq

我的代码(函数的签名没有给出或解决):

#include <assert.h>

int
ex3(int rdi, int rsi,int edx, int r8,int r9 ) {
    int ecx = 0;
    int rax;
    if(ecx>edx){
         rax = ecx;
        r8 =rdi+rax;
        r9 =r8;
        rax =rsi;
        int r10=rax;
        r8=r10;
        rax =r9;
        ecx+=1;
    }
    return rax;
}

如果您发现任何错误,请解释导致错误的原因。

【问题讨论】:

  • r 寄存器是 64 位的。 int 是 x86-64 C 实现中的 32 位类型。如果它实际上是int,你会使用int edi,就像你对edx所做的那样。更重要的是,asm 包含一个向后条件分支,但您的 C 缺少一个循环。此外,asm 有一堆从内存中加载的零扩展字节,但你的 C 没有任何指针变量。
  • 请编辑您的问题以包含 asm 代码。不要发布指向图片的链接,也不要用图片本身替换该链接。花点时间在问题中输入它。
  • 那个 ASM 代码看起来像一个循环。
  • 我对您的问题投了反对票,因为您发布了一张代码图片。一旦您将其替换为文本形式的代码,将删除我的反对票。
  • @fuz:Steve Friedl 努力将图片翻译成文字。我删除了图片,所以现在问题应该没问题了。

标签: c assembly x86-64 reverse-engineering


【解决方案1】:

我很确定是这样的:交换两个内存区域:

void memswap(unsigned char *rdi, unsigned char *rsi, int edx) {
        int ecx;
        for (ecx = 0; ecx < edx; ecx++) {
                unsigned char r9 = rdi[ecx];
                unsigned char r10 = rsi[ecx];
                rdi[ecx] = r10;
                rsi[ecx] = r9;
        }
}

【讨论】:

    【解决方案2】:

    (编者注:这是解决循环结构的部分答案。它不包括movzbl字节加载,或者其中一些变量是指针的事实,或者类型宽度。还有其他答案可以涵盖问题的其他部分。)


    C 支持goto,尽管它们的用法经常不受欢迎,但它们在这里非常有用。使用它们使其与组件尽可能相似。这允许您在开始引入更合适的控制流机制(如 while 循环)之前确保代码正常工作。所以我会做这样的事情:

        goto L2;
    L1:
        rax = ecx;
        r8 =rdi+rax;
        r9 =r8;
        rax =rsi;
        int r10=rax;
        r8=r10;
        rax =r9;
        ecx+=1;
    L2:
        if(edx<ecx) 
            goto L1;
    

    您可以轻松地将上面的代码转换为:

    while(edx<ecx) {
        rax = ecx;
        r8 =rdi+rax;
        r9 =r8;
        rax =rsi;
        int r10=rax;
        r8=r10;
        rax =r9;
        ecx+=1;
    }
    

    请注意,我没有检查 L1 块中的代码是否正确,然后是 while 块。 (编者注:它缺少所有的内存访问)。但是你的跳跃是错误的,现在已经纠正了。

    您可以从这里(再次假设这是正确的)开始尝试查看模式。似乎ecx 被用作某种索引变量。并且变量rax可以在一开始就被替换。我们可以做一些其他类似的改变。这给了我们:

    int i=0;
    while(edx<i) {
                        // rax = ecx;
                        // r8 =rdi+i; // r8=rdi+i
                        // r9 = rdi + i; // r9 = r8
                        // rax =rsi;
        int r10 = rsi;  // int r10=rax;
        r8 = r10;
        rax = r9 = rdi+i;
        i++;
    }
    

    这里显然有些事情有点可疑。 while 条件是edx&lt;i,但i递增,而不是每次迭代都递减。这是一个很好的迹象,表明有问题。我在组装方面不够熟练,无法弄清楚,但至少这是您可以使用的方法。 一步一步来吧。

    add $0x1,%ecx 是 AT&T 语法,用于将 ecx 递增 1。根据 this site 使用 Intel 语法,结果存储在 first 操作数中。在 AT&T 语法中,这是 last 操作数。

    需要注意的一件有趣的事情是,如果我们删除 goto L2 语句,这将等效于

    do {
        // Your code
    } while(edx<ecx);
    

    while 循环可以编译为带有附加 goto 的 do-while 循环。 (见Why are loops always compiled into "do...while" style (tail jump)?)。这很容易理解。

    在汇编中,循环是由在代码中向后跳转的 goto 构成的。您进行测试,然后决定是否要跳回。所以为了在第一次迭代之前进行测试,需要先跳转到测试。 (编译器有时也会编译顶部带有if()break 底部的jmp 的while 循环。但仅限于禁用优化。请参阅While, Do While, For loops in Assembly Language (emu8086)

    前向跳转通常是编译 if 语句的结果。

    我也刚刚意识到我现在有了三种使用 goto 的好方法。前两个是打破嵌套循环并以相反的分配顺序释放资源。现在是第三个,当你对汇编进行逆向工程时。

    【讨论】:

    • 三个好方法在C中实际使用goto?或者你只是说作为逆向工程的一部分?问题中的 asm 是 GCC 如何在 C 中的 -O0-O1 编译 while(){}for 循环。 (在 C++ 模式下,g++ -O0 喜欢创建一个愚蠢的循环,顶部是 if()break,底部是无条件的 jmp。)另见 Why are loops always compiled into "do...while" style (tail jump)? - 启用优化后,编译器会剥离第一个测试,然后使用底部的条件创建一个正常的do{}while() 循环,如下所示。
    • 顺便说一句,这只是一个非常部分的答案。您很好地介绍了循环结构,但类型和寻址模式仍然是一个大问题。 OP 甚至没有接近正确加载 movzbl 字节。
    • 我现在意识到了跳跃和循环。但我更关心如何在 C 中显示不同长度的位寄存器。以及如何在 C 中显示像 movzbl 这样的操作。我是否使用任何转换器?
    • @PeterCordes 我的意思是逆向工程的一部分。是的,我知道这是部分答案。剩下的事情我感觉不舒服。
    • 我为你提到的部分投了赞成票。这完全没问题,只要我们避免暗示这是唯一的问题。否则 OP 会浪费时间希望你的改变就足够了。
    【解决方案3】:

    对于那些喜欢 GCC 的 .S 格式的人,我使用了:

    ex3:
      mov $0x0, %ecx
      jmp lpe
      lps:
          movslq %ecx, %rax
          lea (%rdi, %rax, 1), %r8
          movzbl (%r8), %r9d
          add %rsi, %rax
          movzbl (%rax), %r10d
          mov %r10b, (%r8)
          mov %r9b, (%rax)
          add $0x1, %ecx
      lpe:
      cmp %edx, %ecx
      jl lps
    repz retq
    
    
    .data
    .text
    .global _main
    _main:
        mov $0x111111111111, %rdi
        mov $0x222222222222, %rsi
        mov $0x5, %rdx
        mov $0x333333333333, %r8
        mov $0x444444444444, %r9
        call ex3
        xor %eax, %eax
        ret
    

    然后您可以使用gcc main.S -o main 编译它并运行objdump -x86-asm-syntax=intel -d main 以intel 格式查看它,或者在反编译器中运行生成的main 可执行文件.. 但是嗯.. 让我们做一些手动工作..

    首先,我会将 AT&T 语法转换为更常见的 Intel 语法。所以:

    ex3:
      mov ecx, 0
      jmp lpe
      lps:
          movsxd rax, ecx
          lea r8, [rdi + rax]
          movzx r9d, byte ptr [r8]
          add rax, rsi
          movzx r10d, byte ptr [rax]
          mov byte ptr [r8], r10b
          mov byte ptr [rax], r9b
          add ecx, 0x1
      lpe:
      cmp ecx, edx
      jl lps
    rep ret
    

    现在我可以清楚地看到,从lps(循环开始)到lpe(循环结束),是一个for循环。

    怎么样?因为首先它将计数器寄存器 (ecx) 设置为 0。然后它通过执行 cmp ecx, edx 后跟 jl (如果小于则跳转)来检查 ecx &lt; edx。如果是,它运行代码并将ecx 增加 1 (add ecx, 1).. 如果不存在,则存在该块..

    因此它看起来像:for (int32_t ecx = 0; ecx &lt; edx; ++ecx)..(注意 edx 是 rdx 的低 32 位)。

    所以现在我们用以下知识翻译其余部分:

    r10 是一个 64 位寄存器。 r10d 是高 32 位,r10b 是低 8 位。 r9 是一个 64 位寄存器。与r10 相同的逻辑适用。

    所以我们可以像下面这样表示一个寄存器:

    typedef union Register
    {
        uint64_t reg;
        struct
        {
            uint32_t upper32;
            uint32_t lower32;
        };
    
        struct
        {
            uint16_t uupper16;
            uint16_t ulower16;
            uint16_t lupper16;
            uint16_t llower16;
        };
    
        struct
        {
            uint8_t uuupper8;
            uint8_t uulower8;
    
            uint8_t ulupper8;
            uint8_t ullower8;
    
            uint8_t luupper8;
            uint8_t lulower8;
    
            uint8_t llupper8;
            uint8_t lllower8;
        };
    } Register;
    

    哪个更好..你可以自己选择.. 现在我们可以开始查看说明本身了.. movsxdmovslq 将 32 位寄存器移动到带有符号扩展名的 64 位寄存器中。

    现在我们可以写代码了:

    uint8_t* ex3(uint8_t* rdi, uint64_t rsi, int32_t edx)
    {
        uintptr_t rax = 0;
        for (int32_t ecx = 0; ecx < edx; ++ecx)
        {
            rax = ecx;
            uint8_t* r8 = rdi + rax;
            Register r9 = { .reg = *r8 }; //zero extend into the upper half of the register
            rax += rsi;
    
            Register r10 = { .reg = *(uint8_t*)rax }; //zero extend into the upper half of the register
            *r8 = r10.lllower8;
            *(uint8_t*)rax = r9.lllower8;
        }
    
        return rax;
    }
    

    希望我没有搞砸任何事情..

    【讨论】:

    • 非常感谢。我对这些很陌生,你知道如何用 C 编写 reinterpret_cast&lt;int32_t*&gt; 或只是 int8_t int64_t 等吗?我不知道如何显示不同的位寄存器。 union Register 部分一开始是做什么的?
    • @chloezql 添加了 C 翻译代码并删除了 C++ 代码。 int64_t&lt;stdint.h&gt; 中声明
    • IMO 您的寄存器联合应该更简单,并且只包括可作为 x86 部分寄存器直接访问的成员。因此,您需要的唯一结构成员是一对 8 位 AL/AH 半部分。
    • 你是如何从 OP 的汇编中获得 int64_t* r8 的?我只看到字节指针 (uint8_t*)。即使是指针数学也没有缩放,所以再次char*unsigned char*。你用铸造严重地过度复杂化了。
    • 哦,我看到你把 movzbl 误翻译成了 mov r9d, dword ptr [r8]。其实是movzx r9d, byte ptr [r8]
    猜你喜欢
    • 1970-01-01
    • 2014-08-16
    • 2013-04-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-08-03
    • 2013-11-21
    • 1970-01-01
    相关资源
    最近更新 更多