Loading... > 本篇是《Hacker Disassembling Uncovered》的笔记。由于这本书出的时间比较长了,我看的版本还是08年出的,里面有很多东西没什么学习的必要,不过多读点书还是好的吧。在这里记个备忘。 ## 第10章 ### TF标志位 在反调试的方法中,有一种方式是使用`PUSHFD /POP REG`命令来读取FLAGS寄存器的值,随后检查追踪标志位TF来判断程序是否被调试。TF—陷阱标志,为程序调试而设。当TF=1,CPU处于单步执行指令的方式。当TF=0时,CPU正常执行程序。下面给出一种检测方式。 ```c int anti_debug() { int a=0; \_\_asm { ; int 03 pushfd pop eax and eax, 100h jnz under_debugger dec [a] under_debugger: } return a; } ``` 但是这种方式我简单试了一下,一直都失败了,原因未知。这里[Windows下常见的反调试方法](https://www.cnblogs.com/lanrenxinxin/p/5193920.html)包含了很多反调试的实现方式,也有用到TF的,但是总的来说这个方式来检测还是有点问题。下面给出一种win32使用api中读取寄存器的方式,我觉得是一种比较好的实现反调试方式。之后有时间就来实现一下。 [Win32调试器读取寄存器和内存](https://www.cnblogs.com/zplutor/archive/2011/03/13/1983010.html) [恶意样本分析手册——反调试篇](http://blog.nsfocus.net/anti-test-articles1/) ### ptrace反调试 上一个笔记记录了[ptrace的用法](http://cjynet.top/2018/02/19/Hacker%20Disassembling%20Uncovered/Hacker-Disassembling-Uncovered%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0%E4%B8%80/),这次来讲一下ptrace也可以用于反调试。首先还是复习一下ptrace的函数原型 ```c #include <sys/ptrace.h> // /usr/include/sys/ptrace.h long ptrace(enum \_\_ptrace_request request, pid_t pid, void *addr, void *data); 1). enum __ptrace_request request:指示了ptrace要执行的命令。 2). pid_t pid: 指示ptrace要跟踪的进程。 3). void *addr: 指示要监控的内存地址。 4). void *data: 存放读取出的或者要写入的数据。 ``` 怎样实现反调试:一个进程只能被一个进程ptrace,如果自己调用ptarce,这样其它程序就无法通过ptrace调试或者向进程注入代码。一般通过检测ptrace的返回值来判断当前是否再被调试。 代码实现:(包括`LD_PRELOAD`检测,也是反HOOK的一种方法) ```c if ( getenv("LD_PRELOAD") ) { while ( 1 ); } //ptrace(PTRACE_TRACEME,0 ,0 ,0) result = ptrace(0, 0LL, 0LL, 0LL); if ( result < 0 ) { while ( 1 ); } return result; ``` 结果如下图所示,可以看到,调用ptrace后,rax寄存器的值是-1,即ptrace调用失败(成功则返回0),继而进入死循环。 ## 第11章 ### PE代码插入和删除 插入分类,有四种。 A类:不改变寻址方式,插入后使文件长度和内存量都不变。应用于在宿主文件的可用空闲区域,包括PE头、分区尾部。 B类:只改变物理寻址方式,即文件长度会发生变化但是分配的文件内存里不会变化,但是由于物理地址的变化导致需要部分或者全部重建相关结构,应用于修改文件头的大小、将原始文件的部分内容移到数据区、创建新的附加数据。 C类:同时改变文件大小和内存映射,应用于拓展文件的最后一个节,创建自己的节等。 Z类:不接触宿主文件但是以间接方式将X码插入到文件地址空间。 关于这一章的内容,我觉得这部书讲的不好,阅读后没什么感觉,关于PE修改的东西,之后读《WindowsPE权威指南的时候》再练习吧。 ## 第12章 ### 实操Linux-crackme linux下的crackme一向不多,这题我下载下来看了,是个好题。特点如下: - ELF文件头遭破坏 - 动态自修改代码 - 反gdb - 反ptrace 运行是可以运行的,调试是不可以调试的(笑),gdb拒绝,objdump不支持分析,ida打开也出现了问题,很明显就是ELF文件头遭到破坏了。记录报错信息:**elf头入口大小无效(PROGRAM_HEADER_OFFSET_IN_FILE)**、**SHT入口大小无效(SECTION_HEADER_ENTRY_SIZE)**,这我们都不管,直接让IDA继续分析好了。 #### 第一部分 ![start](./assets/Snipaste_2018-03-28_11-10-58.png) 一进来就是start函数,entry point 从FOA0x08开始,然后接连两个跳转。 ![start](./assets/Snipaste_2018-03-28_11-45-11.png) 这里先复习下指令 ```sh lodsb指令将esi指向的地址处的数据取出来赋给AL寄存器 mov AL [edi] esi=esi+1 lodsw指令取的是一个字。 lodsd指令取的是双字节,即 mov eax [esi] esi=esi+4 stosb指令将AL寄存器的值取出来赋给edi所指向的地址处。mov [edi] AL edi=edi+1 stosw指令取的是一个字。 stosd指令取的是双字节,即 mov [edi] eax edi=edi+4 ``` 于是分析下`sub_2002F0`这个函数,可以看到,取`loc_20004B`地址,然后循环开始亦或解码。设置ecx寄存器是循环次数0x2a5>>2,即169。每次亦或的值是0x3F5479F1。使用idc编写脚本进行动态求解。 ```c static unpack(addr, key, times) { auto i; for (i = 0; i < times; ) { PatchDword(addr + i, Dword(addr + i) ^ key); //Message("%#x %#x\n", addr + i, Dword(addr + i)); i = i + 4; } } ``` 然后unpack(0x0020004B, 0x3F5479F1, 0x2A5)就能恢复一部分数据,就是start后面的一部分。注意的是,`sub_2002F0`前3个字节也被patch过了,所以解密后回去再看这个函数头部就会有问题,这里没什么用。直接返回到start了。 回到start后,由于之前的加密数据的原因,ida这里现实start已经结束了,其实并没有,这里我修改start函数,结束改为0x002000E0处,可以看到start接下来的部分。 ![start 解密后](./assets/Snipaste_2018-03-28_12-15-43.png) 可以看到,在encode1解密后,立即设置ebx寄存器是0xf4>>2,取`unk_20019E`地址,调用`sub_2002BC`函数,很明显,该函数和上面一样,同样是在解密。调用上面那个函数进行解密。unpack(0x0020019e, 0x0BEEFC0DA, 0xf4),解密完成后,发现就是对明文字符串进行解密。 ![第二次解密](./assets/Snipaste_2018-03-28_12-25-55.png) #### 第二部分 两次解密完成后,取字符串地址,设置edx,同时调用`sub_20029A`函数。执行以下指令。 ```sh xor eax, eax mov ebx, eax ; fd = 0 mov al, 4 ; 4号系统调用 int 80h ; LINUX - sys_write retn ``` 很明显就是该函数就是write函数,打印出第二次解密的一堆字符串。然后继续start函数,接下来就是一个ptrace反调试。可以用cat /usr/include/asm/unistd_32.h |grep ptrace来查看ptrace的系统调用号,在32位中就是26。这里ptrace中的ptrace_request值在ebx寄存器中,为61。接下来判断返回值是不是0,是0则继续,调用失败则打印失败。当成功时,跳转到`loc_20009C`这里。 这里首先压栈,其次调用read函数,这部分和write差不多,syscall是3,fd=1。要注意的是,读取的字符长度只有4,因为ecx指向的就是只有4byte的缓冲区。然后调用check函数,判断返回的ebx和输入是否相等,不相等则失败。相等则弹栈,再比较ebx,ebx为0则成功。 ![关键代码](./assets/Snipaste_2018-03-28_12-49-59.png) #### 第三部分 最后来看一下关键的check函数。可以看到对start开始逐双字进行加法,然后对结果进行亦或,最后返回。需要注意的是,在CRC校验的过程中,输入的字节地址在CRC校验的范围内。即`result = ((CRC + key) ^ 0x5508046b)` ,又和输入的值进行亦或,然后为0则正确。 ![CRC函数](./assets/Snipaste_2018-03-28_13-08-03.png) 这里贴上CRC校验的idc脚本。 ```c static calcCRC(addr, times) { auto i, result; result = 0; for (i = 0; i < times; i++) { result = (result + Dword(addr + 4 * i)); Message("%x\n", Dword(addr + 4 * i)); } Message("result = %x \n", result); //result = 0x7d5c6d9 } calcCRC(0x0200008, 0xb7); ``` 所以得到flag的条件是`((CRC + key) ^ 0x5508046b) ^ key == 0`即`(0x7d5c6d9 + key == key ^ 0x5508046b)`根据这个写脚本爆破就好了,开始我以为是对的,但是跑了半天一个解也没有,很奇怪。然后我仔细看这个key的位置,能发现,key的位置很特殊,DWORD中的低字是上一个DWORD的高字,高字是下一个DWORD的低字。所以两个key是不一样的,`(0x7d5c6d9 + key(转换) == key ^ 0x5508046b)`,之后再爆破即可,同时根据flag是明文来缩小范围。 附完整idc脚本。 ```c #include <idc.idc> static unpack(addr, key, times) { auto i; for (i = 0; i < times; i++) { PatchDword(addr + 4 * i, Dword(addr + 4 * i) ^ key); //Message("%#x %#x\n", addr + 4* i, Dword(addr + 4 * i)); } } static calcCRC(addr, times) { auto i, result; result = 0; for (i = 0; i < times; i++) { result = (result + Dword(addr + 4 * i)); //Message("%x\n", Dword(addr + 4 * i)); } Message("result = %x \n", result); return result; } static getFlag() { auto result, key; result = calcCRC(0x0200008, 0xb7); auto a, b, c, d, temp; for(a = 0x30; a < 0x7f; a++) { for(b = 0x30; b < 0x7f; b++) { for(c = 0x30; c < 0x7f; c++) { for(d = 0x30; d < 0x7f; d++) { key = a << 24 | b << 16 | c << 8 | d; temp = result + ((key << 16) + (key >> 16)); temp = temp ^ 0x5508046b; if((temp ^ key) == 0) { msg("key found!(%x) is %c%c%c%c \n", key, d, c, b, a); } } } } } Message("finished"); } static main() { unpack(0x0020004B, 0x3F5479F1, 0xa9); unpack(0x0020019e, 0xBEEFC0DA, 0x3d); getFlag(); } ``` 最后计算key如果用ida太慢的话,可以用c来爆破,附脚本。 ```c #include <windows.h> #include <stdio.h> using namespace std; int main() { for(BYTE a = 0x30; a < 0x7f; a++) { for(BYTE b = 0x30; b < 0x7f; b++) { for(BYTE c = 0x30; c < 0x7f; c++) { for(BYTE d = 0x30; d < 0x7f; d++) { DWORD key = a << 24 | b << 16 | c << 8 | d; DWORD temp = 0x7d5c6d9 + ((key << 16) + (key >> 16)); temp = temp ^ 0x5508046b; if((temp ^ key) == 0) { printf ("key found!(%x) is %c%c%c%c \n", key, d, c, b, a); } } } } } } ``` 总体来说,这题还是学到很多知识的,自解密代码,CRC校验,包括大小端和对齐问题。以后有空再补一下elf文件格式的相关知识。 ``` ``` Last modification:January 16th, 2021 at 01:34 pm © 允许规范转载 Support 确定不打赏一下支持博主吗 ×Close Appreciate the author Sweeping payments Pay by AliPay