pwn学习笔记(1)

发布于 2023-01-07  61 次阅读


手把手教你栈溢出从入门到放弃(下)

首先得熟知函数调用的出入栈的变化

当程序的控制权在函数状态之间发生跳转,通过修改函数状态来实现攻击。而控制程序执行指令最关键的寄存器就是 eip。

eip发生关键变化的地方就是退栈“在被调用函数的参数入栈后,保存的函数调用的下一个指令的地址”,如果这里的保存的值发生变化,那么返回到原函数的地址也就不一样了。

  • 修改返回地址,让其指向溢出数据中的一段指令(shellcode
  • 修改返回地址,让其指向内存中已有的某个函数return2libc
  • 修改返回地址,让其指向内存中已有的一段指令ROP
  • 修改某个被调用函数的地址,让其指向另一个函数(hijack GOT

shellcode

用shellcode的起始地址覆盖掉返回地址,

payload :  padding1 + address of shellcode + padding2 + shellcode

可以看到在被调用函数中的padding1通过计算长度一直覆盖到eip处,而eip处的地址被修改为shellcode地址,再通过padding2覆盖到shellcode的地址。 而关键就在于两个padding是如何构造的;

padding1中应该不含有\X00,否则可能会导致截断,在长度可以通过gdb或者不断尝试输入长度的方法来得到。

shellcode的起始地址:在 padding2 里填充若干长度的 “\x90”(NOP)。

如果操作系统关闭了ASLR(内存布局随机化),那么每次程序函数的返回地址固定,可以通过输入无效的溢出数据来生成core文件,再通过调试工具在core文件中找到返回地址的位置,从而确定 shellcode 的起始地址。

前提是函数调用栈具有可执行权限(通常关闭),所以可以去调用内存中原本就有执行权限的函数或者指令(即return2libc和ROP)

return2libc

和shellcode最大的不同就是,在return处填充的不是shellcode的地址,而是可执行函数的地址,并且shellcode用可执行函数的参数(字符串“/bin/sh”进行替代

payload:  padding1 + address of system() + padding2 + address of “/bin/sh”

这就需要解决几个问题,system()函数的地址在哪?字符串的地址在哪?

system通常在libc 动态链接库中,问题就成为“libc的内存起始地址在哪“,system通过libc的偏移地址就可以得到,而libc起始地址由于ASLR也是比较难定位的;

动态库so中是否存在“/bin/sh”,存在就可以通过偏移量得到,不过不存在就将字符串添加到环境变量中,通过getenv来确定地址

ROP

上面return2libc中有提到,存在目标函数的地址很难找到的问题,那么就可以通过“多个指令片段”拼凑出“操作”

payload : padding + address of gadget

可以看到原本跳转函数的地址被修改成gadget。

而多个指令片段(gadget)如何形成链式跳转呢?gadget的最后一步通过ret指令

将eip切换到下一个gadget

payload : padding + address of gadget 1 + address of gadget 2 + ......+ address of gadget n

拼凑出的”操作”要实现一次系统调用,汇编指令对应int 0x80 。执行这个指令时,被调用的函数编号放入eax,函数参数存入ebx,ecx,edx,esi,edi,如果eax⇒125,那么就会调用mprotect (void *addr, size_t len, int prot)

这个函数可以将栈设为可执行(shellcode),那么ebx⇒内存栈的分段地址,ecx⇒空间长度(0x10000?),edx⇒7(RWX)

那参数如何进入寄存器?如果内存中存在可用数据,就直接mov;如果没有就用pop将栈顶数据弹入寄存器,栈顶数据可以直接在payload中输入。pop应该在gadget地址之后,然后再ret进入下一个gadget。

POP:将栈顶的值(payload中的参数)放入eax,并弹出;esp+2

RET:相当于pop EIP

当mprotect函数将栈设为可执行后,还需要将shellcode加入数据,也就可以使用push esp;ret。

push esp:因为此时shellcode在栈顶,而shellcode此时被push入栈了,ret回去就到了shellcode的起始地址了

pop eax; ret;    # pop stack top into eax
pop ebx; ret;    # pop stack top into ebx
pop ecx; ret;    # pop stack top into ecx
pop edx; ret;    # pop stack top into edx
int 0x80; ret;   # system call
push esp; ret;   # push address of shellcode

开源工具可以实现搜索以 ret 结尾的指令片段,著名的包括 ROPgadget、rp++、ropeme等

hijack GOT

需要在内存中修改被调用函数的地址,指向另一个函数;

程序在链接库内通过GOT和PLT定位到目标函数。

GOT(全局偏移量表),存储外部函数在内存的确切地址,GOT在数据段内,可以在运行中修改;

PLT(程序链接表)存储外部函数的入口点,存储在代码段(不可修改)。而这个入口点就是GOT表中对应条目的位置

延迟重定位

(只有动态库函数在被调用时,才会地址解析和重定位工作)

GOT表的初始值指向PLT表对应条目中的一个片段,这个片段调用函数地址解析函数。

程序调用外部函数→PLT中找入口点→跳转到GOT

(如果第一次调用函数)跳转回PLT进行地址解析→覆盖GOT中初始值→跳转函数地址

(如果已经调用过,即非初始值)GOT直接跳转到函数地址

注意:由GOT中的初始值状态位标志是否是第一次函数调用

再次调用时

由于再次调用时直接信任了GOT表中的函数地址,如果将GOT表中的函数A地址修改为了函数B在内存中地址,就可以实现函数的重定向,让原本调用A变成调用B

那么,如何确定函数A在GOT表中的条目位置?通过PLT表对应到GOT的对应条目

例如call 0x08048430 <printf@plt>就说明 printf 在 PLT 表中的入口点是在 0x08048430,所以 0x08048430 处存储的就是 GOT 表中 printf 的条目地址。

那么,如何确定函数B的地址?函数在动态链接库内的相对位置是固定的,通过PLT确定A函数在内存中的地址以及A&B的相对位置,就可以得到B在运行时的内存位置。

那么,如何修改GOT的数值?通过ROP

# 将printf函数的plt存入eax
pop eax; ret; 		# printf@plt -> eax
# 通过plt将GOT中地址存入ebx
mov ebx [eax]; ret;	# printf@got -> ebx
# 通过PLT计算得到的函数地址差值ecx存入
pop ecx; ret; 		# addr_diff = system - printf -> ecx
# 修改
add [ebx] ecx; ret; 	# printf@got += addr_diff

间桐桜のお菓子屋さん