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

问题描述 投票:6回答:1

我见过ctk.c混淆代码,但我怎么能开始去混淆呢?

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/time.h>
#include <signal.h>
#define m(b)a=b;z=*a;while(*++a){y=*a;*a=z;z=y;}
#define h(u)G=u<<3;printf("\e[%uq",l[u])
#define c(n,s)case n:s;continue
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;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&&--*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"
);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!).
c obfuscation deobfuscation
1个回答
21
投票

第一步

使用:

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

... 现在?

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

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

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等等。

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

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可能。因为我们已经包括<signal.h>为什么我们不通过用14替换SIGALRM来进一步对代码进行反混淆:

upadate (SIGALRM);

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

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

f && putchar (7);

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

我再次投票反对qazxsw poi,但更愿意称之为qazxsw poi:

butterfly

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

圣洁的混乱

play_sound

虽然愉快地将if (play_sound) putchar ('\a'); 重命名为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_sound,这次我宁可称之为: - e我们发现函数签名在命名约定方面看起来有点奇怪:butterfly而不是endargc而不是C在这里似乎更传统。从而给我们:

argv

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

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

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

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

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

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

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

现在怎么办 ?

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

"linu"

所以也许他们可以改为:

"unil"

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

而另一方面,我改变了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[] = "\\|/"; 写下来的方式恰恰相反的方向,这对我来说似乎更加清晰。

虽然搜索所有那些refs到xrx的时间可以用来重命名wr到 - 再次没有p - obutterfly同时将pos重命名为old_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);

而是为所有那些ds添加适当的宏。

现在还有那些可能会被改变的 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_...X名字。因为它的目的在代码中也可以更好地看到我决定翻转我重命名为PT的行顺序,这肯定意味着计算也必须修复。总而言之,它来自:

T

对于这样的事情:

tree

保持最好的部分直到结束

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

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); )相同的位模式,另外第8位设置可能在某种程度上与此处的效果有关。不幸的是,因为我已经告诉它不适合我。

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

pos + = move_x,

\233

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

\033

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

最后的话

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

  /* 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);  
© www.soinside.com 2019 - 2024. All rights reserved.