引子—-demo0和demo1的对比
// demo0
#include <stdio.h>
#include <stdlib.h>
void getshell(){
system("/bin/sh\x00");
}
int main(){
getshell();
return 0;
}
demo0,直接在main()中调用后门函数,一切正常
// demo1
#include <stdio.h>
#include <stdlib.h>
void getshell(){
system("/bin/sh\x00");
}
int main(){
size_t array[3];
array[5] = getshell; // 数组越界
return 0;
}
demo1, 用数组越界来模拟pwn中的劫持控制流。
然后理所当然地寄了,就和pwn中直接返回到backdoor中一样。
$ ./test
Segmentation fault (core dumped)
一般这种情况,有两种方法解决,一种在ROPchain中加一个ret指令,一种直接劫持到system()
语句的位置,跳过push rbp
事实上并不是所有这种ret2text都需要这种技巧,这和栈所在的环境有关,不同的程序甚至于不同的机器之间栈都有细微的差别
如何检查16位栈对齐
现在来探索一下system()
是如何检查栈不平衡的
利用上面的demo1动态调试
0x7ffff7dd3d70 <system> endbr64
► 0x7ffff7dd3d74 <system+4> test rdi, rdi 0x555555556004 & 0x555555556004 EFLAGS => 0x202 [ cf pf af zf sf IF df of ]
0x7ffff7dd3d77 <system+7> je system+16 <system+16>
0x7ffff7dd3d79 <system+9> jmp do_system <do_system>
↓
0x7ffff7dd3900 <do_system> push r15
0x7ffff7dd3902 <do_system+2> mov edx, 1 EDX => 1
0x7ffff7dd3907 <do_system+7> push r14
0x7ffff7dd3909 <do_system+9> lea r14, [rip + 0x1cbf30] R14 => 0x7ffff7f9f840 (intr) ◂— 0
0x7ffff7dd3910 <do_system+16> push r13
0x7ffff7dd3912 <do_system+18> lea r13, [rip + 0x1cbe87] R13 => 0x7ffff7f9f7a0 (quit) ◂— 0
0x7ffff7dd3919 <do_system+25> movq xmm2, r14 XMM2 => 0x7ffff7f9f840 (intr) ◂— 0
在system@plt
处stepin, 可以看到单纯地进入system
完全没有问题。
0x7ffff7dd3d79 <system+9> jmp do_system <do_system>
↓
► 0x7ffff7dd3900 <do_system> push r15
0x7ffff7dd3902 <do_system+2> mov edx, 1 EDX => 1
0x7ffff7dd3907 <do_system+7> push r14
0x7ffff7dd3909 <do_system+9> lea r14, [rip + 0x1cbf30] R14 => 0x7ffff7f9f840 (intr) ◂— 0
0x7ffff7dd3910 <do_system+16> push r13
0x7ffff7dd3912 <do_system+18> lea r13, [rip + 0x1cbe87] R13 => 0x7ffff7f9f7a0 (quit) ◂— 0
0x7ffff7dd3919 <do_system+25> movq xmm2, r14 XMM2 => 0x7ffff7f9f840 (intr) ◂— 0
然后跳转到do_system
► 0x7ffff7dd3967 <do_system+103> mov qword ptr [rsp + 0x188], 0 [0x7fffffffd8c0] => 0
0x7ffff7dd3973 <do_system+115> movaps xmmword ptr [rsp], xmm1 <[0x7fffffffd738] not aligned to 16 bytes>
0x7ffff7dd3977 <do_system+119> lock cmpxchg dword ptr [rip + 0x1cbe01], edx
0x7ffff7dd397f <do_system+127> jne do_system+816 <do_system+816>
0x7ffff7dd3985 <do_system+133> mov eax, dword ptr [rip + 0x1cbdf9] EAX, [sa_refcntr] => 0
0x7ffff7dd398b <do_system+139> lea edx, [rax + 1] EDX => 1
0x7ffff7dd398e <do_system+142> mov dword ptr [rip + 0x1cbdf0], edx [sa_refcntr] => 1
0x7ffff7dd3994 <do_system+148> test eax, eax 0 & 0 EFLAGS => 0x246 [ cf PF af ZF sf IF df of ]
0x7ffff7dd3996 <do_system+150> ✔ je do_system+536 <do_system+536>
↓
0x7ffff7dd3b18 <do_system+536> lea rbp, [rsp + 0x180] RBP => 0x7fffffffd8b8 ◂— 1
0x7ffff7dd3b20 <do_system+544> mov rdx, r14 RDX => 0x7ffff7f9f840 (intr) ◂— 0
结果单步一下直接给我干到了do_system+103
的位置,就是检查到没有16位对齐的上一句,之前调试kernel的时候也有类似的问题
检查先放一下,看一下do_system
汇编,免得漏掉什么
0x7ffff7dd3900 <do_system>: push r15
0x7ffff7dd3902 <do_system+2>: mov edx,0x1
0x7ffff7dd3907 <do_system+7>: push r14
0x7ffff7dd3909 <do_system+9>: lea r14,[rip+0x1cbf30] # 0x7ffff7f9f840 <intr>
0x7ffff7dd3910 <do_system+16>: push r13
0x7ffff7dd3912 <do_system+18>: lea r13,[rip+0x1cbe87] # 0x7ffff7f9f7a0 <quit>
0x7ffff7dd3919 <do_system+25>: movq xmm2,r14
0x7ffff7dd391e <do_system+30>: push r12
0x7ffff7dd3920 <do_system+32>: movq xmm1,r13
0x7ffff7dd3925 <do_system+37>: push rbp
0x7ffff7dd3926 <do_system+38>: punpcklqdq xmm1,xmm2
0x7ffff7dd392a <do_system+42>: push rbx
0x7ffff7dd392b <do_system+43>: mov rbx,rdi
0x7ffff7dd392e <do_system+46>: sub rsp,0x388
0x7ffff7dd3935 <do_system+53>: mov rax,QWORD PTR fs:0x28
0x7ffff7dd393e <do_system+62>: mov QWORD PTR [rsp+0x378],rax
0x7ffff7dd3946 <do_system+70>: xor eax,eax
0x7ffff7dd3948 <do_system+72>: mov DWORD PTR [rsp+0x18],0xffffffff
0x7ffff7dd3950 <do_system+80>: mov QWORD PTR [rsp+0x180],0x1
0x7ffff7dd395c <do_system+92>: mov DWORD PTR [rsp+0x208],0x0
=> 0x7ffff7dd3967 <do_system+103>: mov QWORD PTR [rsp+0x188],0x0 # 执行到这里了
0x7ffff7dd3973 <do_system+115>: movaps XMMWORD PTR [rsp],xmm1
可以看到没有跳转,就单纯是一路执行下来的,关注一下两个xmm寄存器
参考:https://cch123.gitbooks.io/duplicate/content/part3/translation-details/function-calling-sequence/xmm-registers.html
SSE(Streaming SIMD Extensions)是针对当前CPU寄存器以及指令集的一个拓展,有xmm0 ~ xmm15
16个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有什么
pwndbg> print $xmm1
$1 = {
v8_bfloat16 = {-6.49e+33, -1.01e+34, nan(0x7f), 0, -1.558e+34, -1.01e+34, nan(0x7f), 0},
v8_half = {-31232, -32656, nan(0x3ff), 0, -34816, -32656, nan(0x3ff), 0},
v4_float = {-1.01398777e+34, 4.59163468e-41, -1.01399768e+34, 4.59163468e-41},
v2_double = {6.9533491570647782e-310, 6.9533491570726832e-310},
v16_int8 = {-96, -9, -7, -9, -1, 127, 0, 0, 64, -8, -7, -9, -1, 127, 0, 0},
v8_int16 = {-2144, -2055, 32767, 0, -1984, -2055, 32767, 0},
v4_int32 = {-134613088, 32767, -134612928, 32767},
v2_int64 = {140737353742240, 140737353742400},
uint128 = 2596145946097181985715420921460640
}
关注v2_int64
的两个值,0x7ffff7f9f7a0<quit>和0x7ffff7f9f840<intr> ,这两个值在前面通过r13和r14寄存器放到了xmm1中
在下面有对这两个值的使用,以一种类似硬编码的方式使用
0x7ffff7dd39d3 <do_system+211>: xor eax,eax
0x7ffff7dd39d5 <do_system+213>: cmp QWORD PTR [rip+0x1cbe63],0x1 # 0x7ffff7f9f840 <intr>
0x7ffff7dd39dd <do_system+221>: setne al
0x7ffff7dd39e0 <do_system+224>: add rax,rax
0x7ffff7dd39e3 <do_system+227>: cmp QWORD PTR [rip+0x1cbdb5],0x1 # 0x7ffff7f9f7a0 <quit>
0x7ffff7dd39eb <do_system+235>: mov QWORD PTR [rsp+0x100],rax
直接看汇编还是太逆天了,下面是IDA的反汇编,注意qword_21C840是<intr>,qword_21C7A0是<quit>
v16[0] = 2LL * (qword_21C840 != 1);
if ( qword_21C7A0 != 1 )
v16[0] = (2LL * (qword_21C840 != 1)) | 4;
posix_spawnattr_init(v20);
posix_spawnattr_setsigmask(v20, v15);
posix_spawnattr_setsigdefault(v20, v16);
posix_spawnattr_setflags(v20, 12LL);
后续是各种posix的操作,也就是开进程。
然后, 检查下当前程序走向,如果不是栈平衡的问题,应该到达do_system+536
,也就是说上面开进程的内容被跳过了
► 0x7ffff7dd3973 <do_system+115> movaps xmmword ptr [rsp], xmm1 <[0x7fffffffd638] not aligned to 16 bytes>
0x7ffff7dd3977 <do_system+119> lock cmpxchg dword ptr [rip + 0x1cbe01], edx
0x7ffff7dd397f <do_system+127> jne do_system+816 <do_system+816>
0x7ffff7dd3985 <do_system+133> mov eax, dword ptr [rip + 0x1cbdf9] EAX, [sa_refcntr] => 0
0x7ffff7dd398b <do_system+139> lea edx, [rax + 1] EDX => 1
0x7ffff7dd398e <do_system+142> mov dword ptr [rip + 0x1cbdf0], edx [sa_refcntr] => 1
0x7ffff7dd3994 <do_system+148> test eax, eax 0 & 0 EFLAGS => 0x10246 [ cf PF af ZF sf IF df of ]
0x7ffff7dd3996 <do_system+150> ✔ je do_system+536 <do_system+536>
↓
0x7ffff7dd3b18 <do_system+536> lea rbp, [rsp + 0x180] RBP => 0x7fffffffd7b8 ◂— 1
0x7ffff7dd3b20 <do_system+544> mov rdx, r14 RDX => 0x7ffff7f9f840 (intr) ◂— 0
0x7ffff7dd3b23 <do_system+547> mov edi, 2 EDI => 2
然后是有关[rsp]
的操作,这里是存放两个值到xmm4,然后调用子函数__GI___libc_cleanup_push_defer
, 这是一个用于清理线程的函数, 之后便没有相关操作了。
0x7ffff7dd3b84 <do_system+644>: movdqa xmm4,XMMWORD PTR [rsp]
......
0x7ffff7dd3bb0 <do_system+688>: movaps XMMWORD PTR [rsp+0x20],xmm4
0x7ffff7dd3bb5 <do_system+693>: call 0x7ffff7e141c0 <__GI___libc_cleanup_push_defer>
再次总结,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
等也会有类似的需求。
// demo2
#include <stdio.h>
#include <stdlib.h>
void backdoor(){
puts("LeakBox");
}
int main(){
size_t array[3];
array[5] = backdoor; // 数组越界
return 0;
}
结果是
► 0x7ffff7e274c0 <_int_malloc+2832> movaps xmmword ptr [rsp + 0x10], xmm1 <[0x7fffffffd758] not aligned to 16 bytes>
0x7ffff7e274c5 <_int_malloc+2837> mov eax, dword ptr [rbx + 8] EAX, [main_arena+8] => 0
0x7ffff7e274c8 <_int_malloc+2840> test eax, eax 0 & 0 EFLAGS => 0x10246 [ cf PF af ZF sf IF df of ]
0x7ffff7e274ca <_int_malloc+2842> ✔ je _int_malloc+3869 <_int_malloc+3869>
► 0 0x7ffff7e274c0 _int_malloc+2832
1 0x7ffff7e279c9 tcache_init.part+57
2 0x7ffff7e281de malloc+318
3 0x7ffff7e281de malloc+318
4 0x7ffff7e01ba4 _IO_file_doallocate+148
5 0x7ffff7e10ce0 _IO_doallocbuf+80
6 0x7ffff7e0ff60 _IO_file_overflow+416
7 0x7ffff7e0e6d5 _IO_file_xsputn+213
8 0x7ffff7e03f1c __GI__IO_puts+204
9 0x555555555180 backdoor+23
不难发现, 涉及malloc
,_IO_file_xsputn
都需要检查,直白点说就是涉及IO的都会有栈平衡问题,但不保证是_IO_file_xsputsn
的问题,比如vprintf本身就有xmm寄存器对齐要求
其次,堆分配(malloc)也会有这类问题,但一般不会很显著
你以为这就完了吗? 怎么会。如果IO能跳过_IO_file_xsputn
,不就可以正常运行了吗,实际上write
和read
就是这样的, 因为这两个单纯就是把syscall
包装了一下
.text:00000000001147D0 ; __unwind {
.text:00000000001147D0 endbr64 ; Alternative name is '__read'
.text:00000000001147D4 mov eax, fs:18h
.text:00000000001147DC test eax, eax
.text:00000000001147DE jnz short loc_1147F0
.text:00000000001147E0 syscall ; LINUX -
.text:00000000001147E2 cmp rax, 0FFFFFFFFFFFFF000h
.text:00000000001147E8 ja short loc_114840
.text:00000000001147EA retn
.text:00000000001147EA ;
再看一个demo
// demo3
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
void backdoor(){
char website[12];
read(0, website, 12);
write(1, "\n", 1);
website[11] = '\n';
write(1, website, 12);
}
int main(){
size_t array[3];
array[5] = backdoor; // 数组越界
return 0;
}
$ ./test
godbolt.org
godbolt.org
[1] 5924 segmentation fault (core dumped) ./test