【问题标题】:What is the decompiled (C) code construct of this assembly x86 code?这个汇编 x86 代码的反编译 (C) 代码结构是什么?
【发布时间】:2019-09-09 14:12:43
【问题描述】:

此代码将字符串的每个字符(位于ebp+arg_0)与不同的常量(ASCII 字符)如“I”、“o”和“S”进行比较。我猜,基于其他代码部分,这段代码最初是用 C 编写的。

这个比较代码部分看起来非常低效。 我的问题,您认为这段代码在 C 中的外观如何?最初使用的是什么代码结构?到目前为止我的想法

  • 这不是 for 循环。因为我没有看到任何向上跳跃和停止条件。

  • 这不是 while/case/switch 代码构造

  • 我最好的猜测是,这是很多连续的 if/else 语句。 你能帮忙吗?

是的,这是挑战的一部分,我已经有了标志/解决方案,不用担心。只是想更好地理解代码。

【问题讨论】:

  • 如果这是某种crackme,那么它应该没有效率。 crackmes 的全部意义在于迷惑研究人员。
  • 你可以使用定时攻击来找到正确的密码

标签: c assembly x86 reverse-engineering decompiling


【解决方案1】:

这不是一个 for 循环。因为我没有看到任何向上的跳跃和停止条件。

正确。

不是while/case/switch代码构造

不可能,它比较了数组的不同索引。

我最好的猜测是,这是很多连续的 if/else。你能帮忙吗?

看起来可能是这样的代码:

void f(const char* arg_0) {
    if(arg_0[4] == 'I' && arg_0[5] == 'o' && arg_0[6] == 'S') {
        printf("Gratz man :)");
        exit(0); //noreturn, hence your control flow ends here in the assembly
    }
    puts("Wrong password"); // Or `printf("Wrong password\n");` which gets optimized to `puts`
    // leave, retn
}

This is how gcc compiles it without optimizations:

.LC0:
        .string "Gratz man :)"
.LC1:
        .string "Wrong password"
f(char const*):
        push    ebp
        mov     ebp, esp
        sub     esp, 8
        mov     eax, DWORD PTR [ebp+8]
        add     eax, 4
        movzx   eax, BYTE PTR [eax]
        cmp     al, 73
        jne     .L2
        mov     eax, DWORD PTR [ebp+8]
        add     eax, 5
        movzx   eax, BYTE PTR [eax]
        cmp     al, 111
        jne     .L2
        mov     eax, DWORD PTR [ebp+8]
        add     eax, 6
        movzx   eax, BYTE PTR [eax]
        cmp     al, 83
        jne     .L2
        sub     esp, 12
        push    OFFSET FLAT:.LC0
        call    printf
        add     esp, 16
        sub     esp, 12
        push    0
        call    exit
.L2:
        sub     esp, 12
        push    OFFSET FLAT:.LC1
        call    puts
        add     esp, 16
        nop
        leave
        ret

看起来与您的反汇编代码非常相似。

这个比较代码部分看起来效率很低

看起来它是在没有优化的情况下编译的。启用优化后,gcc compiled the code to

.LC0:
        .string "Gratz man :)"
.LC1:
        .string "Wrong password"
f(char const*):
        sub     esp, 12
        mov     eax, DWORD PTR [esp+16]
        cmp     BYTE PTR [eax+4], 73
        jne     .L2
        cmp     BYTE PTR [eax+5], 111
        je      .L5
.L2:
        mov     DWORD PTR [esp+16], OFFSET FLAT:.LC1
        add     esp, 12
        jmp     puts
.L5:
        cmp     BYTE PTR [eax+6], 83
        jne     .L2
        sub     esp, 12
        push    OFFSET FLAT:.LC0
        call    printf
        mov     DWORD PTR [esp], 0
        call    exit

不知道为什么 gcc 决定跳下并再次备份,而不是直线 jnes。此外,ret 消失了,您的 printf 得到了尾调用优化,即 jmp printf 而不是 call printf,后跟 ret

【讨论】:

  • 哇,这确实很接近!感谢您的支持!
  • puts 添加换行符,printf 没有。 (并且 gcc 不会寻找使用 fputs(str, stdout) 来优化不以换行符结尾的常量字符串。)无论如何,asm 使用 puts,最清楚的是只使用 puts。无论如何,缺少 "\n" 是您的 gcc 版本没有得到优化的原因。
  • @PeterCordes 谢谢,我已经编辑它以使用 puts。我没有意识到puts 附加了一个换行符。
  • 如果不是查看编译器 asm 输出并了解该优化,我可能不会那么容易记住 puts :P 现代 gcc 甚至可以处理 printf("%s\n", "string"),甚至对于"string" 是常量传播后具有编译时常量值的变量的情况。但是我确实有时会输入puts(尤其是在玩具/实验代码中),因为它的名称比printf短,因为它附加了一个我不必输入的\n,并且因为它不受用于输出运行时变量字符串的格式字符串漏洞的影响。
【解决方案2】:

第一个参数 (arg_0) 是指向给定密码字符串的指针,例如,const char *arg_0。该指针(第一个字符的地址)被加载到eax 寄存器(mov eax, [ebp+arg_0])中,然后将当前字符的索引添加到该索引(add eax, 4 等)。然后将该地址处的单个字节加载到eax (movzx eax, byte ptr [eax])。

然后将该字节/字符与正确的字节/字符(cmp eax, 'I' 等)进行比较。如果结果不为零(即,如果它们不相等),则程序跳转到“密码错误”分支(jnz - 如果不为零则跳转),否则继续进行下一个比较(最终成功) .

因此,最接近的直接 C 等效项将类似于:

void check(const char *arg_0) {
    // presumably comparisons 0-3 are omitted
    if (arg_0[4] != 'I') goto fail;
    if (arg_0[5] != 'o') goto fail;
    if (arg_0[6] != 'S') goto fail;
    printf("Gratz man :)");
    exit(0);
fail:
    puts("Wrong password");
}

(当然,实际的 C 代码不太可能是这样的,因为 goto fail 的排列方式并不典型。)

【讨论】:

  • 谢谢!当我反编译时,汇编代码看起来几乎相同。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-02-08
  • 2019-02-27
  • 2011-06-14
  • 1970-01-01
  • 2015-12-21
相关资源
最近更新 更多