pwn入门记录

x86 x64 c语言栈的格式

先说x86的栈的格式
函数的参数全部在栈上面,有一个ebp寄存器记录当前函数的帧基指针,esp寄存器是栈顶指针
然后栈的粒度是4字节
函数调用时候遵循下面的入栈顺序:
参数n~1依次入栈,然后入栈函数的返回地址(也就是这个被调用函数退出后返回到原来函数的位置),然后入栈ebp,然后才入栈这个被调用函数的局部变量。
入栈ebp之后ebp寄存器就被更改到入栈ebp后的栈顶的地址也就是esp
然后函数调用结束时候返回前需要先恢复寄存器,然后mov %esp,%ebp pop %ebp
所以局部变量的栈溢出可以覆盖返回地址劫持程序执行流
x86有8个4字节寄存器,eax经常是函数返回值
image

对于x64,栈的粒度是8字节,然后ebp esp换成rbp rsp
而且函数调用的参数的前六个会依次存储在寄存器rdi rsi rdx rcx r8 r9,多余的才会存储在栈上,
x64内存地址不能大于0x00007FFFFFFFFFFF,6个字节长度,否则会异常

image

image

对于arm

先看一下 arm 下的函数调用约定,函数的第 1 ~ 4 个参数分别保存在 r0 ~ r3 寄存器中, 剩下的参数从右向左依次入栈, 被调用者实现栈平衡,函数的返回值保存在 r0 中

除此之外,arm 的 b/bl 等指令实现跳转; pc 寄存器相当于 x86 的 eip,保存下一条指令的地址,也是我们要控制的目标

ASLR PIE Canary NX 保护
对于elf文件,为了防止漏洞利用有很多保护。
aslr是linux内核实现的内存布局随机化,/proc/sys/kernel/randomize_va_space,为0关闭aslr,1是部分开启,libc和stack的地址随机化,2是完全开启,heap,stack,libc都随机化。
但是只开启aslr可执行文件的地址不会变
pie:pie是让程序本身变成位置无关代码,在开启aslr情况下可执行文件的加载地址也是随机的,然后plt表 可执行文件地址 heap libc stack全是随机的
image
image

但是只开启pie不开启aslr还是固定的地址,(但是和没有pie时候不一样)
image

另外不管aslr还是pie,都能保持相对偏移不变,所以说如果知道libc里面(这都是动态链接)的一个函数的地址,那么根据其后三位可以得到libc的版本,那么偏移量就定下来了,就可以调用libc里面其他没有在got表上的函数。但是aslr对于libc的随机化,其后12位也是十六进制下后三位保持不变,所以可以根据这个得知libc的版本,这样可以绕过aslr。
aslr下 栈地址是libc的__environ

Canary保护:
canary保护是用来防止栈溢出的,他是在新函数执行时push rbp之后再额外push一个canary value,这个值是从fs寄存器0x28或者0x14处的值(这个看是x86还是x64)
image

image

然后在函数快退出时候会把这个值取出和fs寄存器对应的异或,如果结果不是0会调用__stack_chk_fail函数报错
image

而这个函数会调用__fortify_fail,然后这个函数会输出
image
这个argv[0]就是main函数的argv[0],所以如果覆盖这个就能读取任意地址

然后fs寄存器的值是怎么来的呢,线程pthread结构体里面有个tcbhead_t类型的header
image
image

里面的stack_guard就是canary value,所以一个程序canary不变,即使fork出子进程也是这样
image
这里面tls也能被覆盖
就可以暴力枚举canary的前2 3 4 (5 6 7 8)字节,如果不对就触发stack smashing了,这样32位需要\(256*3\)次,64位需要\(256*7\)
canary的第一个字节是\x00,这是为了防止puts printf啥的把canary打印出来

所以只要把\x00覆盖掉改成别的,然后输出整个字符串就能泄露canary,然后只需要再次利用时候补上canary就行

然后如果说能实现任意地址写入,并且relro没有完全开启,那么可以改got表,直接跳转到getshell的函数或者ropgadget

RELRO保护
有部分和完全的,完全的就是全都是只读,部分的got表可以修改其他只读,正常plt表 dynamic啥的全能改

例题

ret2text

ret2shellcode

pwntools教程 shellcraft asm

ida动态远程调试virtualbox ubuntu上的elf,获取栈的地址

ROP gadget

这样和scanf有点像

name = "Alice"
age = 25
print("My name is %s and I'm %d years old." % (name, age))
print("My name is {} and I'm {} years old.".format(name, age))

context.bits=64可以指定fmtstr_payload的位数

格式化字符串

格式化字符串就是%x$p泄露栈上的值,%s是把栈上的值当做地址打印地址对应的内容(不一定都是字母的字符串,但是00有截断)

然后这样只要字符串在栈上就可以控制格式化字符串为某一个地址有关,然后知道这个字符串相对栈顶的偏移就可以%x$s泄露任意内存

然后%x$n是写入之前(不包括这个)已经成功输出的字符的个数,%n的没有输出不算,所以可以前面输出%yc,y是一个值。这样可以控制写入的值,写入是栈上对应的值作为地址去写入到这个地址

然后劫持程序执行可以通过劫持got表或者泄露rbp然后劫持返回地址,这样可以绕过canary保护

然后格式化字符串不在栈上时候可以想办法找中介p1->p2->p3,就是p1在栈上,这样能控制p2,p2也在栈上,这样能实现任意写入大概

然后还有栈迁移,就是当前函数执行的栈里保存的rbp是上一个函数保存rbp的地址,所以实际上就可以实现任意写入上一个函数保存的rbp,然后上一个函数退出时候mov rsp,rbp.pop rbp.ret这样rbp被修改了但是rsp正常,然后到上上个函数退出时候rsp被改成劫持的rbp了,然后注意栈指针被改了,改到我们的bss段一般是(data段也有可能也有可能是heap段),这一段需要我们提前用任意写入去控制,然后pop一个元素(rsp增加了4),然后ret时候是把栈顶元素当成保存的返回地址了,这样就劫持了控制流

盲打:
首先要求是能重复利用格式化字符串一般,然后先%p确定位数

然后先泄露栈看看flag在不在栈上

不在就考虑泄露binary,比如64位base地址在0x400000,当然要先找到格式化字符串在栈上的偏移,可以AAAAAAAA加上一堆%p,然后看第几个出现0x4141...这样的

然后就可以泄露binary,用%x$s输出,如果返回的是空的说明是\0,那么就是\x00,然后要注意起始和结束打标记防止换行符什么的。这样泄露binary之后就可以看程序大概的逻辑

然后这样got表地址是知道的基本可以考虑劫持got表,把某些函数换成system,然后输入/bin/sh,或者也可以在栈上写入类似栈溢出那样劫持控制流(甚至不用管canary)

堆溢出

很复杂...我们从头开始
先看内存分配相关。

posted @ 2025-12-09 00:03  hicode002  阅读(8)  评论(0)    收藏  举报