这四行棘手的C代码背后的概念

cod*_*er1 381 c deobfuscation

为什么这段代码会输出C++Sucks?它背后的概念是什么?

#include <stdio.h>

double m[] = {7709179928849219.0, 771};

int main() {
    m[1]--?m[0]*=2,main():printf((char*)m);    
}
Run Code Online (Sandbox Code Playgroud)

在这里测试一下.

das*_*ght 492

该数字7709179928849219.0具有以下二进制表示形式为64位double:

01000011 00111011 01100011 01110101 01010011 00101011 00101011 01000011
+^^^^^^^ ^^^^---- -------- -------- -------- -------- -------- --------
Run Code Online (Sandbox Code Playgroud)

+显示标志的位置; ^指数和-尾数(即没有指数的值).

由于表示使用二进制指数和尾数,因此将数字加倍会使指数递增1.你的程序精确地完成了771次,所以从1075(十进制表示10000110011)开始的指数最终变为1075 + 771 = 1846; 1846年的二进制表示是11100110110.结果模式如下所示:

01110011 01101011 01100011 01110101 01010011 00101011 00101011 01000011
-------- -------- -------- -------- -------- -------- -------- --------
0x73 's' 0x6B 'k' 0x63 'c' 0x75 'u' 0x53 'S' 0x2B '+' 0x2B '+' 0x43 'C'
Run Code Online (Sandbox Code Playgroud)

此模式对应于您看到的打印字符串,仅向后.同时,数组的第二个元素变为零,提供null终止符,使字符串适合传递给printf().

  • @Derek x86是小端的 (95认同)
  • 为什么字符串向后? (22认同)
  • @Derek这是因为特定于平台的[endianness](http://en.wikipedia.org/wiki/Endianness#Endianness_and_hardware):抽象IEEE 754表示的字节以递减的地址存储在内存中,因此字符串打印正确.在具有大字节序的硬件上,需要以不同的数字开头. (16认同)
  • @AlvinWong你是对的,标准不要求IEEE 754或任何其他特定格式.这个程序大概是不可移植的,或者非常接近它:-) (14认同)
  • @GrijeshChauhan我使用了[双精度IEEE754计算器](http://www.binaryconvert.com/convert_double.html):我粘贴了`7709179928849219`值,并得到了二进制表示. (10认同)
  • @JimBalter技能水平低的人有什么不可思议的?人们必须从某个地方开始.你也不是一个C++大师. (7认同)
  • C++标准是否要求在IEEE 754中表示浮点数?维基百科说没有...... (4认同)
  • @dasblinkenlight你是如何生成二进制文件的?只是读取位模式? (3认同)
  • 不知道我是否应该给你+1来搞清楚,或者-1因为认为它值得花时间. (3认同)
  • @EdwardFalk首先写它需要时间; 计算出来的时间不到五分钟,所有计算器都可以在线使用. (2认同)
  • @ us2012,Jim对于"快车道"的学习风格是绝对正确的.了解如何在硬件上表示和处理数据是体面课程中教授的第一件事,或者是由真正感兴趣和好奇的学生发现的事情之一.自称为"IT大师"的数量炫耀他们使用一些高级框架或语言作为"知识渊博"或"专家"是令人作呕的,并且对他们所站立的巨人的肩膀是一种伤害. (2认同)

Ada*_*zyk 221

更易阅读的版本:

double m[2] = {7709179928849219.0, 771};
// m[0] = 7709179928849219.0;
// m[1] = 771;    

int main()
{
    if (m[1]-- != 0)
    {
        m[0] *= 2;
        main();
    }
    else
    {
        printf((char*) m);
    }
}
Run Code Online (Sandbox Code Playgroud)

它递归调用main()771次.

在开始的时候,m[0] = 7709179928849219.0,这代表C++Suc;C.在每次通话中,m[0]加倍,以"修复"最后两个字母.在最后一次通话,m[0]包含的ASCII字符表示C++Sucks,并m[1]只包含零,所以它有一个空结束C++Sucks字符串.所有假设m[0]都存储在8个字节中,因此每个char占用1个字节.

没有递归和非法main()调用它将如下所示:

double m[] = {7709179928849219.0, 0};
for (int i = 0; i < 771; i++)
{
    m[0] *= 2;
}
printf((char*) m);
Run Code Online (Sandbox Code Playgroud)

  • 这是postfix减量.所以它将被称为771次. (8认同)

Rei*_*ica 105

免责声明:这个答案被发布到问题的原始形式,其中仅提到C++并包含C++标题.问题转换为纯C是由社区完成的,没有原始提问者的意见.


从形式上讲,这个程序是不可能的,因为它是不正确的(即它不是合法的C++).它违反了C++ 11 [basic.start.main] p3:

函数main不得在程序中使用.

除此之外,它依赖于这样的事实:在典型的消费者计算机上,a double是8字节长,并且使用某种众所周知的内部表示.计算阵列的初始值,以便在执行"算法"时,第一个的最终值double将使得内部表示(8个字节)将是8个字符的ASCII码C++Sucks.然后是数组中的第二个元素0.0,其第一个字节0位于内部表示中,使其成为有效的C样式字符串.然后使用它将其发送到输出printf().

在硬件上运行此操作,其中一些上述操作不会导致垃圾文本(或者甚至是访问超出范围).

  • 我必须补充一点,这不是C++ 11的发明--C++ 03也有`basic.start.main` 3.6.1/3,措辞相同. (25认同)

Jer*_*fin 56

也许理解代码的最简单方法是反过来解决问题.我们将首先打印一个字符串 - 为了平衡,我们将使用"C++ Rocks".关键点:就像原版一样,它的长度恰好是八个字符.由于我们将(大致)像原始一样,并以相反的顺序打印出来,我们将从相反的顺序开始.对于我们的第一步,我们只是将位模式视为a double,并打印出结果:

#include <stdio.h>

char string[] = "skcoR++C";

int main(){
    printf("%f\n", *(double*)string);
}
Run Code Online (Sandbox Code Playgroud)

这产生了3823728713643449.5.因此,我们希望以某种不明显的方式操纵它,但很容易逆转.我将半任意选择乘以256,这给了我们978874550692723072.现在,我们只需要编写一些混淆代码来除以256,然后以相反的顺序打印掉它的各个字节:

#include <stdio.h>

double x [] = { 978874550692723072, 8 };
char *y = (char *)x;

int main(int argc, char **argv){
    if (x[1]) {
        x[0] /= 2;  
        main(--x[1], (char **)++y);
    }
    putchar(*--y);
}
Run Code Online (Sandbox Code Playgroud)

现在我们有很多转换,将参数传递给(递归)main,这些参数被完全忽略(但是获得增量和减量的评估是至关重要的),当然还有完全随意的数字来掩盖我们正在做的事实非常简单明了.

当然,由于整点都是混淆,如果我们觉得这样,我们也可以采取更多步骤.例如,我们可以利用短路评估,将我们的if语句转换为单个表达式,因此main的主体看起来像这样:

x[1] && (x[0] /= 2,  main(--x[1], (char **)++y));
putchar(*--y);
Run Code Online (Sandbox Code Playgroud)

对于那些不习惯混淆代码(和/或代码高尔夫)的人来说,这开始看起来确实很奇怪 - 计算和丢弃and一些无意义的浮点数和返回值的逻辑,main甚至没有返回值.更糟糕的是,如果没有意识到(和思考)短路评估是如何工作的,那么它如何避免无限递归甚至可能不会立即显而易见.

我们的下一步可能是将每个角色与发现该角色分开打印.我们可以通过生成正确的字符作为返回值main,并打印出返回值来轻松地做到这一点main:

x[1] && (x[0] /= 2,  putchar(main(--x[1], (char **)++y)));
return *--y;
Run Code Online (Sandbox Code Playgroud)

至少在我看来,这似乎足够混淆,所以我会留下它.


D.R*_*.R. 23

它只是构建一个双数组(16个字节) - 如果解释为char数组 - 为字符串"C++ Sucks"构建ASCII代码

但是,代码不能在每个系统上运行,它依赖于以下一些未定义的事实:


Ser*_*sen 11

打印以下代码C++Suc;C,因此整个乘法仅适用于最后两个字母

double m[] = {7709179928849219.0, 0};
printf("%s\n", (char *)m);
Run Code Online (Sandbox Code Playgroud)


Yu *_*Hao 10

其他人已经非常彻底地解释了这个问题,我想补充说明这是根据标准的未定义行为.

C++ 11 3.6.1/3 主要功能

函数main不得在程序中使用.main的链接(3.5)是实现定义的.将main定义为已删除或将main声明为内联,静态或constexpr的程序是不正确的.名称main不以其他方式保留.[示例:成员函数,类和枚举可以称为main,其他名称空间中的实体也可以称为main. - 末端的例子]


Jac*_*ley 9

代码可以像这样重写:

void f()
{
    if (m[1]-- != 0)
    {
        m[0] *= 2;
        f();
    } else {
          printf((char*)m);
    }
}
Run Code Online (Sandbox Code Playgroud)

它正在做的是在double数组m中产生一组字节,恰好对应于字符'C++ Sucks',后跟一个空终止符.他们通过选择一个double值来模糊代码,当加倍771次时,在标准表示中产生的字节集与数组的第二个成员提供的null终止符相同.

请注意,此代码在不同的endian表示下不起作用.此外,main()不严格允许通话.

  • 为什么你的`f`返回一个`int`? (3认同)