如何对2001年IOCCC的获胜者ctk.c代码进行去混淆?

我见过ctk.c混淆代码,但是如何开始对其进行去混淆呢?

#include  #include  #include  #include  #include  #define m(b)a=b;z=*a;while(*++a){y=*a;*a=z;z=y;} #define h(u)G=u<=w[21])exit(0);if(g!=G){struct itimerval t= {0,0,0,0};g+=((g<G)<> 3)+1);setitimer(0,&t,0);f&&printf("\e[10;%u]",g+24);}f&&putchar(7);s+=(9-w[21] )*((g>>3)+1);o=p;m(x);m(w);(n=rand())&255||--*w||++*w;if(!(**P&&P++||n&7936)){ while(abs((X=rand()%76)-*x+2)-*w<6);++X;P=T;}(n=rand()&31)<3&&(d=n);!d&&--*x79&&(--*x,--d);signal(i,u);}void e(){signal(14, SIG_IGN);printf("\e[0q\ecScore: %u\n",s);system("stty echo -cbreak");}int main (int C,char**V){atexit(e);(C<2||*V[1]!=113)&&(f=(C=*(int*)getenv("TERM"))==( int)0x756E696C||C==(int)0x6C696E75);srand(getpid());system("stty -echo cbreak" );h(0);u(14);for(;;)switch(getchar()){case 113:return 0;case 91:case 98:c(44,k =-1);case 32:case 110:c(46,k=0);case 93:case 109:c(47,k=1);c(49,h(0));c(50,h(1 ));c(51,h(2));c(52,h(3));}} 

http://www.ioccc.org/2001/ctk.hint :

 This is a game based on an Apple ][ Print Shop Companion easter egg named 'DRIVER', in which the goal is to drive as fast as you can down a long twisty highway without running off the road. Use ',./', '[ ]', or 'bnm' to go left, straight, and right respectively. Use '1234' to switch gears. 'q' quits. The faster you go and the thinner the road is, the more points you get. Most of the obfuscation is in the nonsensical if statements among other things. It works best on the Linux console: you get engine sound (!) and the * Lock keyboard lights tell you what gear you're in (none lit=4th). The 'q' argument (no leading '-') will silence the sound. It won't work on a terminal smaller than 80x24, but it works fine with more (try it in an XTerm with the "Unreadable" font and the window maximized vertically!). 

第一步

使用:

 sed -e'/#include/d' ctk.c | gcc -E - | sed -e's/;/;\n/g' -e's/}/}\n/g' -e '/^#/d' | indent 

我能够生成以下输出,虽然不完美已经似乎可读性更好:

 char x[] = "((((((((((((((((((((((", w[] = "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"; char r[] = { 92, 124, 47 } , l[] = { 2, 3, 1, 0} ; char *T[] = { " |", " |", "%\\|/%", " %%%", "" } ; char d = 1, p = 40, o = 40, k = 0, *a, y, z, g = -1, G, X, **P = &T[4], f = 0; unsigned int s = 0; void u (int i) { int n; printf ("\233; %uH\233L%c\233; %uH%c\233; %uH%s\23322; %uH@\23323; %uH \n", *x - *w, r[d], *x + *w, r[d], X, *P, p += k, o); if (abs (p - x[21]) >= w[21]) exit (0); if (g != G) { struct itimerval t = { 0, 0, 0, 0 } ; g += ((g < G) << 1) - 1; t.it_interval.tv_usec = t.it_value.tv_usec = 72000 / ((g >> 3) + 1); setitimer (0, &t, 0); f && printf ("\e[10; %u]", g + 24); } f && putchar (7); s += (9 - w[21]) * ((g >> 3) + 1); o = p; a = x; z = *a; while (*++a) { y = *a; *a = z; z = y; } ; a = w; z = *a; while (*++a) { y = *a; *a = z; z = y; } ; (n = rand ()) & 255 || --*w || ++*w; if (!(**P && P++ || n & 7936)) { while (abs ((X = rand () % 76) - *x + 2) - *w < 6); ++X; P = T; } (n = rand () & 31) < 3 && (d = n); !d && --*x <= *w && (++*x, ++d) || d == 2 && ++*x + *w > 79 && (--*x, --d); signal (i, u); } void e () { signal (14, SIG_IGN); printf ("\e[0q\ecScore: %u\n", s); system ("stty echo -cbreak"); } int main (int C, char **V) { atexit (e); (C < 2 || *V[1] != 113) && (f = (C = *(int *) getenv ("TERM")) == (int) 0x756E696C || C == (int) 0x6C696E75); srand (getpid ()); system ("stty -echo cbreak"); G = 0 << 3; printf ("\e[%uq", l[0]); u (14); for (;;) switch (getchar ()) { case 113: return 0; case 91: case 98: case 44: k = -1; continue; case 32: case 110: case 46: k = 0; continue; case 93: case 109: case 47: k = 1; continue; case 49: G = 0 << 3; printf ("\e[%uq", l[0]); continue; case 50: G = 1 << 3; printf ("\e[%uq", l[1]); continue; case 51: G = 2 << 3; printf ("\e[%uq", l[2]); continue; case 52: G = 3 << 3; printf ("\e[%uq", l[3]); continue; } } 

... 现在?

我认为此时自动化流程无法执行,因为从现在开始,术语“更”可读或“更少”可读取可能取决于读者的具体偏好。

可以执行的一个步骤是从字符串中删除转义序列并将它们分别放在某处。 事实certificate整体而言

 char l[] = {2, 3, 1, 0} 

没有其他目的,只能在主循环的转义序列中使用:

 printf ("\e[%uq", l[0]); 

等等。 抬头看看他们的意思:

 ESC [ 0 q: clear all LEDs ESC [ 1 q: set Scroll Lock LED ESC [ 2 q: set Num Lock LED ESC [ 3 q: set Caps Lock LED 

根据您的口味,您可能希望将它们与宏或函数调用交换,对您更有意义,如clear_all_LEDs等。

我强烈怀疑机器会同意这是一种简化。 事实certificate,整个主循环似乎正在使用用户输入的密钥,因此将数字转换为相应的字符可能会增加可读性,例如替换:

 case 113: return 0; case 91: case 98: case 44: k = -1; // ... case 49: G = 0 << 3; printf ("\e[%uq", l[0]); 

有类似的东西:

 case 'q': return 0; case '[': case 'b': case ',': k = -1; // ... case '1': G = 0 << 3; set_Num_Lock_LED (); 

哦 - 虽然我们已经到了,但为什么我们不想将这个名字从这个相当奇怪的G改为gear 。 我再次强烈怀疑自动化过程是否会发现从G更改为gear比将其重命名为butterfly更好。 好吧也许它甚至不是。

虽然美化名称可能是由单个u引用的此函数是另一个候选者:

 u (14); 

有可能更有意义的名称update 。 因为我们已经包含为什么我们不通过用SIGALRM替换14来进一步反混淆代码:

 upadate (SIGALRM); 

正如您所看到的,“反混淆”需要与之前完全相反的步骤。 这次用宏替换扩展。 机器如何确定哪一个更有用?

我们可能想要用其他东西替换裸号的另一个地方是更新function中的这个:

 f && putchar (7); 

为什么不用\a替换7 ,因为它最终会变成相同的。 也许我们甚至应该用更“有意义”的东西改变裸露的f

我再次投票反对butterfly但更愿意称之为play_sound

 if (play_sound) putchar ('\a'); 

可能是我们正在寻找的更具可读性的版本。 当然,我们不应该忘记在所有其他位置替换f。 在我们的主要function开始时就是这样一个罪魁祸首:

圣洁的混乱

 int main (int C, char **V) { atexit (e); (C < 2 || *V[1] != 113) && (f = (C = *(int *) getenv ("TERM")) == (int) 0x756E696C || C == (int) 0x6C696E75); 

虽然愉快地将f重命名为play_sounde仍然没有butterfly ,但这次我宁愿称之为: - end我们发现函数签名在命名约定方面看起来有点奇怪: argc而不是Cargv而不是V在这里似乎更常规。 从而给我们:

 int main (int argc, char* argv[]) { atexit (end); (argc < 2 || *argv[1] != 113) && (playsound = (argc = *(int *) getenv ("TERM")) == (int) 0x756E696C || argc == (int) 0x6C696E75); 

由于这仍然不是一种美,我们问我们的标准人,他告诉我们,替换它是可以的

 (A || B) && (C) 

 if (A || B) { C } 

 E = (x=F)==H || x==I 

 x = F; if (x==H || x==I) A=1; else A=0;` 

所以也许这应该是整个代码的更易读的版本:

 if (argc < 2 || *argv[1] != 'q') { argc = *(int*) getenv ("TERM"); if (argc == (int) 0x756E69 || argc == (int) 0x6C696E75)) play_sound = 1; /* skip the else brach here as play_sound is alredy initialized to 0 */ } 

现在还有另一个人出现并开始通知我们,根据名为endianness的东西,如果存储在内存中的奇怪的数字0x6C696E75和0x756E69(当将原始字节值解释为ASCII代码时)看起来像"unil""unil" 。 一个是在一个architecure类型上“unil”而另一个是“linu”,而在另一个具有不同endianness的架构上反过来。

所以仔细看看这里发生的事情是:

  • 我们得到一个指向getenv(“TERM”)字符串的指针,我们在取消引用之前将其打印到指向int的指针,从而将存储在字符串位置的位模式作为int引导。
  • 接下来,我们将这个值与我们将获得的值进行比较,如果使用存储在该特定位置的“unil”或“linu”执行相同的操作。

可能我们只想检查TERM环境变量是否设置为“linux”,因此我们的反混淆版本可能希望在此处执行字符串比较。

另一方面,我们无法确定是否允许名称以“unil”开头的终端播放声音可能是该软件的一个特殊function,所以我决定最好保持原样。

现在怎么办 ?

在重命名和重新编码变量名称和值时,这些奇怪的char数组可能是我们的下一个受害者。 下面的混乱看起来不太好:

 char x[] = "((((((((((((((((((((((", w[] = "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"; char r[] = { 92, 124, 47 }; 

所以也许他们可以改为:

 char x_offset[] = { 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 0 }; char width[] = { 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 0 }; const char border[] = "\\|/"; 

正如你所看到的,我只是选择将x之间描述的值作为字符串常量切换为x作为数组写下来的方式,这样存储在这里的值的目的对我来说似乎有点清楚。

而另一方面,我改变了r的方式,恰好在完全相反的方向上写下来,这对我来说似乎更加清晰。

在搜索所有那些refs到xwr时,可以使用时间将po重命名为 - 抱歉再次没有butterfly - posold_pos重命名s score

改变例如:

  s += (9 - w[21]) * ((g >> 3) + 1); o = p; a = x; z = *a; while (*++a) { y = *a; *a = z; z = y; } ; a = w; z = *a; while (*++a) { y = *a; *a = z; z = y; } ; 

至:

  /* update score */ score += (9 - width[NEXT_LINE]) * ((g >> 3) + 1); old_pos = pos; /* shift x_offset */ a = x_offset; z = *a; while (*++a) { y = *a; *a = z; z = y; }; /* shift width */ a = width; z = *a; while (*++a) { y = *a; *a = z; z = y; }; 

除了将其转换为其他类型的循环的可能性之外,对于两个移位函数都没有太多的美化,因此可能添加适当的注释是您可以做的最大值。 删除幻数21可能是另一个想法NEXT_LINE似乎不是这里最糟糕的选择。

标记为变量g的单个字符看起来仍然不太好。 但是将它重命名为update_interval还有机会消除另一个奇怪的终端转义序列:

  if (g != G) { struct itimerval t = { 0, 0, 0, 0 } ; g += ((g < G) << 1) - 1; t.it_interval.tv_usec = t.it_value.tv_usec = 72000 / ((g >> 3) + 1); setitimer (0, &t, 0); f && printf ("\e[10; %u]", g + 24); } 

也许看起来比以下更令人困惑:

  /* update simulation speed */ if (update_interval != gear) { struct itimerval t = { 0, 0, 0, 0 } ; update_interval += ((update_interval < gear) << 1) - 1; t.it_interval.tv_usec = t.it_value.tv_usec = 72000 / ((update_interval >> 3) + 1); setitimer (0, &t, 0); if (play_sound) change_bell_frequency (update_interval + 24); } 

最后修复

虽然现在代码应该看起来更具可读性,但仍然存在一些令人讨厌的部分:

 !d && --*x <= *w && (++*x, ++d) || d == 2 && ++*x + *w > 79 && (--*x, --d); 

d选择另一个(希望)更有意义的名称并打破运算符优先级可能最终会产生如下结果:

  if (curve == CURVE_LEFT) { --*x_offset; if (*x_offset < *width) { ++*x_offset; curve = CURVE_NONE; } } else if (curve == CURVE_RIGHT) { ++*x_offset; if (*x_offset + *width > 79) { --*x_offsett; curve = CURVE_NONE; } } 

而是为所有这些CURVE_... s添加适当的宏。

现在仍有那些XPT名称悬挂在那里也可能会被改变。 因为它的目的在代码中也更好一点,我决定翻转我重命名为treeT的行顺序,这肯定意味着计算也必须修复。 总而言之,它来自:

 char *T[] = { " |", " |", "%\\|/%", " %%%", "" }; char X, **P = &T[4]; // ... if (!(**P && P++ || n & 7936)) { while (abs ((X = rand () % 76) - *x + 2) - *w < 6); ++X; P = T; } 

对于这样的事情:

 char *tree[] = { "", " %%%", "%\\|/%", " |", " |", }; char **tree_line = tree; char tree_position; // ... /* update tree line pointer */ if (!(**tree_line && tree_line-- || n & 7936)) { /* find the right spot to grow */ while (abs ((tree_position = rand () % 76) - *x_offset + 2) - *width < 6) ; ++tree_position; tree_line = &tree[4]; } 

保持最好的部分直到结束

虽然代码看起来对我来说看起来更漂亮,但现在还有一部分缺失。 这就是那个正在做所有输出的人。 我正在谈论的是这条线:

  printf ("\233;%uH\233L%c\233;%uH%c\233;%uH%s\23322;%uH@\23323;%uH \n", *x - *w, r[d], *x + *w, r[d], X, *P, p += k, o); 

除了看起来很难阅读之外,甚至对计算机进行模糊处理以产生任何可用的结果。

我尝试了很多不同的东西在其他终端模拟器中运行,更改终端设置和来回切换语言环境而没有成功。

因此,除了这种混淆似乎更完美,因为它甚至似乎混淆了我的电脑,我仍然无法分辨作者在这里的意图。

八进制代码\233具有与转义字符( \033 )相同的位模式,其中第8位另外设置,这可能在某种程度上与此处预期的效果相关。 不幸的是,因为我已经说过它对我不起作用。

幸运的是,转义序列似乎仍然很容易猜到,所以我想出了以下替代品:

pos + = move_x,

  /* draw street */ printf ("\e[1;%uH" "\e[L" "%c" "\e[1;%uH" "%c", *x_offset - *width, border[curve], *x_offset + *width, border[curve]); /* draw tree */ printf ("\e[1;%uH" "%s", tree_position, *tree_line); /* redraw car */ printf ("\e[22;%uH" "@" "\e[23;%uH" " " "\n", pos, old_pos); 

将绘图分解为(希望)使它们更具可读性。 实际行和前一行仍然在硬编码,就像在原始版本中一样。 如下所示,从那里提取它们甚至可以提高可读性:

  /* draw street */ printf ("\e[1;%uH" "\e[L" "%c" "\e[1;%uH" "%c", *x_offset - *width, border[curve], *x_offset + *width, border[curve]); /* draw tree */ printf ("\e[1;%uH" "%s", tree_position, *tree_line); /* redraw car */ printf ("\e[%u;%uH" "@" "\e[%u;%uH" " " "\n", NEXT_LINE +1, pos, NEXT_LINE +2, old_pos); 

这最终让我进入了第一个可用的版本,然后我“测试”了很多。 虽然可能不是100%的现有技术,但它似乎仍然很容易上瘾。

最后的话

这是我带来的最终未经混淆的版本。 正如您将看到我没有实现LED设置function和清除屏幕function,但不应该很难找到分散在整个混淆版本中的所需转义序列。 事实上,我已经在这篇文章中提到了LED序列。 清除屏幕的是“\ e [”“。 快乐的黑客。

 #include  #include  #include  #include  #include  #define NEXT_LINE 21 #define CURVE_LEFT 0 #define CURVE_NONE 1 #define CURVE_RIGHT 2 char x_offset[] = { 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 0 }; char width[] = { 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 0 }; const char border[] = "\\|/"; void change_bell_frequency () {} void clear_screen () {} void clear_all_LEDs () {} void set_Num_Lock_LED () {} void set_Scroll_lock_LED () {} void set_Caps_Lock_LED () {} char *tree[] = { "", " %%%", "%\\|/%", " |", " |", }; char **tree_line = tree; char tree_position; char curve = CURVE_NONE; char *a, y, z; char move_x = 0; char update_interval = -1; char pos = 40; char old_pos = 40; char play_sound = 0; char gear; unsigned int score = 0; void move (char x, char y) { printf ("\e[%u;%uH", x, y); } void insert () { printf ("\e[L"); } void update (int i) { int n; pos += move_x, /* draw street */ printf ("\e[1;%uH" "\e[L" "%c" "\e[1;%uH" "%c", *x_offset - *width, border[curve], *x_offset + *width, border[curve]); /* draw tree */ printf ("\e[1;%uH" "%s", tree_position, *tree_line); /* redraw car */ printf ("\e[%u;%uH" "@" "\e[%u;%uH" " " "\n", NEXT_LINE + 1, pos, NEXT_LINE +2, old_pos); /* did we leave the road ? */ if (abs (pos - x_offset[NEXT_LINE]) >= width[NEXT_LINE]) exit (0); /* update simulation speed */ if (update_interval != gear) { struct itimerval t = { 0, 0, 0, 0 } ; update_interval += ((update_interval < gear) << 1) - 1; t.it_interval.tv_usec = t.it_value.tv_usec = 72000 / ((update_interval >> 3) + 1); setitimer (0, &t, 0); if (play_sound) change_bell_frequency (update_interval + 24); } /* play sound */ if (play_sound) putchar ('\a'); /* update score */ score += (9 - width[NEXT_LINE]) * ((update_interval >> 3) + 1); old_pos = pos; /* shift x_offset */ a = x_offset; z = *a; while (*++a) { y = *a; *a = z; z = y; }; /* shift width */ a = width; z = *a; while (*++a) { y = *a; *a = z; z = y; }; /* generate new road */ n = rand (); if (!(n & 255) && *width > 1) --*width; /* set tree line pointer */ if (!(**tree_line && tree_line-- || n & 7936)) { /* find the right spot to grow */ while (abs ((tree_position = rand () % 76) - *x_offset + 2) - *width < 6) ; ++tree_position; tree_line = &tree[4]; } /* new offset */ n = rand () & 31; if (n < 3) curve = n; if (curve == CURVE_LEFT) { --*x_offset; if (*x_offset <= *width) { ++*x_offset; curve = CURVE_NONE; } } else if (curve == CURVE_RIGHT) { ++*x_offset; if (*x_offset + *width > 79) { --*x_offset; curve = CURVE_NONE; } } signal (SIGALRM, update); } void end () { signal (SIGALRM, SIG_IGN); clear_all_LEDs (); clear_screen (); printf ("Score: %u\n", score); system ("stty echo -cbreak"); } int main (int argc, char **argv) { atexit (end); if (argc < 2 || *argv[1] != 'q') { argc = *(int*) getenv ("TERM"); if (argc == (int) 0x6C696E75 || argc == (int) 0x756E696C) play_sound = 1; } srand (getpid ()); system ("stty -echo cbreak"); gear = 0 << 3; clear_all_LEDs (); update (14); for (;;) switch (getchar ()) { case 'q': return 0; case '[': case 'b': case ',': move_x = -1; continue; case ' ': case 'n': case '.': move_x = 0; continue; case ']': case 'm': case '/': move_x = 1; continue; case '1': gear = 0 << 3; set_Num_Lock_LED (); continue; case '2': gear = 1 << 3; set_Caps_Lock_LED (); continue; case '3': gear = 2 << 3; set_Scroll_lock_LED (); continue; case '4': gear = 3 << 3; clear_all_LEDs (); continue; } }