Skip to content

pwnable kr maze

复制本地路径 | 在线编辑

前言

主要的参考: [https://shayete.tistory.com/entry/Grotesque-maze]
超级感谢这个博主,让我终于有了些思路,否则就对 pwnable kr 做题绝望了。感觉还是应该要分享,排名真的无所谓,可惜现在网上的 writeup 全是加密。

程序分析

main 函数如下,就是走迷宫,每次迷宫通过后都会增加一个敌人,通过二十关后就可以记录姓名。
record_name 函数有一个明显的 Stack Overflow.

int __fastcall main(__int64 a1, char **a2, char **a3)
{
  level = 1;
  do
  {
    init_maze();
    one_level();
    printf("\rlevel %d clear!\n", (unsigned int)level);
    sleep(1u);
    ++level;
  }while ( (unsigned int)level <= 0x14 );
  puts("Congratz! you win!");
  record_name();
  return 0LL;
}
int record_name()
{
  char v1; // [rsp+0h] [rbp-30h]

  stream = fopen("record", "a+");
  printf("record your name : ", "a+");
  gets(&v1);
  fprintf(stream, "PLAYER : %s has PWNED the MAZE\n", &v1);
  return fclose(stream);
}

实际上通过第五关就非常难了,所以肯定不是直接 DFS 来通过完。

奇怪的代码

one_level 函数就是使用 while 处理用户输入直到满足条件退出循环,但是这里面有一段特别奇怪的代码。

if ( dword_6020C0 || user_choice != 'O' )
{
    if ( !dword_6020C0 ) dword_6020C0 = 0;
    if ( dword_6020C0 != 1 || user_choice != 'P' )
    {
        if ( dword_6020C0 == 1 ) dword_6020C0 = 0;
        if ( dword_6020C0 != 2 || user_choice != 'E' )
        {
            if ( dword_6020C0 == 2 ) dword_6020C0 = 0;
            if ( dword_6020C0 != 3 || user_choice != 'N' )
            {
                ...
                if ( dword_6020C0 != 10 || row != 14 || col != 8 || (unsigned int)level <= 4 )
                {
                    if ( dword_6020C0 == 10 ) dword_6020C0 = 0;
                }else
                  byte_602218 = 48;
                ...
            }else
                dword_6020C0 = 4;
        }else
            dword_6020C0 = 3;
    } else
        dword_6020C0 = 2;
} else
    dword_6020C0 = 1;

这段代码真的很奇怪,而且 dword_6020C0 这个变量对我们程序没有作用,所以我一开始以为这就是用来迷惑人的,然而大错特错。
可以试着这样逆着分析代码

  1. 我想要执行最里面的 if-else 段,这里我换种思路,我想要 else 里的内容,需要什么条件?
  2. 判断条件可以简化为 dword_6020C0 != 10,所以我需要 dword_6020C0 = 10
  3. 发现这条语句只有在上一级的 if-else 中的 else 里出现过,所以我现在需要执行上一级 if-elseelse
  4. 判断条件是 dword_6020C0 != 9 || user_choice != 'X',所以我需要 dword_6020C0 = 9
  5. 同样,发现只要上上级的 if-else 中的 else 里出现过,所以需要执行上上级 if-elseelse
  6. 同样,判断条件是 dword_6020C0 != 8 || user_choice != 'W',所以需要 dword_6020C0 = 8
  7. 一直这样,最后发现需要 dword_6020C0 = 1

然后再正着分析

  1. 第一次我需要 dword_6020C0 = 1,所以不能执行 dword_6020C0 || user_choice != 'O',所以我第一次输入是 O
  2. 第二次我需要 dword_6020C0 = 2,所以不能执行 dword_6020C0 != 1 || user_choice != 'P',所以第二次输入是 P
  3. 一直这样下去,最后发现只要顺着顺序输入 OPEN... 就可到达最里面的 if-else

程序漏洞

漏洞就是在于上面的这段代码,当时做的时候太粗心没注意最里面的 else 是这样的语句: byte_602218 = 48
byte_602218 可是我们的迷宫数据啊,所以这样会把迷宫给改掉,ascii 为 48 的字符是 '0'。
对于我们的迷宫程序,它认为 '0' 表示这里可以走,所以如果我们让 byte_602218 = 48,迷宫有个墙变成了 '0'。

这个有什么用处?这里也想了很久,终于想出了,不容易。
此时可以让角色移动到迷宫之外了!因为迷宫的字符是以一个 maze_table[] 这样的形式存储,每次角色移动会赋值 maze_table[role_x * 24 + role_y] = 'X',所以当角色移动到迷宫之外那么就会造成溢出。

数据存储格式如下

.bss:0000000000602120     maze_char       db 0F0h dup(?)
.bss:0000000000602210                     db    ? ;
...
.bss:0000000000602217                     db    ? ;
.bss:0000000000602218     byte_602218     db ?
.bss:0000000000602219                     align 20h
.bss:0000000000602220     rival_count     dd ?          
.bss:0000000000602220                                   
.bss:0000000000602224     row             db ?          
.bss:0000000000602224                                   
.bss:0000000000602225                     align 10h
.bss:0000000000602230     ; char col_rival[20]
.bss:0000000000602230     col_rival       db ?          
.bss:0000000000602230                                   
...
.bss:0000000000602244     level           dd ?          

所以可以修改 level 大于 20,然后让用户从范围外回到迷宫并到达终点,此时就会直接执行 record_name 函数,从而进行简单的栈溢出即可。

总结

  1. 那个比较复杂的 if-else 嵌套指的反复分析
  2. 让用户移到迷宫外从而造成溢出这一想法,当时想了不少时间