引子—-demo0和demo1的对比
1 | // demo0 |
demo0,直接在main()中调用后门函数,一切正常
1 | // demo1 |
demo1, 用数组越界来模拟pwn中的劫持控制流。
然后理所当然地寄了,就和pwn中直接返回到backdoor中一样。
1 | $ ./test |
一般这种情况,有两种方法解决,一种在ROPchain中加一个ret指令,一种直接劫持到system()语句的位置,跳过push rbp
事实上并不是所有这种ret2text都需要这种技巧,这和栈所在的环境有关,不同的程序甚至于不同的机器之间栈都有细微的差别
如何检查16位栈对齐
现在来探索一下system()是如何检查栈不平衡的
利用上面的demo1动态调试
1 | 0x7ffff7dd3d70 <system> endbr64 |
在system@plt处stepin, 可以看到单纯地进入system完全没有问题。
1 | 0x7ffff7dd3d79 <system+9> jmp do_system <do_system> |
然后跳转到do_system
1 | ► 0x7ffff7dd3967 <do_system+103> mov qword ptr [rsp + 0x188], 0 [0x7fffffffd8c0] => 0 |
结果单步一下直接给我干到了do_system+103的位置,就是检查到没有16位对齐的上一句,之前调试kernel的时候也有类似的问题
检查先放一下,看一下do_system汇编,免得漏掉什么
1 | 0x7ffff7dd3900 <do_system>: push r15 |
可以看到没有跳转,就单纯是一路执行下来的,关注一下两个xmm寄存器
SSE(Streaming SIMD Extensions)是针对当前CPU寄存器以及指令集的一个拓展,有xmm0 ~ xmm1516个128bit的寄存器,xmm寄存器主要干两件事,第一个是浮点运算,第二个是SIMD指令集,一条指令操作多条数据。
对于xmm寄存器,有几种方法控制其中的数据,第一种movq指令,q表示_QWORD,既64bit,该指令会操作xmm寄存器的低64bit而无需检查,另一个操作数可以是xmm寄存器或者一个64bit寄存器;
第二种,movdqa和movdqu,表示Double _QWORD,a代表aligned,u代表unaligned,用于将内存中的128bit数据或者某个xmm的数据,转存到另一个xmm中,很明显aligned代表在操作数为内存时需要16位对齐
第三种,movups和movaps,u和a的含义不变,而ps表示packed single-precision floating-point(打包的单精度浮点数),一个float有32bit,而128bit就是4个float,这就是SIMD的多条数据的含义。
第四种,movupd和movapd,几乎和第三种一样,d可能表示data
然后回到do_system+103,这里涉及SSE为什么需要16位字节对齐,首先显而易见地因为xmm是16字节,所以对xmm寄存器的读取和别的数据一样要按数据类型大小对齐,
但是这实际上不能解释为什么存在不对齐的指令,可能是指令做了一些拼接操作?除了之前movq可以操作xmm的低64位之外,一些像movhlps、punpckhqdq的指令可以操作xmm寄存器的高64位
做个总结,涉及SSE中特定指令,比如movaps、movdqa需要当前内存类型操作数16位对齐,反映在do_system中,rsp指向位置需要16位对齐,也就是栈需要16位对齐。
为什么用xmm
程序为什么要有这一步xmm到[rsp]的赋值操作,先用print $xmm1看一下xmm1有什么
1 | pwndbg> print $xmm1 |
关注v2_int64的两个值,0x7ffff7f9f7a0<quit>和0x7ffff7f9f840<intr> ,这两个值在前面通过r13和r14寄存器放到了xmm1中
在下面有对这两个值的使用,以一种类似硬编码的方式使用
1 | 0x7ffff7dd39d3 <do_system+211>: xor eax,eax |
直接看汇编还是太逆天了,下面是IDA的反汇编,注意qword_21C840是<intr>,qword_21C7A0是<quit>
1 | v16[0] = 2LL * (qword_21C840 != 1); |
后续是各种posix的操作,也就是开进程。
然后, 检查下当前程序走向,如果不是栈平衡的问题,应该到达do_system+536,也就是说上面开进程的内容被跳过了
1 | ► 0x7ffff7dd3973 <do_system+115> movaps xmmword ptr [rsp], xmm1 <[0x7fffffffd638] not aligned to 16 bytes> |
然后是有关[rsp]的操作,这里是存放两个值到xmm4,然后调用子函数__GI___libc_cleanup_push_defer, 这是一个用于清理线程的函数, 之后便没有相关操作了。
1 | 0x7ffff7dd3b84 <do_system+644>: movdqa xmm4,XMMWORD PTR [rsp] |
再次总结,system()中通过r13,r14将<intr>和<quit>放到xmm,然后放到[rsp],方便后续的管理进程和线程, 至于为什么非要放到xmm,个人理解是这两个值是一起被使用的,类似于一个结构体,所以放在一个128bit寄存器比两个64bit更好。
至于<intr>和<quit>,两个变量都放在glibc的.bss,默认都是0。Xrefs发现它们只在do_system中被使用,但是都没有赋值,感觉很奇怪。
ps: (来自很遥远的未来) 这种向量运算其实不算特别少见, 尤其是比较底层的各种库, 为了想办法尽量增加效率, 这种SIMD不在少数. 但是至少在X86_64上, 很多SIMD并不要求16字节对齐, 别的架构不太清楚, 其次一般也只有栈上的利用才容易导致不对齐的问题.
需要栈平衡的函数
在实际实践时发现,不只有system()需要16位,诸如puts, scanf, printf等也会有类似的需求。
1 | // demo2 |
结果是
1 | ► 0x7ffff7e274c0 <_int_malloc+2832> movaps xmmword ptr [rsp + 0x10], xmm1 <[0x7fffffffd758] not aligned to 16 bytes> |
不难发现, 涉及malloc,_IO_file_xsputn都需要检查,直白点说就是涉及IO的都会有栈平衡问题,但不保证是_IO_file_xsputsn的问题,比如vprintf本身就有xmm寄存器对齐要求
其次,堆分配(malloc)也会有这类问题,但一般不会很显著
你以为这就完了吗? 怎么会。如果IO能跳过_IO_file_xsputn,不就可以正常运行了吗,实际上write和read就是这样的, 因为这两个单纯就是把syscall包装了一下
1 | .text:00000000001147D0 ; __unwind { |
再看一个demo
1 | // demo3 |