so保护总结

so保护总结

so反调试

检查Tracepid

  由于调试进程附加到被调试进程后,目标进程会改变进程状态,将调试进程pid设为本进程的跟踪进程,在进程信息里可查看,/proc/[pid]/status中的TracePid字段就是跟踪进程的pid,如果进程未被跟踪,那么该值为0。所以我们可以通过该值是否为0来判断进程是否被调试。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void readtrace()
{
FILE* fd;
char context[128];
char pidname[128];
int pid=getpid();
LOGI("pid is : %d ",pid);
sprintf(pidname,"/proc/%d/status",pid);
while (1) {
fd = fopen(pidname, "r");
while (fgets(context, 128, fd)) {
if (strncmp(context, "TracerPid", 9)==0) {
int status = atoi(&context[10]);
LOGI("the process status is %d, %s", status, context);
fclose(fd);
syscall(__NR_close, fd);
if (status != 0) {
LOGI("find the process was debugging");
kill(pid, SIGKILL);
return;
}
break;
}
}
sleep(3);
}
}

思路讲解

  通过开辟一个线程,在线程中执行检查代码,首先获取当前进程的pid,在获取该进程的status信息,遍历字段,找到TracePid字段,然后检查该值是否为0,如果不为0,即认为进程被调试,kill掉进程即可。

效果

运行程序,开始循环检查。

a-w400

未被调试时,TracePid值为0.
我们用ida附加上去,观察效果。

点击运行后,发现检查到了被调试,TracePid的值时13615.

我们的调试进程的pid正好时13615.

利用ptrace实现双进程保护

  我们发现ida附加上去后,进程停止了,这时候并不能实现检查,那么我们需要开一个进程去检查而不是线程,所以双进程保护是一个很好的方式。我们利用fork函数来开一个子进程,在子进程中检查父进程时候被调试,而子进程由父进程调试,由于linux下,进程只允许被一个进程调试,那么我们用ida附加到子进程是不被允许的,而如果附加到父进程又会被子进程检测到,所以双进程保护可以起到更好的效果。

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
void readtrace2()
{
FILE *fd;
char filename[128];
char line[128];
int pid=getpid();
LOGI("PID : %d", pid);
sprintf(filename, "/proc/%d/status", pid);
pid_t child=fork();
if (child==0) {
wait(NULL);
long pt = ptrace(PTRACE_TRACEME, 0, 0, 0); //子进程反调试

logptrace("child traceme",pt);
while (1) {
fd = fopen(filename, "r");
while (fgets(line, 128, fd)) {
if (strncmp(line, "TracerPid", 9) == 0) {
int status = atoi(&line[10]);
LOGI("the process status is %d, %s", status, line);
fclose(fd);
syscall(__NR_close, fd);
if (status != 0) {
LOGI("find the process was debugging");
kill(pid, SIGKILL);
return;
}
break;
}
}
sleep(3);
}
} else if(child>0){
wait(NULL);
}else{
LOGE("fork error");
}
}

思路讲解

  父进程fork后,在子进程中完成和之前通用的操作,而在父进程中,要完成对子进程的附加操作。我使用了ptrace,关于ptrace,可以看一下我之前遇到问题时找到的一篇文章,我把它翻译了——链接
  父进程wait(NULL);是为了获取子进程SIGTRAP信号,子进程wait(NULL);是为了通知父进程。因为执行PTRACE操作时,目标进程必须是停止状态。在子进程里PTRACE_TRACEME操作,在之前找到的别人的代码中只要子进程执行PTRACE_TRACEME就会被父进程跟踪,而我实验发现并不可行,必须要有这两个wait来满足被跟踪进程停止的条件。

效果

运行程序:

子进程的PTRACE_TRACE执行成功,子进程开始检查。

子进程的TracePid不为0,子进程已经被跟踪。

9998是父进程的一个线程。

这时候我们尝试用ida附加,先附加父进程。

子进程检查到调试进程,这次不用ida使其运行起来,父进程直接被子进程kill掉了。

我们尝试附加子进程。

发现根本无法附加上去。

完整代码

执行时间校验

通过计算某部分代码的执行时间差来判断是否被调试,在Linux内核下可以通过time、gettimeofday,或者直接通过sys call来获取当前时间。另外,还可以通过自定义SIGALRM信号来判断程序运行是否超时。

文件监控

在Linux下,inotify可以实现监控文件系统事件(打开、读写、删除等),加固方案可以通过inotify监控apk自身的某些文件,某些内存dump技术通过/proc/pid/maps、/proc/pid/mem来实现内存dump,所以监控对这些文件的读写也能起到一定的反调试效果。

调试进程及端口检查

  • 使用IDA动态调试APK时,android_server默认监听23946端口,所以通过检测端口号可以起到一定的反调试作用。具体而言,可以通过检测/proc/net/tcp文件,或者直接system执行命令netstat -apn等。
  • 在对APK进行动态调试时,可能会打开android_server、gdb、gdbserver等调试相关进程,一般情况下,这几个打开的进程名和文件名相同,所以可以通过运行状态下的进程名来检测这些调试相关进程。具体而言,可以通过打开/proc/pid/cmdline、/proc/pid/statue等文件来获取进程名。当然,这种检测方法非常容易绕过――直接修改android_server、gdb、gdbserver的名字即可。

信号机制

调试器设置断点时会发出SIGTRAP信号,系统接受到SIGTRAP信号后,会用当前pc值处的原指令替换bp指令。当在代码中本来就存在bp指令时,就会陷入循环(bp指令替换bp指令)。通过先注册自己的signal函数处理breakpoint异常(SIGTRAP),然后在运行时调用该函数,即可触发自定义SIGTRAP的接管函数。而动态调试时,SIGTRAP会先被调试器接收,处理失败。

from other’s blog:
breakpoint命令会使被调试进程发出信号SIGTRAP。通常调试器会截获Linux系统内给被调试进程的各种信号,由调试者可选地传递给被调试进程。但是SIGTRAP是个例外,因为通常的目标程序中不会出现breakpoint,因为这会使得程序自己奔溃。因此,当调试器遇到SIGTRAP信号时会认为是自己下的断点发出的。这样一来当调试器给这个breakpoint命令插入断点breakpoint后,备份的命令也是breakpoint(原指令),这样当继续执行时,调试器将备份指令恢复并执行,结果误以为备份后这个位置发出的SIGTRAP又是自己下的断点造成的,这样一来就会使得调试器的处理逻辑出现错误,不同的调试器会导致各种不同的问题。

利用信号机制对抗,可以在代码中写好断点,自己实现对应的信号处理函数,在处理函数中加入代码解密等功能,使得调试中擅自去除断点指令会导致崩溃。再对处理函数加密或者混淆,使静态分析难度加大,而在动态调试中就必须跟入信号处理函数。

so加密

简述

section 加密
自定义section,把需要加密的函数定义在该section中,该section将会被加密,在init中将其解密。
加密操作是通过so的信息来获取到自定义section,通过offset和size来获取到指定section,把section所有字节加密,修改section修改ELFheader中几个无用的字段来方便解密时索引。
解密操作,通过之前放置的索引来获取到加密的区域,对其解密后,就可以调用了。
尝试了修改e_shoff,由于是动态注册函数,在加载的时候报错了

1
2
03-13 02:17:51.362 11931-11931/com.fancy.testsoshell E/linker: 
"/data/app/com.fancy.testsoshell-2/lib/arm/libencrypto.so" .dynamic section header was not found

尝试将信息都放置e_entry中是可行的。

代码来源:http://www.wjdiankong.cn/archives/563
文章中使用静态注册+修改e_entry+e_shoff的方法,测试中出现问题,该用动态注册,加密代码稍作了修改。

测试实例

函数加密

ELF格式

elffront

ELF header

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typedef struct{
unsigned char e_ident[EI_NIDENT]; //目标文件标识信息
Elf32_Half e_type; //目标文件类型
Elf32_Half e_machine; //目标体系结构类型
Elf32_Word e_version; //目标文件版本
Elf32_Addr e_entry; //程序入口的虚拟地址,若没有,可为0
Elf32_Off e_phoff; //程序头部表格(Program Header Table)的偏移量(按字节计算),若没有,可为0
Elf32_Off e_shoff; //节区头部表格(Section Header Table)的偏移量(按字节计算),若没有,可为0
Elf32_Word e_flags; //保存与文件相关的,特定于处理器的标志。标志名称采用 EF_machine_flag的格式。
Elf32_Half e_ehsize; //ELF 头部的大小(以字节计算)。
Elf32_Half e_phentsize; //程序头部表格的表项大小(按字节计算)。
Elf32_Half e_phnum; //程序头部表格的表项数目。可以为 0。
Elf32_Half e_shentsize; //节区头部表格的表项大小(按字节计算)。
Elf32_Half e_shnum; //节区头部表格的表项数目。可以为 0。
Elf32_Half e_shstrndx; //节区头部表格中与节区名称字符串表相关的表项的索引。如果文件没有节区名称字符串表,此参数可以为 SHN_UNDEF。
}Elf32_Ehdr;

Program segment header table

1
2
3
4
5
6
7
8
9
10
11
/* Program segment header. */ 
typedef struct {
Elf32_Word p_type; /* Segment type */
Elf32_Off p_offset; /* Segment file offset */
Elf32_Addr p_vaddr; /* Segment virtual address */
Elf32_Addr p_paddr; /* Segment physical address */
Elf32_Word p_filesz; /* Segment size in file */
Elf32_Word p_memsz; /* Segment size in memory */
Elf32_Word p_flags; /* Segment flags */
Elf32_Word p_align; /* Segment alignment */
} Elf32_Phdr;

Section header table

1
2
3
4
5
6
7
8
9
10
11
12
 typedef struct{
Elf32_Word sh_name; //节区名,是节区头部字符串表节区(Section Header String Table Section)的索引。名字是一个 NULL 结尾的字符串。
Elf32_Word sh_type; //为节区类型
Elf32_Word sh_flags; //节区标志
Elf32_Addr sh_addr; //如果节区将出现在进程的内存映像中,此成员给出节区的第一个字节应处的位置。否则,此字段为 0。
Elf32_Off sh_offset; //此成员的取值给出节区的第一个字节与文件头之间的偏移。
Elf32_Word sh_size; //此 成 员 给 出 节 区 的 长 度 ( 字 节 数 )。
Elf32_Word sh_link; //此成员给出节区头部表索引链接。其具体的解释依赖于节区类型。
Elf32_Word sh_info; //此成员给出附加信息,其解释依赖于节区类型。
Elf32_Word sh_addralign; //某些节区带有地址对齐约束.
Elf32_Word sh_entsize; //某些节区中包含固定大小的项目,如符号表。对于这类节区,此成员给出每个表项的长度字节数。
}Elf32_Shdr;

特殊节区

section索引过程:

0+header.e_shoff=section_header_table_startaddress

section名称索引过程:

0+section_header_table[header.strndx].s_offset=string_table_startaddresss

从linker源码看so加载

首先会调用do_dlopen, 其中调用find_library来获取soinfo,find_library调用find_library_internal。
find_library_internal会调用find_loaded_library来查看程序so调用链solist中是否已存在需要加载的so,如果有直接返回soinfo,否则调用load_library来获取soinfo。

1
static soinfo* load_library(const char* name) {
    // Open the file.
    int fd = open_library(name);
    if (fd == -1) {
        DL_ERR("library \"%s\" not found", name);
        return NULL;
    }

    // Read the ELF header and load the segments.
    ElfReader elf_reader(name, fd);
    if (!elf_reader.Load()) {
        return NULL;
    }

    const char* bname = strrchr(name, '/');
    soinfo* si = soinfo_alloc(bname ? bname + 1 : name);
    if (si == NULL) {
        return NULL;
    }
    si->base = elf_reader.load_start();
    si->size = elf_reader.load_size();
    si->load_bias = elf_reader.load_bias();
    si->flags = 0;
    si->entry = 0;
    si->dynamic = NULL;
    si->phnum = elf_reader.phdr_count();
    si->phdr = elf_reader.loaded_phdr();
    return si;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ElfReader elf_reader(name, fd);

ElfReader::ElfReader(const char* name, int fd)
: name_(name), fd_(fd),
phdr_num_(0), phdr_mmap_(NULL), phdr_table_(NULL), phdr_size_(0),
load_start_(NULL), load_size_(0), load_bias_(0),
loaded_phdr_(NULL) {
}

elf_reader.Load()

bool ElfReader::Load() {
return ReadElfHeader() &&
VerifyElfHeader() &&
ReadProgramHeader() &&
ReserveAddressSpace() &&
LoadSegments() &&
FindPhdr();
}

ReadElfHeader():获取header
VerifyElfHeader():校验
magic
header_.e_ident[EI_CLASS]
header_.e_ident[EI_DATA]
header_.e_type
header_.e_version
header_.e_machine
ReadProgramHeader():读取Program segment header table
ReserveAddressSpace():根据program header 计算 SO 需要的内存大小并分配相应的空间
LoadSegments():遍历 program header table,找到类型为 PT_LOAD 的 segment,使用 mmap 将 segment 映射到内存
FindPhdr():获取内存中被加载的段地址

soinfo* si = soinfo_alloc(bname ? bname + 1 : name);
为so分配一个soinfo,并添加到solist中

链接:

a.定位动态节;
b.解析动态节;
c.加载依赖so;
d.重定位:

1
2
phdr_table_get_dynamic_section(phdr, phnum, base, &si->dynamic,
&dynamic_count, &dynamic_flags);

获取.dynamic section 在内存中的地址

1
2
(void) phdr_table_get_arm_exidx(phdr, phnum, base,
&si->ARM_exidx, &si->ARM_exidx_count);

获取arm_exidx section 在内存中的地址

1
2
3
4
5
6
7
8
9
uint32_t needed_count = 0;  
for (Elf32_Dyn* d = si->dynamic; d->d_tag != DT_NULL; ++d) {
DEBUG("d = %p, d[0](tag) = 0x%08x d[1](val) = 0x%08x", d, d->d_tag, d->d_un.d_val);
switch(d->d_tag){
case DT_HASH:
si->nbucket = ((unsigned *) (base + d->d_un.d_ptr))[0];
si->nchain = ((unsigned *) (base + d->d_un.d_ptr))[1];
...

1
2
3
4
5
6
7
typedef struct {
Elf32_Word d_tag; /* entry tag value */
union {
Elf32_Addr d_ptr;
Elf32_Word d_val;
} d_un;
} Elf32_Dyn;

由.dynamic section 各自的tag确定该节用途
d_tag = DT_SYMTAB表示该项存储的是符号表的信息
d_un.d_ptr 表示符号表的虚拟地址的偏移
d_tag = DT_RELSZ d_un.d_val 表示重定位表rel的项数

解析的过程就是遍历数组中的每一项,根据d_tag的不同,获取到不同的信息.

不同tag类型的节使用d_ptr和d_val之一,表达意义也是不一致的。

参考文章:http://blog.csdn.net/feibabeibei_beibei/article/details/53004525

http://blog.csdn.net/feibabeibei_beibei/article/details/52986326