【问题标题】:Can someone explain the meaning of malloc(20 * c | -(20 * (unsigned __int64)(unsigned int)c >> 32 != 0))有人可以解释 malloc(20 * c | -(20 * (unsigned __int64)(unsigned int)c >> 32 != 0)) 的含义吗
【发布时间】:2015-12-25 00:23:38
【问题描述】:

在 IDA 生成的反编译代码中,我看到如下表达式:

malloc(20 * c | -(20 * (unsigned __int64)(unsigned int)c >> 32 != 0))
malloc(6  * n | -(3  * (unsigned __int64)(unsigned int)(2 * n) >> 32 != 0))

谁能解释这些计算的目的?
c 和 n 是 int(有符号整数)值。

更新。
原始 C++ 代码是用 MSVC 为 32 位平台编译的。
这是上面第二行反编译C代码的汇编代码(malloc(6 * ..)):

mov     ecx, [ebp+pThis]
mov     [ecx+4], eax
mov     eax, [ebp+pThis]
mov     eax, [eax]
shl     eax, 1
xor     ecx, ecx
mov     edx, 3
mul     edx
seto    cl
neg     ecx
or      ecx, eax
mov     esi, esp
push    ecx             ; Size
call    dword ptr ds:__imp__malloc

【问题讨论】:

  • 写这段代码的人应该被执行。此代码将运算符优先级的滥用提升到了一个新的水平。
  • 这不只是经过编译器优化的伪C代码吗?
  • 我怀疑这是对编译器生成的代码的某种误解。就像以优化形式对 32 -> 64 位数字进行符号扩展一样。 (c >> 32) 最有可能为零。
  • 你能参考一下你在哪里找到这样的代码吗?
  • @user3528438 显然代码是由反编译器生成的——所以反编译器一定已经执行了!

标签: c++ c assembly


【解决方案1】:

我猜原始源代码使用 C++ new 运算符分配一个数组,并使用 Visual C++ 编译。由于 user3528438 的回答表明此代码旨在防止溢出。具体来说,它是一个 32 位无符号饱和乘法。如果乘法的结果大于 32 位无符号数的最大值 4,294,967,295,则结果将被钳制或“饱和”到该最大值。

自 Visual Studio 2005 起,Microsoft 的 C++ 编译器具有 generated code to protect against overflows。例如,我可以通过使用 Visual C++ 编译以下代码来生成可以反编译为您的示例的汇编代码:

#include <stdlib.h>

void *
operator new[](size_t n) {
        return malloc(n);
}

struct S {
        char a[20];
};

struct T {
        char a[6];
};

void
foo(int n, S **s, T **t) {
        *s = new S[n];
        *t = new T[n * 2];
}

其中,使用 Visual Studio 2015 的编译器生成以下汇编代码:

    mov esi, DWORD PTR _n$[esp]
    xor ecx, ecx
    mov eax, esi
    mov edx, 20                 ; 00000014H
    mul edx
    seto    cl
    neg ecx
    or  ecx, eax
    push    ecx
    call    _malloc
    mov ecx, DWORD PTR _s$[esp+4]
; Line 19
    mov edx, 6
    mov DWORD PTR [ecx], eax
    xor ecx, ecx
    lea eax, DWORD PTR [esi+esi]
    mul edx
    seto    cl
    neg ecx
    or  ecx, eax
    push    ecx
    call    _malloc

大部分反编译的表达式实际上只处理一个汇编语句。如果前面的 MUL 指令溢出,汇编指令seto cl 将 CL 设置为 1,否则将 CL 设置为 0。类似地,如果 20 * c 的结果溢出,则表达式 20 * (unsigned __int64)(unsigned int)c &gt;&gt; 32 != 0 的计算结果为 1,否则计算结果为 0。

如果没有这种溢出保护并且20 * c 的结果确实溢出了,那么对malloc 的调用可能会成功,但分配的内存比程序预期的要少得多。然后,该程序可能会写入实际分配的内存末尾并丢弃其他内存位。这相当于缓冲区溢出,可能被黑客利用。

【讨论】:

  • 你是对的,这段代码是由 MSVC 编译的,它的反汇编列表与你的几乎相同。谢谢!
【解决方案2】:

由于这段代码是从 ASM 反编译的,所以我们只能猜测它实际上做了什么。

让我们先对其进行格式化,以便确定优先级:

malloc(20 * c | -(20 * (unsigned __int64)(unsigned int)c >> 32 != 0))
                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
                       //this is first evaluated, promoting c to 
                       //64 bit unsigned int without doing sign
                       //extension, regardless the type of c

malloc(20 * c | -(20 * (uint64_t)c >> 32 != 0))
                  ^^^^^^^^^^^^^^^^
                  //then, multiply by 20, with uint64 result

malloc(20 * c | -(20 * (uint64_t)c >> 32 != 0))
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^
                  //if 20c is greater than 2^32-1, then result is true,
                  //use -1 to generate a mask of 0xffffffff,
                  //bitwise operator | then masks 20c to 0xffffffff 
                  //(2^32-1, the maximum of size_t, input type to malloc)
                  //regardless what 20c actually is

                  //if 20c is smaller than 2^32-1, then result is false,
                  //the mask is 0, bitwise operator | keeps the final    
                  //input to malloc as 20c untouched

什么是 20 和 6?

那些可能来自常见的用法 malloc(sizeof(Something)*count)。这两个对malloc 的调用可能是在编译时将sizeof(Something)sizeof(SomethingElse) 评估为206 进行的。

那么这段代码实际上做了什么:

我的猜测,它试图防止sizeof(Something)*count 溢出并导致malloc 成功并在使用内存时导致缓冲区溢出。

通过在 64 位无符号 int 中评估产品并针对 2^32-1 进行测试,当 size 大于 2^32-1 时,malloc 的输入设置为一个非常大的值,从而保证失败 (没有 32 位系统可以分配 2^32-1 字节的内存)。

【讨论】:

  • 呃。希望这是通过反汇编优化的if (...) 语句,并且没有人直接编写代码。
  • “不管 c 的类型”——好吧,如果 c 的类型比 unsigned int 宽,那么一些信息就会丢失
【解决方案3】:

谁能解释这些计算的目的?

重要的是要理解编译会改变代码的语义。原始代码的许多未指定行为由编译过程指定。

IDA 不知道生成的汇编代码恰好做的事情是否重要。为了安全起见,它会尝试完美地复制汇编代码的行为,即使在考虑到代码的使用方式不可能发生的情况下也是如此。

在这里,IDA 可能正在复制类型转换恰好在此平台上具有的溢出特性。它不能仅仅复制原始 C 代码,因为原始 C 代码对于 cn 的某些值可能具有未指定的行为,可能是负值。

例如,假设我编写了以下 C 代码:int f(unsigned j) { return j; }。我的编译器可能会将其转换为非常简单的汇编代码,给出我的平台恰好给出的 j 负值的任何行为。

但是,如果您反编译生成的程序集,您无法将其反编译为int f(unsigned j) { return j; },因为这与我的程序集代码在具有不同溢出行为的平台上的行为不同。这可以编译为代码(在其他平台上),该代码返回的值与我的汇编代码对 j 的负值所返回的值不同。

因此,将 C 代码反编译为原始代码通常实际上是不可能的(实际上是不正确的),它通常会出现这些“可移植地复制此平台的行为”的怪异之处。

【讨论】:

    【解决方案4】:

    四舍五入到最接近的块大小。

    原谅我。它所做的是计算 c 的倍数,同时检查负值(溢出):

    #include <iostream>
    #include <cstdint>
    
    
    size_t foo(char c)
    {
        return 20 * c | -(20 * (std::uint64_t)(unsigned int)c >> 32 != 0);
    }
    
    int main()
    {
        using namespace std;
        for (char i = -4 ; i < 4 ; ++i)
        {
            cout << "input is: " << int(i) << ", result is " << foo(i) << endl;
        }
        return 0;
    }
    

    结果:

    input is: -4, result is 18446744073709551615
    input is: -3, result is 18446744073709551615
    input is: -2, result is 18446744073709551615
    input is: -1, result is 18446744073709551615
    input is: 0, result is 0
    input is: 1, result is 20
    input is: 2, result is 40
    input is: 3, result is 60
    

    乍一看,对我来说,18446744073709551615 的意义并不大。只有在看到它以十六进制表示后,我才去“啊”。 – 钟韦尔

    添加

    input is: -1, result is ffffffffffffffff
    

    【讨论】:

    • @ScottHunter 有了这样的代码,最好不要想太多。如果它有效,那很好。如果它不起作用,则将其全部扔掉并自己写一些东西。不要试图理解它。
    • @MrLister:答案来自某处;这是一个非常薄的一个没有一些详细说明。
    • 这段代码是反编译器生成的:这是对应x86编译器生成的ASM代码的C代码。我试图理解为什么会生成这样的代码 - 原始程序中的任何 malloc(N) 表达式在编译时是否暗示这些额外的对齐?
    • @leo 这是一种确保任何负值都会导致分配失败的方法。任何负值的表达式的结果都是 0xffffffffffffffff。
    • @leo 我不能说是否所有的编译器都会这样做。难道你实际看到的是内联calloc?这是确保拒绝请求超出内存模型可能容纳的块的好方法。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2017-09-10
    • 2011-12-12
    • 1970-01-01
    • 2019-05-02
    • 2013-08-01
    • 1970-01-01
    • 2011-04-08
    相关资源
    最近更新 更多