调用门提权
在我写的2.保护模式+段探测这篇文章中,我们提到了S位对于段描述符的控制,之前我们已经介绍了代码段和数据段,现在我们来把目光转到系统段
在这么多中结构里面,我们今天要介绍的就是编号为12的,32位调用门
结合上面的图,我们先暂时只需要记住,当S = 0 ,type = C,那么这说明这个段描述符是一个调用门
也就是下图所示
Segment Selector
首先我们可以看见16到31位是段选择子,它的意义就是当我们成功进入调用门之后,这里段选择子的值会被放入cs中,以达到跨段跳转的目的
Offset in Segment
通过上面的图,我们可以看见,Offset被分为两段,这也就是我们在跳转时使用的逻辑地址
以我们在4.数据段代码段的权限划分中的部分代码为示例,稍作修改
int _tmain(int argc,_TCHAR* argv[]){
char bufcode[6] = {0,0,0,0,0x48,0};
*(int *)&bufcode[0] = (int)test;
__asm{
call far bufcode;//主要改了这里
}
}
通过查阅资料得知,bufcode里面的前四个字节,在我们之前的jmp far 跨段中是有意义的,但是在调用门使用的过程中,cpu不会处理前四个字节
这里我画出示意图来描述调用门的过程
关于调用门
实现提权
现在我们写一段示例代码来研究这个过程
#include "stdafx.h"
#include <Windows.h>
Void __declspec(naked) Myfunction(){
__asm{
int 3;
retf;
}
}
int _tmain(int argc,_TCHAR* argv[]){
char bufcode[6] = {0,0,0,0,0x48,0};
printf("%x\r\n",Myfunction);
system("pause");
__asm{
call fword ptr bufcode;
}
return 0;
}
在研究之前我们先为了简化实验,首先关闭了编译器自带的增量链接和地址的随机化
做完这两项之后,我们就可以开始思考怎么去构造我们的调用门
在上面的代码中,我用system函数来打了一个暂停,来看看我们裸函数的地址
这里再贴一次上面的调用门的解构图
我们现在一点点向里面填充,首先是我们的相对地址0x401000,上面的一节31到16为我们只有40,补上00
这时候我们的调用门是0040xxxx xxxx1000(x为暂时没确定的值)
之后我们来看0到16的内容,若要我们调用门要有效,P需置为1,DPL位描述的是我们使用这个调用门需要的权限,因为我们再R3来调用它,所以为11,type为固定的1100,后面的567三位都是0,参数我们没有也是0,所以现在我们的调用门被填充成为了0040ec00 xxxx1000
继续向下,最后两字节我们填入0008,原因我稍后介绍,现在,我们的调用门的值被确定为了
0040ec00 00081000
为什么是0008呢,我们之前说过,调用门的段选择子会在被调用的时候填入我们CS中,对于R3,这时的CS为1B,那么对于R0呢?
如果我们使用Windbg的暂停,我们就会进R0的int 3这时候我们看下我们的寄存器
nt!RtlpBreakWithStatusInstruction:
83e6e394 cc int 3
0: kd> r
eax=00000001 ebx=00026160 ecx=9f99a938 edx=00000000 esi=83f2cd20 edi=83f2e654
eip=83e6e394 esp=83f29b84 ebp=83f29bb8 iopl=0 nv up ei pl nz na po nc
cs=0008 ss=0010 ds=0023 es=0023 fs=0030 gs=0000 efl=00000202
可以看见,这时候CS的值是08,这个值有什么特殊意义呢?
它的二进制是1000,如果我们将其看作是段选择子那么它描述的就是RFL为R0和index为1的段描述符,我们打开此时的GDT,可以看见对应index位置上的值为00cf9b00`0000ffff,其DPL为00,这就对应了我们的R0的权限
0: kd> dq 80b99000
ReadVirtual: 80b99000 not properly sign extended
80b99000 00000000`00000000 00cf9b00`0000ffff//在这里哦
80b99010 00cf9300`0000ffff 00cffb00`0000ffff
80b99020 00cff300`0000ffff 80008b1e`400020ab
80b99030 834093f2`cc003748 0040f300`00000fff
80b99040 0000f200`0400ffff 0040ec00`00081000
80b99050 00000000`00081000 830089f2`a0680068
80b99060 00000000`00000000 00000000`00000000
80b99070 800092b9`900003ff 00000000`00000000
顺便一提,如果细心的人,会在上面R0的寄存器中,发现SS的值为10,对应的二进制的值是0001 0000,也就是刚好index为2的段描述符,值为00cf9300`0000ffff,同样是R0,只不过是Type权限变为了可读可写
那如果是CS的1b和SS的23,刚好也在这张表中,SS的值就是CS的值再加8,两个对应的段描述符同样也在上面的GDT表中,分别是00cffb000000ffff和00cff300
0000ffff通过解析我们就可以知道DPL达到值为11,也就是R3
到这里为止,我们算是初步从段的视角来理解Win的权限划分
回到我们得出的调用门,把它填入到我们所call的段选择子的位置
0: kd> eq 80b99048 0040ec00`00081000
WriteVirtual: 80b99048 not properly sign extended
0: kd> dq 80b99000
ReadVirtual: 80b99000 not properly sign extended
80b99000 00000000`00000000 00cf9b00`0000ffff
80b99010 00cf9300`0000ffff 00cffb00`0000ffff
80b99020 00cff300`0000ffff 80008b1e`400020ab
80b99030 834093f2`dc003748 0040f300`00000fff
80b99040 0000f200`0400ffff 0040ec00`00081000
80b99050 830089f2`b0000068 830089f2`b0680068
80b99060 00000000`00000000 00000000`00000000
80b99070 800092b9`900003ff 00000000`00000000
然后继续运行我们代码,可以看见段寄存器已经改为了R0,而且也正是执行的我们在0x401000的语句
最后继续在windbg里面运行,可以看见我们成功的通过retf跳回了我们R3的代码
但是这样结束了吗,打开段寄存器,原本应该被还原为3b的fs,此时标红被记作0000
这是因为我们对切换的fs并没有还原(3b->30),当我们切回R0的时候,因为当前上下文为R3,所以此时还带着R0值的fs便被清零了,继续向下运行,我们会在下一个函数被调用(用到fs的环节)时,出现报错
解决问题的方法很简单,压入一个fs,最后在call完之后将fs弹出即可,这样我们的函数也就正常退出了
提权后我们可以做什么
如果我们想要访问一个R0的地址,不管是读还是写,都是无效的
下面我们举个例子,这是CreateThreadAPI在经过ntdll后,在R0里面的实现,我标出的0x8407bd51这个地址,我们正常在R3是没有办法访问的
例如我们需要在这里写值
会报0xC0000005
现在我们将这个R0的调用门利用起来
唯独要强调一下,声明的全局变量在使用前要置0,这个坑会在后面介绍页表的时候着重说明
其实不需要出来,我们就可以在windbg的反汇编中看见我们已经正确的读取了本来在R0中的值
外面的程序也可以看见这个值,正如我们所设想的一样,我们提权成功了
关于堆栈
之前有一个问题没说,当我们从R0还原为R3时,除了fs,其他的段寄存器都自动还原了
这里我就借用一下羽夏的博客了
然后我们来实践一下,看看长调用提权的esp下压了什么,省略掉之前重复的过程,我们直接来看
进入了R0之后,我们来看esp,可以看见栈里面已经压了所有我们需要的要修复的值
1: kd> r esp
esp=a8497da0
1: kd> dq a8497da0
ReadVirtual: a8497da0 not properly sign extended
a8497da0 0000001b`0040107b 00000023`0012fe4c//这里压了我们的cs,ss的值 ,eip和esp
a8497db0 00000000`00000000 00000000`00000000
a8497dc0 00000000`0000027f 00000000`00000000
a8497dd0 00000000`00000000 0000ffff`00001f80
a8497de0 00000000`00000000 00000000`00000000
a8497df0 00000000`00000000 00000000`00000000
a8497e00 00000000`00000000 00000000`00000000
a8497e10 00000000`00000000 00000000`00000000
最后一个问题,如果我们加上参数呢?
首先要改下调用门的值,告诉我们需要传参数,比如我们就传两个,参数默认的长度是4字节,这需要注意一下
0: kd> eq 80b99048 0040ec02`00081000
WriteVirtual: 80b99048 not properly sign extended
0: kd> dq 80b99000
ReadVirtual: 80b99000 not properly sign extended
80b99000 00000000`00000000 00cf9b00`0000ffff
80b99010 00cf9300`0000ffff 00cffb00`0000ffff
80b99020 00cff300`0000ffff 80008b1e`400020ab
80b99030 834093f6`cc003748 0040f300`00000fff
80b99040 0000f200`0400ffff 0040ec02`00081000//参数处声明有两个参数
80b99050 830089f6`a0000068 830089f6`a0680068
80b99060 00000000`00000000 00000000`00000000
80b99070 800092b9`900003ff 00000000`00000000
继续运行,蓝屏了
还是没有大佬的功力,排查了很久反复蓝屏,还是先到这里吧(逃