我如何处理这个 CTF 调试程序?

逆向工程 部件 调试 反编译 C 小精灵
2021-06-18 08:47:54

我有一个正在处理的 ELF 可执行文件(从之前的 CTF 比赛中获得)。可执行文件只要求输入密码,然后打印出“congrats”。


代码片段和我的注释很长,所以我将它们包含在单独的 HTML 链接中。

反编译的 C 代码

精灵可执行文件

//让你大致了解这个程序在 C 中做了什么。
int main(int argc, char ** argv)
{
    int32_t 结果;// 成功值


    如果(argc > 1)
    {
        设置(); //分配一个整型数组(g1)

        int32_t v1 = 0; //柜台

        而(真)
        {
            char v2 = *(char *) (*(int32_t *) ((int32_t)argv + 4) + v1); 
            int32_t v3 = *(int32_t *) (4 * v1 + (int32_t) & g1);

            int32_t v4 = v1 + 1; //在g1数组中的索引

            如果(检查((int32_t *)v3,(int32_t)v2)!= 0)
            {
                puts("不");
                返回-1;
            }
            if (v4 >= 31) //当我们到达数组中的最后一个索引时
            {
                休息;
            }
            v1 = v4;
        }
        puts("恭喜!");
        结果 = 0;
    }
    别的
    {
        puts("用法./smoothie");
        结果 = -1;
    }
    返回结果;
}

我尝试使用 GDB 调试程序和 IDA 来理解控制流。我的推论最终有 3 件事:

  • setup() 生成某种整数数组
  • check(int32_t * array, int * array) 被调用 31 次以确认我们的“密码”,如可能的 while 循环所示
  • 在 check(...) 内部,它遍历每个 malloc 的数组[5] 并确认我们密码的每个“字符”。

这是 IDA 的 check() 控制流程图(简单 - 3 部分): 第一组检查 可能的计算和第二组检查 第二组检查

这是 check() 的控制流程图,点击放大

check() 调用图

  • 大多数 check() 比较 * (array) 的上半部分和 * (array + 8) 的下半部分加上 passwordIndex 比较。

  • 在这个程序的main(int argc, char ** argv)中,有一个while循环需要执行31次(密码可能是31个字符)。如果 check() 返回 1,while 循环将退出。


我认为中间部分进行了一些计算(add、sub、cdq 和 idiv、imul 和 xor),这可能有助于了解作为密码传递的内容。

请指导我朝着正确的方向前进(如果可以,请提供提示)。自己调试程序可能会使我的问题更清楚。

假铅

  • 在 check() 中,我假设第一组检查“计算”了我的输入 ascii 应该匹配的正确数字(例如计算 90,因此 index[i] 应该等于 'Z' ~ 90)。
  • 在检查的后半部分,它使用 * (array + 8),这意味着它使用数组的第三个数字进行 101, 142, ... , 数字比较。我们可以将 * (array + 8) 的解引用值更改为计算出的数字 (90) 以进行最终比较。
  • 一旦我们到达最后的比较,它会将我们的输入与第一组检查中计算出的数字进行比较,因此 90 == 90。由于它们相等,函数返回 0 表示成功。

尽管 check(int32_t * array, (int32_t) input) 有任何字符输入,但由于数组中的预设值,结果将是相同的。我能做些什么来纠正这个问题?

2个回答

解决这个 CTF 的方法是同时分析setup()check()方法(废话!)。第一个很长,但当你检查发生了什么时,它很简单。它分配了 20B 的内存来存储 5 x 4 B 的数据。它做了31次。存储在那里的值看起来像是随机的,但实际上不是。只有 5 个不同的值放在第一个位置和第三个位置。

第一个和第三个槽中的值是操作类型值;3rd, 4th - 操作数;5th - 操作数和结果

如果我们检查check()函数。我们看到正在传递的参数是:在标志setup()和 achar创建和填充的缓冲区之一根据在缓冲区中找到的值,对缓冲区中的值进行数学运算。

有关操作的更多信息。基于第 1 和第 3 值,很少。添加,子,模,异或,多。如果我们以第一个缓冲区的值为例: 0x01 0x81 0x65 0x0C 0x5A 这些将被吐出;0x01 - 模组操作,0x65 - 添加;所以字符是 (0x5A % 0x81) + 0x0C。我们对每个缓冲区进行类似(但不同的操作)。

这个二进制文件有什么问题(或者可能是故意的)?好吧,对于几乎所有情况,在操作数上计算的值都不会与标志的字符进行比较,并且eax设置不正确,我们得到“无”。需要修复的是更改跳转,以便所有情况都通过检查标志字符。除了修改二进制文件之外,您还可以做的是欺骗 gdb 使其行为正常。

  • 设置断点 leave
    • 输入命令
    • 输入 p/c $edx
    • 类型集 $eax=0
    • 输入结束

每当触发断点时,您将打印计算出的字符,它将模拟比较正常并允许应用程序继续。这样你就可以运行应用程序,点击几次“c”并观察标志

最后是国旗

标志{HereBeDaFlagForYoDebu?g?h}

还有一点解释...

对于那两个“?” 要么是控制字节错误,要么是操作数错误,因此计算错误。也许我会花一些时间找出并尝试正确计算它们,但是使用当前值,我们得到的是 ASCII 可打印之外的东西。

编辑:正如 Paweł Łukasik 和 DittoPDX 所说,二进制文件确实有缺陷,因此不允许任何正确的标志。然而,下面采用的方法仍然有效,可能有助于类似的任务。


我很确定有很多不同的方法可以解决这个挑战,这里是我从其他 CTF 任务中学到的一种,它几乎不需要逆向工程。

整个解决方案基于构建主循环的方式:

for ( i= 0; i <= 30; i++)
{
    if ( check(ec[i], argv[1][i]) )
    {
        puts("Nope");
        return -1;
    }
}
puts("congrats!");
result = 0;

达到“恭喜!”的唯一途径 将通过整个循环而不在中间击中返回。有趣的部分是在输入的每个字符上调用检查函数,一旦无效,循环就会存在。

这种构造对您在程序执行期间可以评估的事情有一些影响。可以将其称为“侧信道攻击”。


剧透,在此标记下方是解决此任务的更多细节。


我的意思是:对于您获得的每个正确字符,二进制文件执行的指令数量都会增加。解决这一挑战的一种方法是计算已执行指令的数量(提示:PinTools;Source/tools/ManualExamples/inscount),并逐个字符对输入字符进行暴力破解。每次你猜对了字符,执行指令的数量就会增加。编写所有这些脚本的一种简单方法是使用pwntools python 模块 :)

我希望这会有所帮助,CTF 快乐!