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

时间:2013-08-01 11:16:08

标签: c deobfuscation

为什么此代码会提供输出C++Sucks?它背后的概念是什么?

#include <stdio.h>

double m[] = {7709179928849219.0, 771};

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

测试here

9 个答案:

答案 0 :(得分:490)

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

01000011 00111011 01100011 01110101 01010011 00101011 00101011 01000011
+^^^^^^^ ^^^^---- -------- -------- -------- -------- -------- --------

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

由于表示使用二进制指数和尾数,因此将指数加倍会使指数递增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'

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

答案 1 :(得分:219)

更易阅读的版本:

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);
    }
}

递归调用main() 771次。

一开始,m[0] = 7709179928849219.0C++Suc;Cm[0]。在每次通话中,m[0]加倍,以“修复”最后两个字母。在最后一次调用中,C++Sucks包含m[1]的ASCII字符表示,C++Sucks仅包含零,因此m[0]字符串为main()。所有假设double m[] = {7709179928849219.0, 0}; for (int i = 0; i < 771; i++) { m[0] *= 2; } printf((char*) m); 都存储在8个字节上,因此每个char占用1个字节。

如果没有递归和非法{{1}}调用,它将如下所示:

{{1}}

答案 2 :(得分:104)

免责声明:此答案已发布到问题的原始形式,该问题仅提及C ++并包含C ++标题。问题转换为纯C是由社区完成的,没有原始提问者的意见。


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

  

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

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

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

答案 3 :(得分:56)

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

#include <stdio.h>

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

int main(){
    printf("%f\n", *(double*)string);
}

这会产生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);
}

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

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

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

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

我们的下一步可能是将每个角色的打印与找到该角色分开。通过从main生成正确的字符作为返回值,并打印出main返回的内容,我们可以非常轻松地完成此操作:

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

至少对我而言,这似乎已经足够混淆了,所以我会留下它。

答案 4 :(得分:23)

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

但是,代码不适用于每个系统,它依赖于以下一些未定义的事实:

答案 5 :(得分:11)

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

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

答案 6 :(得分:10)

其他人已经非常彻底地解释了这个问题,我想根据标准添加一个注释,这是未定义的行为

C ++ 11 3.6.1 / 3 主要功能

  

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

答案 7 :(得分:9)

代码可以像这样重写:

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

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

请注意,此代码在不同的endian表示下不起作用。此外,严格禁止调用main()

答案 8 :(得分:1)

首先,我们应该记得,双精度数字以二进制格式存储在内存中,如下所示:

(i)1位符号

(ii)指数的11位

(iii)幅度为52位

位的顺序从(i)减少到(iii)。

首先将十进制小数转换为等效的小数二进制数,然后将其表示为二进制的量级形式。

因此,数字 7709179928849219.0 变为

(11011011000110111010101010011001010110010101101000011)base 2


=1.1011011000110111010101010011001010110010101101000011 * 2^52

现在,在考虑幅度位 1。时将其忽略,因为所有幅度顺序方法均应从 1。

开始。

所以幅度部分变为:

1011011000110111010101010011001010110010101101000011 

现在 2 的幂是 52 ,我们需要在其上添加偏差号为 2 ^(指数-1的位)-1  即 2 ^(11 -1)-1 = 1023 ,因此我们的指数变为 52 + 1023 = 1075

现在,我们的代码将 2 771 次乘以数字,从而使指数增加了 771

所以我们的指数是(1075 + 771)= 1846 ,其二进制等效项是(11100110110)

现在我们的数字为正,因此我们的符号位为 0

因此,我们修改后的数字变为:

符号位+指数+大小(这些位的简单串联)

0111001101101011011000110111010101010011001010110010101101000011 

由于m转换为char指针,所以我们将从LSD中将位模式分成8个块

01110011 01101011 01100011 01110101 01010011 00101011 00101011 01000011 

(相当于十六进制的:)

 0x73 0x6B 0x63 0x75 0x53 0x2B 0x2B 0x43 

ASCII CHART 从字符图中显示的是:

s   k   c   u      S      +   +   C 

现在,一旦将其设置为m [1]为0,则表示NULL字符

现在假设您在 little-endian 机器上运行该程序(低位存储在低位地址中),因此指针m指向最低地址位,然后继续占用地址中的位卡盘8(作为强制转换为char *的类型),并且在最后一个块中计数为00000000时,printf()停止...

但是此代码不可移植。