在一些比较复杂的出程序中,仅仅使用gdb调试起来比较费劲,这时候使用动态库调试技术说不定能让调试变得更加简单。
我认为动态库调试技术一般可以分两类:
- 单纯的加symbols
- 劫持程序流
目录
加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);
}