动态库调试技术
TOC
- 1. 加symbols
- 2. 泄露信息
- 3. 更改初始化函数
- 4. 提前停止
- 5. 爆破
在一些比较复杂的出程序中,仅仅使用gdb调试起来比较费劲,这时候使用动态库调试技术说不定能让调试变得更加简单。
我认为动态库调试技术一般可以分两类:
- 单纯的加symbols
- 劫持程序流
加symbols
给目标程序加额外的symbols:由于我们拿到手的程序大多数都是没有symbols,调试起来感觉特别费劲,特别是对于含有多个结构体的程序,没有symbols的话,调试起来将非常麻烦,这时就可以编译一个带symbols的动态库。
举个例子:
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
提前泄露出要爆破的地址,这样就能一次完成调试的目的。
举个例子:
#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;
v0 = seccomp_init(0LL); seccomp_rule_add(v0, 0x7FFF0000LL, 37LL, 0LL); seccomp_load(v0); }
|
上面的函数把除了sys_alarm
的系统调用都禁止了,gdb是没有办法调试的。这是可以用hook
来打开sys_ptrace
。
#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
上去。
代码举例:
#include <stdio.h>
void my_init(void) __attribute__((constructor));
void my_init() { getchar(); }
|
爆破
在一些比较复杂的程序中,特别是被混淆过的程序,要对这种程序进行静态分析是很困难的,这时候我们可以通过将函数进行暴力穷举来分析其功能,或者说爆破出我们想要的返回值。
由于函数可能依赖其他section
,直接提取汇编代码
也基本不可能,用IDA反汇编的源码的话,先不说我们要解决依赖问题,关键是IDA并不能完全还原函数,特别是对于栈的还原,如果是手动还原的话,对于非常复杂的程序我们又难以下手。
这时候就可以用动态库调试技术
进行分析。
举个例子:
#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); 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); }
|