UCTF2016 twi Writeup

 

前言

该题目分类为REVERSE。最终目标是提交一个flag。

从题目来看,本题目应该是UCTF 2016中的题目,可能是由于笔者的搜索能力还有待提高,没有能够找到相关的Writeup。所以,本次只能自己摸着石头过河了。

题目下载地址:https://dn.jarvisoj.com/challengefiles/twi.70087b1e507aee08fc5c376a7b5ccc80

 

解题准备

验证下载正确性

基础信息分析

文件类型识别

可以看到,文件是Atmel AVR 8-bit 的ELF文件。

查看是否有隐藏信息

从binwalk的结果来看,应该没有什么隐藏的数据。

字符串分析

各位小伙伴看到这个是不是就已经跃跃欲试了啊。反正我当时是觉得前途光明,形势非常好。不是小好,而是大好。。。

环境准备

模拟器

笔者手头并没有Atmel AVR架构的板子,所以优先考虑使用模拟器。

经过搜索,发现了三款模拟器:

  • simavr
  • simulavr
  • qemu-system-avr

考虑到simavr在网络上得到了比较多的推荐,优先考虑使用之。

笔者的系统是Ubuntu 20.04 LTS on Windows 10 WSL。这三款模拟器都可以通过apt 直接安装。

sudo apt install simavr

安装成功后,可以执行一下,看看说明信息。在说明信息中,发现了–list-cores参数。我们一并来看一下simavr都支持哪些core。

调试器

有了模拟器,我们可以执行镜像了。接下来,需要调试工具。

sudo apt install gdb-avr

gdb这种神器想必大家已经用得出神入化,不用笔者这种小白啰嗦啦。

反编译器

考虑到笔者对版权的洁癖以及贫穷,IDA虽然强大,但也只能忍痛放弃。

其实Ghidra已然足够强大了,我们本次选择ghidra_9.1.2_PUBLIC_20200212作为逆向工作环境。

在Ghidra中Import 目标文件,AVR架构可以正常识别。之后进行自动分析。

可见镜像从地址0x000000开始。由于镜像没有任何symbol或者debug信息,此处可能需要手动按D来强制进行反汇编。细心的小伙伴可能已经发现,明明每个指令是两个字节,但是前面的地址并没有按照两个字节增加。。。

Atmel AVR架构

Atmel AVR 8-bit 是一个精简指令集架构。具体信息大家可以去网上找。信息还是不少的,必定Arduino就是采用这个架构的。

AVR 采用哈弗结构,指令、数据分开寻址。从上面的图,大家可以看到code地址。其实还有一个mem地址。来看下图:

好的,我们现在来讨论一下上面说的,code地址的增加步进不是2的问题。

可以看到,mem寻址是按照1个字节的步进增加的。这里涉及到AVR的寻址模式。mem是按照正常的方式寻址,而code是按照2个字节为一组寻址。也就是内存中的地址是PC * 2。

关于AVR 8-bit的寄存器,总体来说和一般RISC架构的大同小异。不过这里要注意寄存器的别名。比如 W、X、Y以及Z,这个几个是用来间接寻址的寄存器。由于寻址需要,所以他们是8个通用寄存器,分成4对,作为4个16 bit寄存器。

需要注意的是,在gdb中执行“i r”的话,并不会显示这几个寄存器。为了能更方便的获取这几个寄存器的信息,笔者还专门写了一段gdb脚本。。。

在实际操作中,需要准备一份AVR 8-bit的指令说明书。笔者搞到这个里面里面带广告,就不放出来了,大家自己找找吧。

 

尝试运行

运行镜像,最大的挑战在于如何选择机型。笔者认为,越简单的东西,越不容易出问题。出了问题,也可以更方便的排查,所以先选择了atmega8 作为目标机型。当然,最终机型的确定是在整个解题过程中,一点点确定的。题目中的twi其实指的就是单片机中使用广泛的TWI协议,所以比如0x53、0x21和0x23肯定是TWI相关寄存器。另外,根据程序逻辑,判断0x60 肯定不能是寄存器,而是内存区域。

解题成功后,再次进行实验,目前发现atmega8 和 atmega32 都可以作为目标机型。

执行命令:

simavr -t -m atmega8 –gdb twi.70087b1e507aee08fc5c376a7b5ccc80

执行gdb,并attach:

avr-gdb

target remote :1234

这里笔者遇到了问题。尝试设定断点,可是得到:

首先,断点失败。这个在我偶然ptype $pc的时候找到了答案。由于AVR是指令数据分开寻址,所以要设定断点,我们需要提供code地址而不是data地址。怎么提供呢?大家自己ptype $pc下试试吧。当然,也可以参照笔者编写的gdb脚本。

其次,为啥明明设定的是0x26,地址却变成了0x800026。因为我们提供的0x26被认为是数据,而被模拟器自动转换到了对应的内存区域。具体情况大家可以参照simavr的源代码。

那么执行、断点的问题都解决了,我们可以开始进行逻辑分析了。

 

逻辑分析

入口点

通过Vector表,我们可以找到入口点:

这个函数是我自己加的,默认分析完这里是个label。这段代码的作用主要是进行栈的初始化,并且将程序段的一些数据copy到data区域以及进行一些内存区域的初始化。

从本块代码,我们可以看到,SP被初始化为0x45f。

接下来,将我们关注的字符串”flag{“ copy到了0x60。当然,还有”success”和”fail” 等等。然后跳转到了函数:FUN_code_0002d3。

FUN_code_0002d3

这个函数里面逻辑还是挺多的。

嗯,顿时头大。到这里,笔者决定先跑起来看看。

寻找线索

运行起来,没有Log输出。(后来发现,偶尔会有”fail” 的log出来,应该是模拟器的问题。)

经过了几次c – CTRL_C  循环后,发现程序都停在这里:

接下来看看这里在干什么。注意,这里地址是0x240,需要换算成code地址:0x120。

经过一番痛苦的分析,笔者发现:这段逻辑是个延迟。没有发现任何side effect,纯纯的延时。嗯,那还能咋办,为了能够进一步分析,当然要干掉啊。

这里,干掉这个延时有两个办法:

  1. 直接修改镜像,把无关紧要的代码修改为 xx c0或者 xx cf 跳转指令。
  2. 在gdb中下套。

当然,考虑到解题也是以学习为目的,笔者两个办法都尝试了。这里贴一下gdb下套的方案:

细心的盆友们可能又要问了,这里pc的设定为啥是按照内存地址值呢?看下一gdb中pc的显示:

注意,虽然显示的时候是按照code地址显示,但是大家在使用$pc设定值和运算的时候,一定要用内存地址值哦。

FUN_code_00012d

我们继续寻找线索。在把那个大延时干掉之后,我们又来到了一个新的区域。仍旧是c CTRL_C 循环,位置比较固定:

根据地址,寻找逻辑。

看到笔者对函数的命名,大家应该明白这个是干啥的了。这里,看起来好像很轻松就识别到了这个函数的作用,可是是实际上,笔者因此损失了多少本来就不多的头发啊!而且,靠的是灵光一闪,突然有思路。在无数次进入这个函数,笔者满地打滚、狂薅头发的过程中,突然想到:看看这东西到底在输出啥吧。。。

于是,这个函数的名字就有了。

接下来,肯定是要找引用点啦。

success 与 fail

在Ghidra强大引用分析的帮助下,不到1秒就成功定位相关逻辑。这样一来,程序的大体逻辑基本清晰了。另外,这两个点都在FUN_code_0002d3函数中。这样,就可以通过这两个指令块,来向上逆推正确的逻辑路径了。

确定路径是逆向中比较基本的工作,这里不再赘述。

第一个分支点

其中,0x60中保存的字符串是:”flag{\0”。

进入函数,分析其逻辑。

由于生平第一次接触AVR架构,再次经过了满地打滚以及狂薅头发的分析之后,发现这个函数是在检查W所指向地址中的内容前的5个字节是不是等于”flag{”。

在这个时候,笔者还没想到TWI的真实意义。(原谅我对硬件的无知与迟钝),所以还以为程序运行到一定程度,会解密出flag放在内存中。于是,这里的解决方案是:将0x60的5个字节copy过来。。。虽然,结合谜底来看简直是蠢爆了,但是在最终解决问题阶段还是用上了,也算没白费劲儿。

 

(中式英语是伟大的!是她维护了文件编码的纯洁性。

— SimonTheCoder)

第二个分支点

接下来,来到LAB_code_00030a。

这段逻辑就比较容易理解了。在判断刚才那段内存中的0x26位置是不是”}”(0x7d)。

解决方案和上一个分支点的处理方法一致,你要什么,我就给什么。

数据处理函数

这里,涉及到两个函数(如图)。又是一番痛苦的分析之后,发现这两个函数一个是将flag{******}中的******(32个)向前移动5个字节,覆盖掉”flag{”。

另一个,将这32个字节作为hexstring转换为16个字节数据。

两个函数功能虽然简单,但是由于笔者对AVR不够熟悉,着实进行一番痛苦的分析。

最终分支

这个分支点是最后一个影响程序结果的分支点,也是最难搞的一个。

由于比较长,这里只截取了一小部分指令。这一大段逻辑,是用来对之前处理好的16字节数据进行校验的。注意是校验,而不是解密。

可以看到,最终是要判断R22和R10是否相当。根据逻辑,R10以及其它参与运算的寄存器的数据,都是从数据区域读出的固定值。而R22的基础值也是从固定数据中读出,与待验证的16个字节,经过运算后形成。

其中,负责提供算法复杂性的是FUN_code_00036c:

可以看到,大量寄存器参与了运算。并且还会进行mul运算。这么一段逻辑,估计要完全分析清楚估计要很长时间以及非常强大的精神力量。

另外,整段逻辑没有任何st指令。换言之,没有对内存进行写操作。这也就意味着,并不存在将数据解密后放在内存中的逻辑。笔者之前的猜测显然是错误的。

 

接受了这个事实之后,我们需要解决两个问题:

  • 用来验证的flag是如何加载到内存中的。
  • flag的内容为何。

再一次的 FUN_code_0002d3

为了解决之前的两个问题,重新回到 FUN_code_0002d3进行分析。

用来验证的flag是如何加载到内存中的。

考虑到镜像文件中并没有相关的数据,那么只有可能是来自于外部输入。查看了一下FUN_code_0002d3中的函数调用:

几个函数中,只有000062我们目前还没有接触过。查看一下代码:

其实,只要分析清楚这块逻辑,就没有必要再进行000062的分析了。首先,可以看到这是个循环。该循环每次从FUN_code_000062中获得一个字节,放进Z中。而Z此时,正指向待check区域的地址(也就是flag放的地址)。(这个可以从gdb和使用了R3R2赋值两个角度来验证。)

len(“flag{}”)+len(“32字节神秘字符串”) = 0x6 + 0x20 = 0x26,count也对上了。

当然,实际上还是对FUN_code_000062进行了分析的。

节选一段。从代码来看,该函数再操作寄存器。根据寄存器,搜索了一下,发现是TWI相关寄存器。至此,题目中的TWI的来由我们清楚了。

从而,我们可以确定,本题目在板子上真机运行的时候,可以通过TWI将flag输入到程序中。

flag的内容为何。

确定了数据的来源,那么接下来解决数据的内容。

通过之前对数据校验区域的分析,我们可以得到一下结论:

  • 算法非常复杂,短时间内难以完成你算法。
  • flag的核心是32字节的hex string,经过处理函数处理后,得到了16字节数据。
  • flag通过TWI 输入系统。

通过以上几个结论,再考虑到之前屏蔽掉的那个延时逻辑,那么解决方案就呼之欲出了,

出题人是在疯狂暗示我们:爆破。

 

PWN

既然考虑到了爆破,那么爆破的可行性如何?

  • 16*255 种可能,完全可以接受。
  • 延时逻辑已经被干掉。
  • 本来我们也不是通过TWI输入数据,相关函数可跳过,进一步节省时间。
  • 可以通过gdb脚本实现自动化。

既然可行,那么接下来相对就要简单了。

  • 通过断点下套,跳过耗时的逻辑。
  • 通过之前的copy  “flag{” 和写入 “}”来绕过分支点。
  • 自动生成测试数据,填充到flag区域。
  • 在check判定成功、失败节点上下断点,从而获取次测试结果。

具体实现,请参照:

https://gist.github.com/SimonTheCoder/7ead8ca45008386c500c3d4813e19ff2

脚本使用:

  • 启动模拟器
  • 启动gdb
  • source help.gdb
  • lk
  • pwn

 

总结

本次的题目可以说非常折磨人,但是非常有趣。如果不研究到最后,不太容易想到需要爆破。如果没想到要爆破,也就不明白为啥前面有个奇怪的延时。直到最后,所有的线索都串起来了,豁然开朗。不过,虽然题目解开了,但是失去的头发和爆的肝也找不回来了。

最后,感谢出题人设计出这么有趣的题目。感谢Jarvis OJ提供了这么一个方便、高效又有趣的平台。对开帮助笔者这样的小白学习知识、开阔眼界提供了巨大的帮助。

(完)