CTF入门——系统调用、shellcode、沙箱(orw)与srop

何为系统调用

那些深奥的难懂的解释网上有一大堆,在这里我解释的通俗一些,最起码让你在CTF中够用即可

简单来说,系统调用(这是个名词哦)就是程序调用底层设备,比如显示屏等等的汇编命令,比如你所熟知的printf函数,最终要调用write这个系统调用,来把你想输出的东西打印到屏幕上,scanf函数,最重要调用read这个系统调用,把你输入的东西放到程序里,如果没有这些系统调用作为交互接口,你该如何和电脑做交互呢?

所以,所谓系统调用,就是调用系统设备的接口,他和函数很像,都需要传入参数,在x86中,通过汇编指令“int 0x80”来实现,在x86-64中,通过汇编指令“syscall”来实现

那如何区分不同的系统调用呢?于是系统调用号的概念应运而生,简而言之就是对不同的系统调用编个号,这样调用的时候直接说一下编号,系统就知道你想调用哪个了,我的另一篇博客收集了32位和64位的系统调用号,这里展示几个常用的吧(以下都是64位程序的系统调用号)

#define __NR_read 0
#define __NR_write 1
#define __NR_open 2
#define __NR_close 3
#define __NR_execve 59

read是读数据的系统调用,write是写数据的系统调用,open、close是打开/关闭文件的系统调用,而execve,是执行系统命令的系统调用,也是system函数会调用的系统调用

要注意,在执行syscall或者int 0x80之前,要把系统调用号放到rax/eax寄存器里,这样系统就知道你要调用哪个系统调用了,rax存放系统调用号,这个很重要哦!

至于其他参数嘛,就仍然按照rdi,rsi,rdx,rcx,r8,r9的顺序来喽,具体的参数有哪些,根据不同的函数自己查找,常见的有

read(int fd, void *buf, size_t count);
  • fd (文件描述符):
    • 表示要读取的文件、管道或其他 I/O 流的文件描述符。
    • 文件描述符是一个非负整数,通常是通过 open 或 socket 等函数获得的。
  • buf (缓冲区):
    • 指向存储读取数据的内存区域的指针。
    • 数据将被写入到这个缓冲区中。
  • count (读取字节数):
    • 表示希望从文件描述符中读取的最大字节数。
    • 如果 count 为 0,read 会立即返回 0,不进行任何实际读取操作。
    • 如果 count 大于可用空间,read 只会读取实际可用的数据量。

读取成功则返回读取的字节数,失败返回-1

write(int fd, const void *buf, size_t count);
  • fd (文件描述符):
    • 表示要写入的目标文件、管道或其他 I/O 流的文件描述符。
    • 文件描述符是一个非负整数,通常是通过 open 或 socket 等函数获得的。
  • buf (缓冲区):
    • 指向包含要写入数据的内存区域的指针。
    • 数据将从这个缓冲区中读取并写入到文件描述符中。
  • count (写入字节数):
    • 表示希望写入的最大字节数。
    • 如果 count 大于可用空间,write 只会写入实际可能的数量。

输出成功则返回输出的字节数,否则返回-1

int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);  // 创建新文件时需要指定权限

open有两种重载,通常只需要指定文件路径和打开模式,即使用第一种方法

  • pathname (文件路径):
    • 指向文件路径的字符串。
    • 可以是绝对路径(如 /home/user/file.txt)或相对路径(如 ./file.txt)。
  • flags (打开模式):
    • 指定如何打开文件。常见的标志包括:
      • O_RDONLY:只读模式。
      • O_WRONLY:只写模式。
      • O_RDWR:读写模式。
      • O_CREAT:如果文件不存在,则创建它。
      • O_EXCL:与 O_CREAT 一起使用,确保文件不存在时才创建(避免覆盖)。
      • O_TRUNC:如果文件已存在,则将其长度截断为 0。
      • O_APPEND:每次写入时将数据追加到文件末尾。
    • 这些标志可以通过按位或 (|) 组合使用。
  • mode (文件权限):
    • 当使用 O_CREAT 标志创建新文件时,必须提供该参数来指定文件权限。
    • 权限通常用八进制表示,例如:
      • S_IRUSR (0400):文件所有者具有读权限。
      • S_IWUSR (0200):文件所有者具有写权限。
      • S_IXUSR (0100):文件所有者具有执行权限。
      • 常见组合:0644(所有者可读写,其他用户只读)或 0755(所有者可读写执行,其他用户可读执行)。
  • O_RDONLY 的值为 0
  • O_WRONLY 的值为 1
  • O_RDWR 的值为 2

flags一般设置为2或4

这里你可能要问,什么是文件描述符,专业解释自行百度,通俗解释请看下文

简单来说,如果你的程序打开了很多文件,那么为了后续方便查找,我们给所有打开的文件编个号,后续根据这个号去索引,就能快速的找到你打开的文件,那么这个索引从0开始排,0,1,2分别对应标准输入,标准输出,标准错误,后续打开的外部文件描述符依次为3,4,5……

open打开文件成功则返回这个文件的文件描述符

所以linux中,其实你的输入输出,也被看做了文件

close(int fd);
  • fd (文件描述符):
    • 要关闭的文件描述符。
    • 文件描述符是一个非负整数,通常是通过 opensocket 或其他 I/O 函数获得的。
execve(const char *pathname, char *const argv[], char *const envp[]);
  • pathname (程序路径):
    • 指向可执行文件路径的字符串。
    • 可以是绝对路径(如 /bin/ls)或相对路径(如 ./a.out)。
  • argv (参数列表):
    • 一个指向字符串数组的指针,表示传递给新程序的参数。
    • 第一个参数通常是可执行文件的名字(惯例,但不是强制要求)。
    • 数组的最后一个元素必须为 NULL,以标记参数列表的结束。
  • envp (环境变量列表):
    • 一个指向字符串数组的指针,表示传递给新程序的环境变量。
    • 每个字符串的格式为 "VAR=value"
    • 数组的最后一个元素也必须为 NULL

这里要提醒的是想拿shell的时候后面两个参数必须设置为0,即执行execve(“/bin/sh”,0,0);这很重要

我们来看几个例子

1.read(0,buf,0x40)

//设置系统调用号为 0 (read)
mov rax, 0
//设置第一个参数:文件描述符 (标准输入 0)
mov rdi, 0
//设置第二个参数:缓冲区地址
lea rsi, [buf] //使用 lea 指令加载 buf 的地址
//设置第三个参数:读取字节数 (40)
mov rdx, 0x40
//执行系统调用
syscall

2.execve(“/bin/sh”,0,0)(这里假设/bin/sh字符串已经写入了0xdeadbeef这个地址)

//设置系统调用号为 59 (execve)
mov rax, 59
//设置第一个参数/bin/sh
mov rdi,0xdeadbeef 
//设置第二个参数为0,这里我用mov的方式实现,下面的xor也可以哦,自由选择
mov rsi,0
//设置第三个参数为0,xor指令是很常见的设置0的指令
xor rdx,rdx
//执行系统调用
syscall

shellcode

看到这了?哎哟,不错嘛

难道你没意识到你已经看懂了shellcode吗?

没错,shellcode很简单,就是利用汇编代码来执行系统调用,从而实现攻击

只不过……

在正常情况下,程序都会开启NX保护,即堆栈不可执行保护,因此,大多数时候你的shellcode能写入的位置,比如栈或者bss段,他们是没有可执行权限的,所以一般题目要考察shellcode的话,会关闭NX保护,或者自己编写一个shellcode函数,专门用于执行你的shellcode,也有可能利用mmap分配一段有可执行权限的空间,当然还有一个杀气,就是mprotect,这也是一个系统调用,可以改变内存的权限,感兴趣的自己去查一下吧,这里就不展开叙述了

在pwntools里,已经集成好了shellcode的生成工具——shellcraft

通常我们会利用shellcraft.sh()来自动生成get shell的shellcode,再利用asm将shellcode转化为比特流

即shellcode=asm(shellcraft.sh())

常见的还有shellcraft.cat(‘flag’),shellcraft.read(fd,buf,size)等等,其原理都是生成了一段shellcode进行系统调用

沙箱(orw)

如果你把前面两个板块都弄懂了,那么恭喜你,你可以学习沙箱与orw了

首先,什么是沙箱?

同样的,专业定义自行百度,这里只说通俗版

简而言之,沙箱分两种,白名单和黑名单,白名单就是只允许你调用某些系统调用,黑名单就是不允许你调用某些系统调用,初学阶段通常白名单允许的调用是open,read,write,黑名单禁止的是execve函数,这也就意味着你无法直接获得程序的shell权限

检测沙箱的工具叫secccomp-tools,安装方法在这篇文章里面有

使用方法:seccomp-tools dump ./程序名

一个典型的白名单沙箱如下

阅读方法和读c语言的代码一样,if怎么怎么样那么goto哪里,这里可以看到if(A==open)那么goto 第17行,也是就allow,所以允许调用open,其他函数同理

一个典型的黑名单沙箱如下

显而易见,如果调用的是execve,那么就会KILL,也就是不允许

好了,无论是白名单还是黑名单,都无法get shell,那么我们该怎么获得shell呢?于是,orw这种专门应对ctf的方法应运而生

o对应的是open,r对应的是read,w对应的是write

如果无法getshell,那我们就可以将flag这个文件读入到内存中,再将它输出到显示屏上,这就是orw

所以我们要先open(“flag”)(需要提前在可控地址读入“flag”这个字符串哦),然后read(3,buf,0x40),注意了,这里的文件描述符是3,为什么呢?前面讲过了,后续打开的文件,其文件描述符从3开始排,于是我们从flag这个文件中,读取了0x40(通常flag的长度不会超过这个数的)的数据到buf这个地址上,一般而言,buf可以再栈上,也可以在bss上,只要可读可写即可。最后,调用write(1,buf,0x40),将刚才写入的flag输出到显示屏上,于是就获得了flag

例题讲解

当然是上课再讲啦~

srop

srop利用的是sigreturn,这也是一个系统调用,他可以清空所有寄存器并重置为自己想要的值,但是很少用到,因为需要很大的溢出量,而有这么大的溢出量,我其实完全可以不用srop,所以用的很少,了解即可

SROP – CTF Wiki

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇