动态库调试技术

TOC

  1. 1. 加symbols
  2. 2. 泄露信息
  3. 3. 更改初始化函数
  4. 4. 提前停止
  5. 5. 爆破

在一些比较复杂的出程序中,仅仅使用gdb调试起来比较费劲,这时候使用动态库调试技术说不定能让调试变得更加简单。

我认为动态库调试技术一般可以分两类:

  1. 单纯的加symbols
  2. 劫持程序流

加symbols

给目标程序加额外的symbols:由于我们拿到手的程序大多数都是没有symbols,调试起来感觉特别费劲,特别是对于含有多个结构体的程序,没有symbols的话,调试起来将非常麻烦,这时就可以编译一个带symbols的动态库。

举个例子:

// compiled: gcc -g -fPIC -shared hook.c -o hook.so
struct container
{
long long size;
char *malloc_ptr;
struct container * next;
};

struct container no_use;

不定义结构体变量的话,生成的文件中将没有该结构体,应该是编译器识别到并没有使用该结构体,所以直接忽略该结构体了。

之后我们便可以直接给程序套上我们的结构体。

pwndbg> set env LD_PRELOAD=./hook.so
pwndbg> r
Starting program: /home/ex/test/a.out
...
Program received signal SIGINT
pwndbg> ptype ptr
type = <data variable, no debug info>
pwndbg> x/gx &ptr
0x555555755018 <ptr>: 0x0000555555756260
pwndbg> p *(struct container*)ptr
$1 = {
size = 256,
malloc_ptr = 0x5555557562b0 "aaaaaaaa\n",
next = 0x5555557563c0
}

泄露信息

在泄露信息时,我们主要是在.init里加上自己定义的初始化函数,就可以泄露程序基地址以及libc基地址,当然这并不是掩耳盗铃,这仅仅是为了方便调试。

当面对需要用部分覆盖爆破的程序,如果我们每次都是等待覆盖成功才进行调试的话,那么调试一次知识需要做15次覆盖失败的情况,而且如果概率是1/256或者更大的话,那么用这种方法将更加复杂;或许我们可以用gdb手动校正,但是每一次调试都要校正一遍,无疑增加了调试的负担,这时我们就可以利用hook提前泄露出要爆破的地址,这样就能一次完成调试的目的。

举个例子:

// compiled: gcc -g -fPIC -shared hook.c -o hook.so -ldl
#include <stdio.h>
#include <dlfcn.h>

void my_init(void) __attribute__((constructor));

void my_init()
{
printf("image base addr: %p\n", *(char **)dlopen(NULL, RTLD_LAZY));
printf("libc base addr: %p\n", *(char **)dlopen("libc.so.6", RTLD_LAZY));
}

这样我们就能提前计算出地址,方便下一步调试。

ex@Ex:~/test$ gcc -g -fPIC -shared hook.c -o hook.so -ldl
ex@Ex:~/test$ LD_PRELOAD=./hook.so ./a.out
image base addr: 0x559808c7e000
libc base addr: 0x7fea872b5000

void my_init(void) attribute((constructor)); //告诉gcc把这个函数扔到init section
void my_fini(void) attribute((destructor)); //告诉gcc把这个函数扔到fini section

更改初始化函数

对于一些禁止sys_ptrace基地址动态变换的程序,调试变得根本不可能,这时候我们可以用hook提前对程序完成初始化,并关闭程序的一些限制。

可以选择在hook中进行patched或者直接用patched好的程序,用hook来完成被patched的功能。

举个例子:

void __cdecl set_seccomp()
{
__int64 v0; // ST08_8

v0 = seccomp_init(0LL);
seccomp_rule_add(v0, 0x7FFF0000LL, 37LL, 0LL);
seccomp_load(v0);
}

上面的函数把除了sys_alarm的系统调用都禁止了,gdb是没有办法调试的。这是可以用hook来打开sys_ptrace

// compiled: gcc -g -fPIC -shared hook.c -o hook.so -lseccomp
#include <stdio.h>
#include <seccomp.h>

void my_init(void) __attribute__((constructor));

void my_init()
{
scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_KILL);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(alarm), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(ptrace), 0);
seccomp_load(ctx);
}

提前停止

当我们想用pwntools调试一些初始化函数时,由于没有I/O阻塞,程序一下就执行完了,当pwntools这时候attach,可能早就执行完了初始化程序。

由于.init段的函数用于初始化,所以里面的函数是要比main函数执行的早,我们可以先在.init段的函数中设置一个I/O阻塞,这样程序就能提前停下来,我们就能正常的用pwntools这时候attach上去。

代码举例:

// compiled: gcc -g -fPIC -shared hook.c -o hook.so
#include <stdio.h>

void my_init(void) __attribute__((constructor));

void my_init()
{
getchar();
}

爆破

在一些比较复杂的程序中,特别是被混淆过的程序,要对这种程序进行静态分析是很困难的,这时候我们可以通过将函数进行暴力穷举来分析其功能,或者说爆破出我们想要的返回值。

由于函数可能依赖其他section,直接提取汇编代码也基本不可能,用IDA反汇编的源码的话,先不说我们要解决依赖问题,关键是IDA并不能完全还原函数,特别是对于栈的还原,如果是手动还原的话,对于非常复杂的程序我们又难以下手。

这时候就可以用动态库调试技术进行分析。

举个例子:

// compiled: gcc -g -fPIC -shared hook.c -o hook.so -ldl
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>

void my_init(void) __attribute__((constructor));

typedef void (*FUNC)(char *, char *);

void my_init()
{
FUNC func;
char *printable = "_0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";

#define LENGTH 63
char in[8], out[8];
char *image_base = *(void **)dlopen(NULL, RTLD_LAZY);
printf("Image base: %p\n", image_base);
// 爆破 sub_C22 函数
func = image_base + 0xC22;
*(size_t *)in = 0;
for (int i = 0; i < LENGTH; i++)
{
in[0] = printable[i];
for (int ii = 0; ii < LENGTH; ii++)
{
in[1] = printable[ii];
for (int iii = 0; iii < LENGTH; iii++)
{
in[2] = printable[iii];
for (int iiii = 0; iiii < LENGTH; iiii++)
{
in[3] = printable[iiii];
*(size_t *)out = 0;
func(in, out);
switch(*(size_t *)out)
{
case 0x746373:
fprintf(stderr, "%s -> %s\n",in, out);
break;
case 0x395F66:
fprintf(stderr, "%s -> %s\n",in, out);
break;
case 0x323031:
fprintf(stderr, "%s -> %s\n",in, out);
break;
}
}
}
}
}
printf("over\n");
exit(0);
}