刷题笔记

shellcraft 模块

功能:生成特定架构的汇编代码(如执行 /bin/sh、反弹 Shell 等)。

语法:shellcraft.<架构>.<功能>()

架构:amd64、i386、arm、mips 等。

功能:sh()(获取 shell)、cat(“flag.txt”)(读取文件)、connect(“IP”, PORT)(反弹 Shell)等。

close(1)关闭标准输出流

解决方法 exec 1 > &0

在Linux中

  • 一切都可以作为文件,文件描述符(file descriptor)是内核为了高效管理已被打开的文件所创建的索引,是一个非负整数(通常是小整数),用于指代被打开的文件,所有执行I/O操作的系统调用都通过文件描述符。程序刚刚启动的时候,0是标准输入,1是标准输出,2是标准错误。 如果此时去打开一个新的文件,它的文件描述符会是3。
  • 而标准输入输出的指向是默认的,我们也可以去修改他的指向,也就是重定位文件描述符。 例如,可以用exec 1>myoutput把标准输出重定向到myoutput文件中,也可以用exec 0<myinput把标准输入重定向到myinput文件中,而且,文件名字可以用&+文件描述符来代替。

close(1);close(2);便是关闭了 标准输出标准错误输出

但我们就可以使用重定位文件描述符的办法,将标准输出重定位到标准输入上来达到返回shell的目的(因为默认打开一个终端后,0,1,2都指向的是当前终端,所以该语句相当于重启了标准输出,也就可以看到程序的输出了)

例如exec 1 >&0将标准输出流定位到标准输出流

格式化字符串漏洞

可以通过aaaa-%p%p%p%p%p%p%p测量偏移量

a的ASCII码为61 所以我们只需要找到0x61616161的位置,开始部分到这部分的地址数就是偏移

偏移指的是从格式化字符串开始,到第一个用户可控参数在栈上的相对位置。换句话说,就是确定在格式化字符串函数的参数列表里,用户输入的字符串是第几个参数。例如,当偏移为 3 时,意味着用户输入的格式化字符串是函数参数列表中的第 3 个参数。

针对pie保护的应对方法

开启pie后,系统的低三位地址和原来的低三位地址一样

我们要计算pro_base(pie的基地址)

第一种方法———利用printf的截断机制

C语言中,printf会打印内容到 “”\x00” 截断符号为止

当我们输入的数据超过溢出点所能承受的最大范围后,读入的数据会溢出覆盖这个截断符号,此时溢出字符后

Stack Smash

在做此类题目时,可以在题目文件同一目录下创建flag文件

然后在gdb调试中 使用search flag找到flag的地址

此地址就是flag在远程服务器的真实地址

高glibc版本失效此攻击方法

伪随机数

rand()函数在调用时会使用srand()函数

srand函数设置种子,按照种子生成随机数,但是有一定范围

默认种子为1

例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from pwn import*
from ctypes import *
context(os='linux',arch='amd64',log_level='debug')
elf = ELF('./randnum')
#p = process('./randnum')
ip = '10.129.182.103'

port = 33085
p = remote(ip,port)
libc = cdll.LoadLibrary("/lib/x86_64-linux-gnu/libc.so.6")

payload = 'a'*0x10
#p.sendlineafter('Please enter a number.\n',payload)

libc.srand(1)
for i in range(10):
p.recvuntil('Please enter a number.\n')
payload += str(libc.rand()%1000)
p.sendline(payload)
p.interactive()

如果种子为时间同步,格式为

1
2
seed = int(time.time())
libc.srand(Seed)

call qword ptr [r12+rbx*8]

1. 操作数部分

  • **qword ptr**:这是一个操作数大小修饰符,明确指定操作数的大小为 64 位(8 字节)。在 x86 - 64 架构中,由于可以处理不同大小的数据,使用 qword ptr 可以确保从内存中读取的数据是 64 位的。

  • [r12 + rbx*8]

    :这是一个内存寻址方式,属于基址 - 变址寻址。其中:

    • r12 是基址寄存器,提供一个基础地址。
    • rbx 是变址寄存器,它的值会乘以 8(因为是 *8),然后与 r12 的值相加,得到最终的内存地址。

2. 指令执行步骤

  1. 保存返回地址:将当前 call 指令的下一条指令的地址压入栈中。在 x86 - 64 架构中,栈指针寄存器 rsp 会自动减 8(因为返回地址是 64 位),然后将返回地址存入 rsp 指向的内存位置。
  2. 计算目标地址:计算 r12 + rbx*8 的值,得到内存地址。
  3. 读取目标地址:从计算得到的内存地址处读取一个 64 位的值,这个值就是要调用的子程序的地址。
  4. 跳转执行:将程序的控制权转移到读取到的目标地址处,开始执行子程序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 假设 r12 和 rbx 已经有值
r12 = 0x1000
rbx = 0x2

# 计算内存地址
memory_address = r12 + rbx * 8

# 从内存地址处读取目标地址
target_address = read_memory(memory_address, 8) # 读取 8 字节

# 保存返回地址到栈
rsp = rsp - 8
write_memory(rsp, next_instruction_address)

# 跳转到目标地址
pc = target_address
  • 内存访问权限:如果计算得到的内存地址没有有效的访问权限,会引发内存访问错误(如段错误)。
  • 栈管理:子程序执行完毕后,需要使用 ret 指令从栈中弹出返回地址,并将程序控制权转移回调用处。

通过这种间接调用的方式,可以实现更灵活的程序设计,例如实现函数指针数组等。

寄存器的值对程序的影响(ret2cus&&泄露libc)

  • rbx(值为 0)
    • __libc_csu_init 函数中的特定代码逻辑里,rbx 通常用作计数器。当 rbx 为 0 时,配合后续操作可以避免一些不必要的跳转和错误计算,确保代码按照预期流程执行。
  • rbp(值为 1)
    • rbp 通常用于控制循环或者条件跳转。设置为 1 是为了避免在 __libc_csu_init 函数中的某个条件判断后发生跳转,使得代码能够继续执行后续用于设置寄存器参数的操作。
  • r12(值为 1)
    • ret2csu 利用过程中,r12 最终会被用于设置目标函数的第一个参数。这里将其设置为 1,是因为要调用 write 函数,write 函数的第一个参数 fd 表示文件描述符,1 代表标准输出(stdout),即要将数据输出到标准输出。
  • r13(值为 write_got
    • r13 最终会被用于设置目标函数的第二个参数。对于 write 函数,第二个参数 buf 是要写入的数据的缓冲区地址。write_gotwrite 函数在全局偏移表(GOT)中的地址,这里将其作为缓冲区地址,意味着要将 write 函数在 GOT 表中的内容写入到标准输出。
  • r14(值为 8)
    • r14 最终会被用于设置目标函数的第三个参数。对于 write 函数,第三个参数 count 表示要写入的字节数。设置为 8 是因为在 64 位系统中,地址通常是 8 字节,这里要写入 write 函数在 GOT 表中的地址(8 字节)。
  • r15(值为 write_got
    • r15 一般用于存储要调用的函数的地址。这里将其设置为 write_got,表示要调用 write 函数。
  • func(值为 main_addr
    • 调用完目标函数(这里是 write 函数)后,程序会跳转到 func 所指定的地址继续执行。将其设置为 main_addr,意味着调用完 write 函数后,程序会回到 main 函数重新开始执行,这样可以进行多次利用或者获取更多信息。

堆中的canary

在堆内存管理中,canary(金丝雀值) 是一种用于检测堆溢出的安全机制,其设计思想与栈溢出保护中的 栈 canary 类似,但实现方式和作用场景有所不同。以下是关于堆中 canary 的详细说明:

  1. 堆 canary 的作用
    堆 canary 的核心目标是检测堆块的溢出,防止攻击者通过覆盖相邻堆块的数据(如堆块头部、元数据等)来执行恶意操作(如劫持控制流、篡改内存等)。它通常通过在堆块之间插入特定值,并在释放或访问堆块时验证这些值是否被破坏。
  2. 堆 canary 的常见实现方式
    不同的堆分配器(如 glibc 的 ptmalloc、Google 的 tcmalloc 等)对堆 canary 的实现有所差异。以下是两种典型实现:
    (1)Guard Bytes(保护字节)
    原理:在堆块的末尾插入固定值(如 0x00、0x55、0xAA 等),或随机生成的 canary 值。当堆块被释放时,检查这些值是否被修改,若被修改则判定发生溢出。
    示例(ptmalloc):
    在堆块的 prev_size 和 size 字段之后,可能插入一个 guard byte(如 0x00)。
    当释放堆块时,检查该 guard byte 是否被破坏。
    若堆块被溢出覆盖,guard byte 会被修改,触发程序崩溃。
    (2)Heap Canary(显式随机值)
    原理:在堆块的头部或尾部插入随机生成的 canary 值(类似栈 canary),并在访问或释放堆块时验证该值是否被篡改。
    示例(某些自定义分配器):
1
2
3
4
5
6
struct heap_chunk {
size_t prev_size; // 前一个堆块的大小
size_t size; // 当前堆块的大小
uint64_t canary; // 随机生成的canary值
char data[]; // 用户数据区域
};

分配堆块时,canary 字段被填充为随机值。
释放堆块时,检查 canary 是否与原始值一致,若不一致则判定溢出。

与栈 canary 的区别
特性 栈 canary 堆 canary
位置 位于栈帧的返回地址前 位于堆块的头部、尾部或数据区域边界
生成方式 随机生成(通常由编译器插入) 随机生成或固定值(由分配器决定)
检测时机 函数返回前检查 堆块释放或访问时检查
防御目标 防止栈溢出覆盖返回地址 防止堆溢出破坏相邻堆块或元数据

堆溢出攻击与 canary 的绕过
攻击方式:
攻击者可能通过覆盖堆块的元数据(如 size、prev_size)或相邻堆块的 canary 值,绕过保护机制。
绕过手段:
部分覆盖:仅修改部分 canary 字节,使其仍通过校验。
信息泄露:通过其他漏洞泄露 canary 值,再构造精准攻击。
利用未检查的路径:某些分配器在特定操作(如分配小内存块)时可能跳过 canary 检查。

pthread_create多线程竞争

pthread_create函数是创建线程的主要方式。它的函数声明为int pthread_create(pthread_t restrict tidp,const pthread_attr_t restrict_attr,void(start_rtn)(void),void *restrict arg)。第一个参数为指向线程标识符的指针,线程创建成功后,该指针会被填充上新线程的标识符。第二个参数用于设置线程属性,如果传入NULL,则使用默认的线程属性。第三个参数是线程运行函数的起始地址,该函数的返回值类型为void *,并接受一个void 类型的参数。最后一个参数是运行函数的参数。

[NISACTF 2022]shop_pwn

在售卖阶段会有一个检测bag中商品是否存在的操作,且money是公共资源,但是检测与置空会有一个sleep的延迟,因此只要手速快,那么就可以实现一个doubkle fetch

free函数

free函数在执行的时候(指针),只是将目标的堆内存标记为释放状态,其内容并没有被清空

free函数会将目标堆块的指针放在main_arena中(或是fastbin中)

fastbin

fastbin具有和栈类似的结构(后入先出),当程序需要重新malloc函数并且需要从fastbin中挑取堆块的时候,堆管理器会优先选择后面新加入的堆块来进行分配

TOP chunk

TOP chunk是高地址‌。在堆内存管理中,TOP chunk是指堆内存的顶端部分,即地址最高的部分,尚未被分配的内存区域‌
堆内存是从低地址向高地址增长的,因此TOP chunk位于堆内存的最顶端‌

查找flag

find / -name *flag

find / -name flag*

劫持fini_array重新触发漏洞

什么是什么是init_arrayfini_array

在ELF(Executable and Linkable Format)格式的Linux可执行文件中,有两个特殊的数组:init_arrayfini_array

  • init_array:这个数组包含了一系列函数指针,这些函数在main函数执行之前被自动调用。这些函数通常用于初始化程序运行前需要执行的代码,例如设置环境变量、配置系统资源等。
  • fini_array:这个数组包含了一系列函数指针,这些函数在程序正常退出前被自动调用。这些函数通常用于清理工作,例如释放资源、记录日志等

用gdb调试main函数的时候,不难发现main的返回地址是__libc_start_main也就是说main并不是程序真正开始的地方,__libc_start_main的执行是在main的前面。

可以发现__libc_start_main函数的参数中,有3个是函数指针:

其中__libc_csu_fini是在main执行完毕后执行的

简单地说,在main函数后会调用.init段代码和.init_array段的函数数组中每一个函数指针。而我们的目标就是修改.fini_array数组的第一个元素为start。需要注意的是,这个数组的内容在再次从start开始执行后又会被修改,且程序可读取的字节数有限,因此需要一次性修改两个地址并且合理调整payload

程序结束时会调用_fini_array指向的函数指针,所以我们将其修改为main的地址就会循环调用了

利用过程
让main函数多执行几次,这样就可以控制足够大的内存空间,往里面布置ROP链

ROP攻击的思路:

利用任意写,劫持fini_array

循环执行main,利用任意写,将ROP链布置到fini_array+0x10

终止循环,并将栈迁移到fini_array+0x10执行ROP链

劫持fini_array+循环

fmtstr_payload怎么用?

fmtstr_payload是pwntools里面的一个工具,用来简化对格式化字符串漏洞的构造工作。

可以实现修改任意内存
fmtstr_payload(offset, {printf_got: system_addr})(偏移,{原地址:目的地址})

fmtstr_payload(offset, writes, numbwritten=0, write_size=‘byte’)
第一个参数表示格式化字符串的偏移;
第二个参数表示需要利用%n写入的数据,采用字典形式,我们要将printf的GOT数据改为system函数地址,就写成{printfGOT:
systemAddress};本题是将0804a048处改为0x2223322
第三个参数表示已经输出的字符个数,这里没有,为0,采用默认值即可;
第四个参数表示写入方式,是按字节(byte)、按双字节(short)还是按四字节(int),对应着hhn、hn和n,默认值是byte,即按hhn写。
fmtstr_payload函数返回的就是payload

实际上我们常用的形式是fmtstr_payload(offset,{address1:value1})

fork函数

fork函数在调用时会复制父函数的一切状态包括但不限于代码段、数据段、堆和栈

当程序存在fork函数并触发canary时,__ stack_chk_fail函数只能关闭fork函数所建立的进程,不会让主进程退出,所以当存在大量调用fork函数时,我们可以利用它来一字节一字节的泄露。

如果创建子进程发生了错误的话就会返回-1

沙箱保护(判断是否走execve系统调用)

当我们通过以下

1
2
3
4
5
6
7
8
9
➜  ISCC3 seccomp-tools dump ./attachment-42
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x02 0xc000003e if (A != ARCH_X86_64) goto 0004
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x15 0x00 0x01 0x0000003b if (A != execve) goto 0005
0004: 0x06 0x00 0x00 0x00000000 return KILL
0005: 0x06 0x00 0x00 0x7fff0000 return ALLOW

发现当系统调用号是0x3b时,程序会kill,这种时候就需要走ORW

易混淆!!!

1.system 函数 ≠ execve 系统调用

system 函数:

是 libc 库函数,用于执行一个 shell 命令(如 system(“/bin/sh”))。

它的底层实现会调用 fork + execve 来启动新进程。

system 本身不直接对应系统调用号,但它最终会触发 execve 系统调用。

execve 系统调用:

是 Linux 内核的直接接口,系统调用号为 0x3b(x86-64 架构)。

若沙箱规则禁止 execve(如你提供的 seccomp 规则),则无论通过 system 还是直接调用 execve,都会被拦截并终止进程。

2. 为什么沙箱规则中的 0x3b 对应 execve

在 Linux x86-64 架构中,系统调用号定义如下(可通过 /usr/include/asm/unistd_64.h 查看):

1
#define __NR_execve 59   // 59 = 0x3b

因此:

当程序触发 execve 系统调用时,**rax 寄存器值为 0x3b**。

沙箱规则中的 0x0000003b 正是匹配这一行为,直接拦截 execve

3. system("/bin/sh") 为何被沙箱阻止?

即使你通过 ret2libc 调用 system 函数,其内部依然会执行以下步骤:

  1. system 调用 fork 创建子进程。
  2. 子进程调用 execve("/bin/sh", ...) 启动 shell。
  3. execve 的系统调用号 0x3b 被沙箱检测到,进程被终止

因此,只要沙箱规则禁止 execve,任何试图获取 shell 的操作(包括 system)都会失败

4.如何验证?

查看 system 的底层调用:
使用 strace 跟踪程序执行:

bash
strace -e execve ./program
调用 system(“/bin/sh”) 时,会看到 execve 被触发。

查阅系统调用表:

x86-64 系统调用号列表:https://chromium.googlesource.com/chromiumos/docs/+/master/constants/syscalls.md

关键结论

system 函数依赖 execve,因此沙箱禁用 0x3b(execve)后,system 必然失效。

此题必须使用 ORW 绕过沙箱,直接读取 flag 文件。