0%

反调试技术

在开始前,先用一张图简要说明反调试技术的种类

image-20220412122021162

静态反调试

承接之前学习的PEB结构体,该结构体的成员主要有以下四个更值得关注,且都与反调试有关。

image-20220412122133677

首先复习下如何访问PEB结构体

image-20220412122227236

利用上面的方式再加上上面四个成员的偏移量就可以访问对应的成员了。

BeingDebugged

该成员在值为1是表示进程处在调试状态,为0则正常运行。

Ldr

在进程处于调试状态的堆区中,未使用的内存区域全部由0xFEEEFEEE填充,若将其用NULL覆盖即表明程序处于正常运行状态。

Process Heap

该成员是指向Heap结构体的指针

image-20220412122817083

其中,在正常运行时,Flags和Force Flags成员会被设置为特定的值,分别为0x2,0x0

NtGlobalFlag

调试状态时该成员的值会被设置为0x70,正常运行状态则值为0.

NtQueryInformationProcess

image-20220412141513488

image-20220412141652792

此处的例子出于将会多次涉及参数数值的更改,会很麻烦,所以涉及api hook,

NtQureyObject

首先,当进程处于调试状态时,系统会分配一个调试对象类型的内核对象,通过检测该对象是否存在,即可判断是否处于调试状态。

简要说明检测过程:

通过NtQureyObject()api获取系统各种内核对象的信息。定义如下

image-20220412143047207

通过书中的调试案例可更快理解该反调试的具体过程

ZwSetInformationThread

该api会将线程隐藏,调试器无法接受信息,从而无法调试。

可以让该api不调用,可以解决问题,或者在调用该api之前,将存储在栈中的的第二个参数ThreadInformationClass的值设置为0即可。

and so on

再者就是之前介绍过的TLS回调函数实现反调试了,很经典。

书中最后还介绍了一种通过检测程序运行所处的系统是否为你想专用的系统来决定是否实现反调试。

image-20220412144545994

动态反调试技术

异常是常被用来实现动态反调试的手段之一。本章前面所描述的部分内容可在SEH那章找到。

而当进程中未注册SEH或SEH未处理异常时

image-20220412144910002

Timing Check

原理为当程序在正常运行时和调试时所耗费的时间不同来检测进程是否处于调试状态。

如下是检测方法

image-20220412145143458

RDTSC反调试的破解方式如图

image-20220412145402293

image-20220412145410760

TF与单步

image-20220412145445431

前面的章节提到,Windows中存在单步异常。

在单步执行模式中,该异常触发后,TF会自动清零

需要注意TF的值无法修改

大体流程如下

image-20220412145859419

调试例子可以通过将-1修改为0x401036的值也可绕过反调试。

image-20220412150430023

INT 2D

该异常比较有意思,在调试状态时,解析了该指令后,系统会忽略下一条指令的第一个字节,导致之后指令解析错误。

跟着例子调一下就明白了。

0xCC探测

断点对应的x86指令为0xCC

如果只是扫面0xCC就判断在调试状态,那大概率会“误伤”

故一般会检测一些API函数的第一个字节是否0xCC,所以尽量别在API函数开头设置软件断点,往后一点,或者设置硬件断点即可规避该反调试手段。

比较校验和

程序中如若设置了断电会导致校验和的值不相同,具体可结合书中实例查看

我们只需不在会被计算校验和的代码区设置断点即可规避

高级逆向分析技术

  1. 垃圾代码(花指令)
  2. 扰乱代码对齐(向代码插入经过精巧设计的不必要的代码来降低反汇编代码可读性),该方法会使调试器生成错误的反汇编代码,具体请阅读书籍。
  3. 对关键数据和代码加解密(此中可能会参杂花指令使可读性降低)
  4. Stolen Bytes(在转储进程内存时将EP代码的部分剪切到其他地方,从而使转储的文件无法正常运行反转储技术,被剪切的代码最后会添加jmp指令跳转到还存在的EP代码继续执行)
  5. API重定向;这跟上面那个差不多,API重定向是把所有EP代码都复制到其他区域,再将要保护的代码修改,从而使被复制的代码得以执行(该技术支持反转储)

最后的

image-20220412155347155

Last

这几章的内容都很注重实操,不应该停留在阅读理解。

TLS(Thread Local Storage)回调函数

首先需要注意的一点就是,TLS回调函数是在程序进入OEP之前调用的,因此利用这一特性可以在此设置反调试。

简要说明TLS定义:TLS是各线程的独立的数据存储空间,使用TLS技术可在线程内部独立使用或修改进程的全局数据或静态数据,就像对待自身的局部变量一样。

那么TLS回调函数是指

image-20220409084748247

值得一提的是,TLS表是需要在编程中开启TLS功能才会被设置在PE头文件中

IMAGE_TLS_DIRECTORY结构体有两种版本,32位和64位。

AddressOfCallBacks: 指向TLS注册的回调函数的函数指针(地址)数组(这个逆向时就很重要了)

通过TLS回调函数和DLLmain的定义比较可以发现,二者是类似的

image-20220409085002301

image-20220409085011556

上述图一中,reason是指调用TLS函数的原因

如图所示

image-20220409085140790

分别是DLL进程/线程附加;DLL进程/线程分离

image-20220409182903022

关于调试TLS函数和手动添加TLS函数此处不在描述,过程中涉及对PE文件结构知识的回顾,重在实践。

TEB

贴上书中的定义

image-20220410113647863

丛书中的描述可知,TEB结构体的成员多而复杂,但在用户模式调试中起重要作用的成员有两个

image-20220410113850420

先看ProcessEnvironmentBlock,它是指向PEB—进程环境块结构体的指针.每个进程对应一个PEB结构体

再看NtTib成员,TEB结构体的第一个成员为_NT_TIB(Thread Information Block—线程信息块)结构体

下图为其结构体定义

image-20220410114802311

self成员为该结构体的自引用指针,同时因为该结构体为TEB的第一个结构体成员,所以它同时也是指向TEB结构体的指针。

TEB访问方法(用户模式下)

这里对FS段寄存器做了说明

原文解释的很清楚

image-20220410120445854

由上可知,FS实际存储的是SDT的索引

image-20220410120647654

PEB(进程环境块)

PEB是存放进程信息的结构体

根据PEB结构体地址我们可以用如下方式访问PEB结构体

image-20220410121548028

PEB的重要成员

image-20220410121808724

先看第一个成员,顾名思义,用来检测当前进程是否处于调试状态,是则返回1,否则返回0.

image-20220410122356393

可以看到该API就是通过调用该成员实现检测当前进程是否处于调试状态。

接下来是第二个成员

该进程用来获取进程的ImageBase

image-20220410122602024

上图的API用来获取ImageBase,实现方法与上一个API类似。

再下一个Ldr,该成员是指向PEB_LDR_DATA结构体的指针,结构体成员如下图所示

image-20220410122801231

该成员可直接获取加载到进程的DLL模块的加载基地址

可以看到结构体中有三个_LIST_ENTRY类型的成员。,该结构体定义如下图

image-20220410123646015

最后两个成员都应用于反调试技术。进程处于调试状态时,有特定值。

SEH

image-20220410151204978

SEH机制在程序源代码中使用try,except,finally关键字来实现。

书中第一个例子的异常触发是由于访问未分配的内存。

image-20220410162947013

操作系统的异常处理方法大致可根据程序运行情况分为两大类

  1. 正常运行时处理
  2. 调试运行时处理

第一种操作系统会将异常交给进程处理,若进程代码有相应的解决代码,则顺利处理,否则终止进程运行。

第二种则是交给调试器处理。调试器的权限很多,除了与被调试的进程有几乎一样的权限外,还有被调试者的虚拟内存,寄存器的读写权限。在遇到异常时,调试器会暂停运行,异常处理完后继续调试

下图为几种处理方法

image-20220410165322561

一下是操作系统所定义的—异常

image-20220410165654556

书中着重讨论了5种

EXCEPTION_ACCESS_VIOLATION(C0000005)

这就是访问不存在或不具访问权限的内存区域是所触发的异常—非法访问异常(最常见)

image-20220410170337983

EXCEPTION_BREAKPOINT(80000003)

这就是调试器实现断点的原理,CPU遇到被设置了断点的代码会暂停运行。

实现方法:

​ INT3为设置断点命令的汇编语言,所对应的机器码为0xCC。CPU若遇到了该指令,即触发该异常

EXCEPTION_ILLEGAL_INSTRUCTION(C000001D)

CPU遇到无法解析(在该CPU架构下未定义)的指令时触发异常

EXCEPTION_INT_DIVIDE_BY_ZERO(C0000094)

小学数学经典错误,整数除法当中,分母为0.当然,若分母为某个变量,变量在某个时刻为0,也会触发该异常

。比如一个变量自己跟自己异或,返回0

EXCEPTION_SINGLE_STEP

单步过后程序暂停,程序进入单步模式后,每执行一次单步,就触发一次该异常。

image-20220410172513951

SEH链

原图简洁明了

image-20220410172810751

异常处理函数的定义

image-20220410172842993

该函数传入四个参数,返回一个枚举类型.四个参数都保存异常的相关信息,第三个参数是指向CONTEXT结构体的指针.

该结构体的定义如下

image-20220410180000470

该结构体用来备份CPU寄存器的值(多线程环境下需要).每个线程内部都拥有一个CONTEXT结构体,当CPU暂时离开当前线程去执行其他线程时,CPU寄存器的值就会保存到当前的CONTEXT结构体中,再次运行该线程时,就会用保存的值覆盖寄存器的值,从之前暂停的代码处继续执行.正是因此,OS才可以在多线程下安全运行各线程.

image-20220410180451097

所返回的枚举类型如下

image-20220410183502324

在异常发生时执行异常代码的线程会被中断运行,转而运行SEH,OS会把线程的CONTEXT结构体指针传递给异常处理函数的相应参数.

第一个成员是一个结构体,其定义如下

image-20220410174054428

其中,第一个成员用来指出异常类型;第四个则用来表示发生异常的代码地址.

TEB结构体的第一个成员是TEB.NtTib.ExceptionList,通过它可以很容易地访问进程的SEH链.

image-20220410185707466

SEH安装方法

image-20220410185822888

后面的调试建议动手调,没有什么需要特别说明的。

关于IA32指令解析,就像是在教你如何查阅程序员的字典一样,跟着阅读即可。

IL2CPP

IL2CPP是什么

首先我们来了解一下它的前缀,IL (Intermediate Language-中间语言)—基于堆栈的伪汇编语言;很多时候还会看到CIL(Common Intermediate Language,特指在.Net平台下的IL标准)。这两者是属于通用语言架构和.NET框架的低阶的人类可读的编程语言。目标位.NET框架的语言被编译成(C)IL,然后汇编为字节码。(C)IL类似于一个面向对象的汇编语言,并且他是完全基于堆栈的,并且是在虚拟机上(.Net Framework, Mono VM)运行的语言。

IL2CPP(把IL中间语言转换成C++源代码,然后利用标准c++平台编译器产生二进制文件。简单的来说就是将IL转化为CPP文件。)

Fundamentally, it differs from the current implementation in that the IL2CPP compiler converts assemblies into C++ source code. It then leverages the standard platform C++ compilers to produce native binaries.(这是下面的文章对IL2CPP的部分描述)

这里有一篇Unity3D官方博客的文章,引出了IL2CPP的概念,以及一篇对IL2CPP的介绍。(值得一看)

The future of scripting in Unity

An introduction to IL2CPP internals

文中指出IL2CPP由AOT预编译器和虚拟机两部分组成。并且明确指出IL2CPP不会重新构建整个.NET框架和Mono工具链;MonoC#编译器和Mono的类、库,以及现在支持的功能和MonoAOT使用的第三方库都将会和IL2CPP继续使用

下图为IL2CPP的工具链概念图

il2cpp-toolchain-smaller

文中还提到了AOT预编译(Ahead of Time),可以做个了解。主要是对程序在运行前进行优化。

IL2CPP是怎样工作的

IL2CPP-3

整体流程可以用如上图表示,例子详见上面的第二篇文章

How IL2CPP works

  1. Unityj scripting API 代码被编译为常规的.NETDLL程序集
  2. 所有的不是脚本的一部分的托管程序集由Unused Bytecode Stripper处理(从动态链接库的DLL中去除一些无用的类和方法),这个过程会显著减小创建游戏的大小。(其上在上面那片文章也有提到)
  3. 所有的托管程序集被翻译为标准的C++代码
  4. 生成的C++代码和IL2CPP运行时生成的部分代码被本地平台的编译器编译
  5. 最后,代码会被连接到可执行文件(取决于你的目标平台)或DLL文件。

至于为什么会产生许多无用代码

我们易一段简单的C++代码和IL汇编代码作为示例

1
2
3
4
5
6
7
#include <stdio.h>

int main(int argc, char **argv) {
int a = 1;
int b = 2;
printf("Hello world: %d\r\n", a + b);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
ldc.i4.1 #将常量1压入栈
stloc.0 #将1从栈中取出,并赋给第一个变量a
ldc.i4.2
stloc.1
ldstr "Hello World: {0}" #将字符串压入栈
ldloc.0 #将变量a压入栈
ldloc.1
add #a+b
box System.Int32 #将类型装箱,其实这一步是在一定程度上优化程序
call System.Void System.Console::WriteLine(System.String,System.Object) #输出函数调用
ret

#这块程序集对应上面大括号里的内容

接下来插入一段对应IL2CPP工作的第一部后出现的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// System.Void HelloWorld.Program::Main(System.String[])
IL2CPP_EXTERN_C IL2CPP_METHOD_ATTR void Program_Main_m7A2CC8035362C204637A882EDBDD0999B3D31776 (StringU5BU5D_t933FB07893230EA91C40FF900D5400665E87B14E* ___args0, const RuntimeMethod* method)
{
static bool s_Il2CppMethodInitialized;
if (!s_Il2CppMethodInitialized)
{
il2cpp_codegen_initialize_method (Program_Main_m7A2CC8035362C204637A882EDBDD0999B3D31776_MetadataUsageId);
s_Il2CppMethodInitialized = true;
}
int32_t V_0 = 0;
int32_t V_1 = 0;
{
V_0 = 2;
int32_t L_0 = V_0;
V_1 = ((int32_t)il2cpp_codegen_add((int32_t)1, (int32_t)L_0));
int32_t L_1 = V_1;
int32_t L_2 = L_1;
RuntimeObject * L_3 = Box(Int32_t585191389E07734F19F3156FF88FB3EF4800D102_il2cpp_TypeInfo_var, &L_2);
IL2CPP_RUNTIME_CLASS_INIT(Console_t5C8E87BA271B0DECA837A3BF9093AC3560DB3D5D_il2cpp_TypeInfo_var);
Console_WriteLine_m22F0C6199F705AB340B551EA46D3DB63EE4C6C56(_stringLiteral331919585E3D6FC59F6389F88AE91D15E4D22DD4, L_3, /*hidden argument*/NULL);
return;
}
}

可以看到代码非常复杂。

如此复杂的原因在于IL2CPP对IL字节码线性扫描,并转换为与C++代码等效的非基于堆栈的代码。所以才会产生如此多的多余的变量和赋值操作。

再接下来,是IL2CPP版本的代码

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
; void __fastcall Program_Main_m2325437134(Il2CppObject *__this, StringU5BU5D_t1642385972 *___args0, MethodInfo *method)
Program_Main_m2325437134 proc near
push rbx
sub rsp, 20h
cmp cs:s_Il2CppMethodInitialized_8016, 0
jnz short loc_14038BFF1
mov ecx, cs:?Program_Main_m2325437134_MetadataUsageId@@3IB
call ?InitializeMethodMetadata@MetadataCache@vm@il2cpp@@SAXI@Z
mov cs:s_Il2CppMethodInitialized_8016, 1
loc_14038BFF1:
mov rcx, cs:?Int32_t2071877448_il2cpp_TypeInfo_var@@3PEAUIl2CppClass@@EA
lea rdx, [rsp+48h]
mov dword ptr [rsp+48h], 3
call ?Box@Object@vm@il2cpp@@SAPEAUIl2CppObject@@PEAUIl2CppClass@@PEAX@Z
mov rcx, cs:?Console_t2311202731_il2cpp_TypeInfo_var@@3PEAUIl2CppClass@@EA
mov rbx, rax
test byte ptr [rcx+10Ah], 1
jz short loc_14038C02B
cmp dword ptr [rcx+0BCh], 0
jnz short loc_14038C02B
call ?ClassInit@Runtime@vm@il2cpp@@SAXPEAUIl2CppClass@@@Z
loc_14038C02B:
mov rdx, cs:?_stringLiteral3443654334@@3PEAUString_t2029220233@@EA
xor r9d, r9d
mov r8, rbx
xor ecx, ecx
call Console_WriteLine_m3776981455
add rsp, 20h
pop rbx
retn
Program_Main_m2325437134 endp

可以看到有很多刚开始没有的符号,这时候就需要用一些工具去除这些多余的符号了,完全去除,就可以开始分析代码了。

写在最后

(本文是在做一个Unitylab的时候当作前置知识学习的一篇简短的笔记,原本想着和Unitylab实验的思路一起发出来,还是提前发了。)

实验开始前还得先把文件编译一下

在相应目录下,使用如下命令,如

1
python generate.py 1234 00_angr_find

贴一个angr中文文档angr (非官方)

angr-官方,英文的。angr-api,英语苦手泪目。

(本题中,有些导入的库不一定那个用得到,但是题目数量多,懒得增删改,就一次性导入了…)

00_angr_find

先上伪代码

image-20220308172612146

当然,其实这整个实验,汇编语言是很重要的,伪代码是为了帮助理清程序逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
.text:0804864E                 push    offset s2       ; "FPQPMQXT"
.text:08048653 lea eax, [ebp+s1]
.text:08048656 push eax ; s1
.text:08048657 call _strcmp
.text:0804865C add esp, 10h
.text:0804865F test eax, eax
.text:08048661 jz short loc_8048675
.text:08048663 sub esp, 0Ch
.text:08048666 push offset s ; "Try again."
.text:0804866B call _puts
.text:08048670 add esp, 10h
.text:08048673 jmp short loc_8048685
.text:08048675 ; ---------------------------------------------------------------------------
.text:08048675
.text:08048675 loc_8048675: ; CODE XREF: main+9A↑j
.text:08048675 sub esp, 0Ch
.text:08048678 push offset aGoodJob ; "Good Job."
.text:0804867D call _puts
.text:08048682 add esp, 10h
.text:08048685
.text:08048685 loc_8048685:

(这题可以直接逆来着)

发现程序中正确结果的走向,有good job语句,利用angr的explore,让angr找到相对应路径,执行即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import angr
import sys
import claripy
from Crypto.Util.number import long_to_bytes

def main():
path_to_binary = "E:\\LAB\\angr\\angr\program\\00_angr_find"#打开二进制文件
project = angr.Project(path_to_binary, auto_load_libs=False)#创建对象
initial_state = project.factory.entry_state()#state初始化
simulation = project.factory.simgr(initial_state)

print_good_address = 0x8048678
simulation.explore(find=print_good_address)#寻找地址对象

if simulation.found:
solution_state = simulation.found[0]
solution = solution_state.posix.dumps(sys.stdin.fileno())
print("THE Answer is: {}".format(solution))
else:
raise Exception('Could not find the solution')
if __name__ == "__main__":
main()

01_angr_avoid

这题因为太大反汇编不了,所以还是得看汇编。

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
.text:0804890F                 jz      short loc_804892E
.text:08048911 call avoid_me
.text:08048916 sub esp, 8
.text:08048919 lea eax, [ebp+var_20]
.text:0804891C push eax
.text:0804891D lea eax, [ebp+var_34]
.text:08048920 push eax
.text:08048921 call maybe_good
.text:08048926 add esp, 10h
.text:08048929 jmp loc_80D456F
.text:0804892E ; ---------------------------------------------------------------------------
.text:0804892E
.text:0804892E loc_804892E: ; CODE XREF: main+30D↑j
.text:0804892E sub esp, 8
.text:08048931 lea eax, [ebp+var_20]
.text:08048934 push eax
.text:08048935 lea eax, [ebp+var_34]
.text:08048938 push eax
.text:08048939 call maybe_good
.text:0804893E add esp, 10h
.text:08048941 jmp loc_80D456F
.text:08048946 ; ---------------------------------------------------------------------------
.text:08048946
.text:08048946 loc_8048946: ; CODE XREF: main+2E5↑j
.text:08048946 call avoid_me

.....
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
39
40
41
42
43
text:080485B5                 public maybe_good
.text:080485B5 maybe_good proc near ; CODE XREF: main+31F↓p
.text:080485B5 ; main+337↓p ...
.text:080485B5
.text:080485B5 arg_0 = dword ptr 8
.text:080485B5 arg_4 = dword ptr 0Ch
.text:080485B5
.text:080485B5 ; __unwind {
.text:080485B5 push ebp
.text:080485B6 mov ebp, esp
.text:080485B8 sub esp, 8
.text:080485BB movzx eax, should_succeed
.text:080485C2 test al, al
.text:080485C4 jz short loc_80485EF
.text:080485C6 sub esp, 4
.text:080485C9 push 8
.text:080485CB push [ebp+arg_4]
.text:080485CE push [ebp+arg_0]
.text:080485D1 call _strncmp
.text:080485D6 add esp, 10h
.text:080485D9 test eax, eax
.text:080485DB jnz short loc_80485EF
.text:080485DD sub esp, 0Ch
.text:080485E0 push offset aGoodJob ; "Good Job."
.text:080485E5 call _puts
.text:080485EA add esp, 10h
.text:080485ED jmp short loc_80485FF
.text:080485EF ; ---------------------------------------------------------------------------
.text:080485EF
.text:080485EF loc_80485EF: ; CODE XREF: maybe_good+F↑j
.text:080485EF ; maybe_good+26↑j
.text:080485EF sub esp, 0Ch
.text:080485F2 push offset aTryAgain ; "Try again."
.text:080485F7 call _puts
.text:080485FC add esp, 10h
.text:080485FF
.text:080485FF loc_80485FF: ; CODE XREF: maybe_good+38↑j
.text:080485FF nop
.text:08048600 leave
.text:08048601 retn
.text:08048601 ; } // starts at 80485B5
.text:08048601 maybe_good endp
.text:08048601

有俩个函数,avoid me和maybegood。字面意思,正确寻址应该在后者里。

查找到goodjob地址为0x080485E0

要避开的tryagain在0x080485F2

(其实这题就把上一题的脚本拿来改个地址也可以得出答案,不过会慢很多,因为不会避开avoidme函数)

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
import angr
import sys
import claripy
from Crypto.Util.number import long_to_bytes

def main():
path_to_binary = "E:\\LAB\\angr\\angr\program\\01_angr_avoid"
project = angr.Project(path_to_binary, auto_load_libs=False)
initial_state = project.factory.entry_state()
simulation = project.factory.simgr(initial_state)

avoid_me_address = 0x080485A8#avoidme函数首地址,不推介用tryagain来避开,应为没有避开avoidme函数,时间要很久。
maybe_good_address = 0x080485E0

simulation.explore(find=maybe_good_address, avoid=avoid_me_address)

if simulation.found:
solution_state = simulation.found[0]
solution = solution_state.posix.dumps(sys.stdin.fileno())
print("ANSWER is: {}".format(solution))
else:
raise Exception('Could not find the solution')
if __name__ == "__main__":
main()

这个脚本相比于上一道题,在于explore中多了一个avoid=要排除执行的路径。

02_angr_find_condition

image-20220308190311979

首先乍一看,多个goodjob,这题肯定就不能直接寻址goodjob了,不唯一。

image-20220308190505546

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
39
40
41
42
43
44
45
46
47
48
.text:0804876B loc_804876B:                            ; CODE XREF: main+112↑j
.text:0804876B cmp [ebp+var_38], 0DEADBEEFh
.text:08048772 jz short loc_80487B5
.text:08048774 sub esp, 8
.text:08048777 lea eax, [ebp+s2]
.text:0804877A push eax ; s2
.text:0804877B lea eax, [ebp+s1]
.text:0804877E push eax ; s1
.text:0804877F call _strcmp
.text:08048784 add esp, 10h
.text:08048787 test eax, eax
.text:08048789 jz short loc_80487A0
.text:0804878B sub esp, 0Ch
.text:0804878E push offset s ; "Try again."
.text:08048793 call _puts
.text:08048798 add esp, 10h
.text:0804879B jmp loc_804D267
.text:080487A0 ; ---------------------------------------------------------------------------
.text:080487A0
.text:080487A0 loc_80487A0: ; CODE XREF: main+1C1↑j
.text:080487A0 sub esp, 0Ch
.text:080487A3 push offset aGoodJob ; "Good Job."
.text:080487A8 call _puts
.text:080487AD add esp, 10h
.text:080487B0 jmp loc_804D267
.text:080487B5 ; ---------------------------------------------------------------------------
.text:080487B5
.text:080487B5 loc_80487B5: ; CODE XREF: main+1AA↑j
.text:080487B5 sub esp, 8
.text:080487B8 lea eax, [ebp+s2]
.text:080487BB push eax ; s2
.text:080487BC lea eax, [ebp+s1]
.text:080487BF push eax ; s1
.text:080487C0 call _strcmp
.text:080487C5 add esp, 10h
.text:080487C8 test eax, eax
.text:080487CA jz short loc_80487E1
.text:080487CC sub esp, 0Ch
.text:080487CF push offset s ; "Try again."
.text:080487D4 call _puts
.text:080487D9 add esp, 10h
.text:080487DC jmp loc_804D267
.text:080487E1 ; ---------------------------------------------------------------------------
.text:080487E1
.text:080487E1 loc_80487E1: ; CODE XREF: main+202↑j
.text:080487E1 sub esp, 0Ch
.text:080487E4 push offset aGoodJob ; "Good Job."
.text:080487E9 call _puts

当然,这题想要直接逆也是可以的,不过我们还是用angr

首先是思路:够狠上面一样题的是,还是应该回避tryagain一类的失败路径,然后再设置两个回调函数,当返回tryagain时,回避路径,反之则不回避。

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
import angr
import sys
import claripy
from Crypto.Util.number import long_to_bytes

def main():
path_to_binary = "E:\\LAB\\angr\\angr\program\\02_angr_find_condition"
project = angr.Project(path_to_binary, auto_load_libs=False)
initial_state = project.factory.entry_state()
simulation = project.factory.simgr(initial_state)

def is_successful(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())#获取模拟执行的控制台输出
if b'Good Job.' in stdout_output:#根据模拟控制台得输出结果来判断,下面同理。
return True#发现路径
else:
return False#忽略路径

def should_abort(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
if b'Try again.' in stdout_output:
return True
else:
return False

simulation.explore(find=is_successful, avoid=should_abort)

if simulation.found:
solution_state = simulation.found[0]
solution = solution_state.posix.dumps(sys.stdin.fileno())
print("The ANSWER is: {}".format(solution))
else:
raise Exception('Could not find the solution')

if __name__ == "__main__":
main()

03_angr_symbolic_registers

image-20220308211050475

上面是伪代码。

主函数汇编

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
39
40
41
42
43
44
45
46
47
48
49
50
.text:080488E8 main            proc near               ; DATA XREF: _start+17↑o
.text:080488E8
.text:080488E8 var_14 = dword ptr -14h
.text:080488E8 var_10 = dword ptr -10h
.text:080488E8 var_C = dword ptr -0Ch
.text:080488E8 var_4 = dword ptr -4
.text:080488E8 argc = dword ptr 8
.text:080488E8 argv = dword ptr 0Ch
.text:080488E8 envp = dword ptr 10h
.text:080488E8
.text:080488E8 ; __unwind {
.text:080488E8 lea ecx, [esp+4]
.text:080488EC and esp, 0FFFFFFF0h
.text:080488EF push dword ptr [ecx-4]
.text:080488F2 push ebp
.text:080488F3 mov ebp, esp
.text:080488F5 push ecx
.text:080488F6 sub esp, 14h
.text:080488F9 sub esp, 0Ch
.text:080488FC push offset aEnterThePasswo ; "Enter the password: "
.text:08048901 call _printf
.text:08048906 add esp, 10h
.text:08048909 call get_user_input
.text:0804890E mov [ebp+var_14], eax
.text:08048911 mov [ebp+var_10], ebx
.text:08048914 mov [ebp+var_C], edx
.text:08048917 sub esp, 0Ch
.text:0804891A push [ebp+var_14]
.text:0804891D call complex_function_1
.text:08048922 add esp, 10h
.text:08048925 mov ecx, eax
.text:08048927 mov [ebp+var_14], ecx
.text:0804892A sub esp, 0Ch
.text:0804892D push [ebp+var_10]
.text:08048930 call complex_function_2
.text:08048935 add esp, 10h
.text:08048938 mov ecx, eax
.text:0804893A mov [ebp+var_10], ecx
.text:0804893D sub esp, 0Ch
.text:08048940 push [ebp+var_C]
.text:08048943 call complex_function_3
.text:08048948 add esp, 10h
.text:0804894B mov ecx, eax
.text:0804894D mov [ebp+var_C], ecx
.text:08048950 cmp [ebp+var_14], 0
.text:08048954 jnz short loc_8048962
.text:08048956 cmp [ebp+var_10], 0
.text:0804895A jnz short loc_8048962
.text:0804895C cmp [ebp+var_C], 0
.text:08048960 jz short loc_8048974

可以看到,四个函数调用。第一个就是让我们输入三个数;eax,ebx,edx分别存放输入数据,也是接下来三个函数的参数。

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
39
40
41
.text:0804889A get_user_input  proc near               ; CODE XREF: main+21↓p
.text:0804889A
.text:0804889A var_18 = dword ptr -18h
.text:0804889A var_14 = dword ptr -14h
.text:0804889A var_10 = dword ptr -10h
.text:0804889A var_C = dword ptr -0Ch
.text:0804889A
.text:0804889A ; __unwind {
.text:0804889A push ebp
.text:0804889B mov ebp, esp
.text:0804889D sub esp, 18h
.text:080488A0 mov ecx, large gs:14h
.text:080488A7 mov [ebp+var_C], ecx
.text:080488AA xor ecx, ecx
.text:080488AC lea ecx, [ebp+var_10]
.text:080488AF push ecx
.text:080488B0 lea ecx, [ebp+var_14]
.text:080488B3 push ecx
.text:080488B4 lea ecx, [ebp+var_18]
.text:080488B7 push ecx
.text:080488B8 push offset aXXX ; "%x %x %x"
.text:080488BD call ___isoc99_scanf
.text:080488C2 add esp, 10h
.text:080488C5 mov ecx, [ebp+var_18]
.text:080488C8 mov eax, ecx
.text:080488CA mov ecx, [ebp+var_14]
.text:080488CD mov ebx, ecx
.text:080488CF mov ecx, [ebp+var_10]
.text:080488D2 mov edx, ecx
.text:080488D4 nop
.text:080488D5 mov ecx, [ebp+var_C]
.text:080488D8 xor ecx, large gs:14h
.text:080488DF jz short locret_80488E6
.text:080488E1 call ___stack_chk_fail
.text:080488E6 ; ---------------------------------------------------------------------------
.text:080488E6
.text:080488E6 locret_80488E6: ; CODE XREF: get_user_input+45↑j
.text:080488E6 leave
.text:080488E7 retn
.text:080488E7 ; } // starts at 804889A
.text:080488E7 get_user_input endp

我们看接下来三个函数

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
.text:08048509 complex_function_1 proc near            ; CODE XREF: main+35↓p
.text:08048509
.text:08048509 arg_0 = dword ptr 8
.text:08048509
.text:08048509 ; __unwind {
.text:08048509 push ebp
.text:0804850A mov ebp, esp
.text:0804850C xor [ebp+arg_0], 0A78D4A5Fh
.text:08048513 add [ebp+arg_0], 3EC98793h
.text:0804851A xor [ebp+arg_0], 51FCDF96h
.text:08048521 add [ebp+arg_0], 546F1B35h
.text:08048528 add [ebp+arg_0], 7CD06332h
.text:0804852F add [ebp+arg_0], 3BEEDF0Bh
.text:08048536 add [ebp+arg_0], 5824146Ch
.text:0804853D add [ebp+arg_0], 15CBC4FFh
.text:08048544 mov ecx, [ebp+arg_0]
.text:08048547 sub ecx, 46C32D9h
.text:0804854D mov [ebp+arg_0], ecx
.text:08048550 xor [ebp+arg_0], 9B99C626h
.text:08048557 add [ebp+arg_0], 6590A23Dh
.text:0804855E xor [ebp+arg_0], 0D16C9C3Ch
.text:08048565 xor [ebp+arg_0], 0D73D3031h
.text:0804856C xor [ebp+arg_0], 37EB59D3h
.text:08048573 mov ecx, [ebp+arg_0]
.text:08048576 sub ecx, 5DDF68E6h
.text:0804857C mov [ebp+arg_0], ecx
.text:0804857F mov ecx, [ebp+arg_0]
.text:08048582 sub ecx, 64F2CB17h
.text:08048588 mov [ebp+arg_0], ecx
.text:0804858B xor [ebp+arg_0], 0F1C347B6h
.text:08048592 xor [ebp+arg_0], 0BB664966h
.text:08048599 xor [ebp+arg_0], 0DCD816D2h
.text:080485A0 xor [ebp+arg_0], 0A523DD67h
.text:080485A7 add [ebp+arg_0], 2C64C6B3h
.text:080485AE mov ecx, [ebp+arg_0]
.text:080485B1 sub ecx, 632C11C3h
.text:080485B7 mov [ebp+arg_0], ecx
.text:080485BA add [ebp+arg_0], 220BCB4Ch
.text:080485C1 xor [ebp+arg_0], 0C13B368Ah
.text:080485C8 xor [ebp+arg_0], 7323522h
.text:080485CF mov ecx, [ebp+arg_0]
.text:080485D2 sub ecx, 7FA5CE75h
.text:080485D8 mov [ebp+arg_0], ecx
.text:080485DB add [ebp+arg_0], 2BA5F91Ah
.text:080485E2 xor [ebp+arg_0], 0CEF3925Eh
.text:080485E9 xor [ebp+arg_0], 879B8252h
.text:080485F0 add [ebp+arg_0], 6D4F923Bh
.text:080485F7 mov ecx, [ebp+arg_0]
.text:080485FA sub ecx, 491AF770h
.text:08048600 mov [ebp+arg_0], ecx
.text:08048603 xor [ebp+arg_0], 96C75B6h
.text:0804860A xor [ebp+arg_0], 51EC989Bh
.text:08048611 xor [ebp+arg_0], 0DE67C3E3h
.text:08048618 add [ebp+arg_0], 2C688544h
.text:0804861F xor [ebp+arg_0], 6A479F09h
.text:08048626 xor [ebp+arg_0], 0D8B0FF0Dh
.text:0804862D xor [ebp+arg_0], 37172519h
.text:08048634 mov ecx, [ebp+arg_0]
.text:08048637 mov eax, ecx
.text:08048639 pop ebp
.text:0804863A retn
.text:0804863A ; } // starts at 8048509
.text:0804863A complex_function_1 endp

逻辑简单,就是异或。

来说整体思路:

首先是三个参数,要异或过后符合要求,输出goodjob。由于这题没有指出最后要比较的三个数的具体的值,所以我们不能套用前面那的方法。此处应该控制输入的三个值。

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import angr
import sys
import claripy
from Crypto.Util.number import long_to_bytes

def main():
path_to_binary = "E:\\LAB\\angr\\angr\program\\03_angr_symbolic_registers"
project = angr.Project(path_to_binary)
start_addr = 0x0804890E
initial_state = project.factory.blank_state(addr=start_addr)

passwd_size_in_bits = 32
passwd0 = claripy.BVS('passwd0', passwd_size_in_bits)#开始初始化符号位向量
passwd1 = claripy.BVS('passwd1', passwd_size_in_bits)
passwd2 = claripy.BVS('passwd2', passwd_size_in_bits)

initial_state.regs.eax = passwd0#更新寄存器内容
initial_state.regs.ebx = passwd1
initial_state.regs.edx = passwd2

simulation = project.factory.simgr(initial_state)

def is_successful(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
if b'Good Job.' in stdout_output:
return True
else:
return False

def should_abort(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
if b'Try again.' in stdout_output:
return True
else:
return False

simulation.explore(find=is_successful, avoid=should_abort)

if simulation.found:
for i in simulation.found:
solution_state = i
solution0 = format(solution_state.solver.eval(passwd0), 'x')
solution1 = format(solution_state.solver.eval(passwd1), 'x')
solution2 = format(solution_state.solver.eval(passwd2), 'x')
solution = solution0 + " " + solution1 + " " + solution2
print("The ANSWER is: {}".format(solution))

else:
raise Exception('Could not find the solution')

if __name__ == "__main__":
main()

04_angr_symbolic_stack

image-20220308215004547

可以明确,关键在handle_user函数里面

image-20220308215133653

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
handle_user     proc near               ; CODE XREF: main+21↓p
.text:08048679
.text:08048679 var_10 = dword ptr -10h
.text:08048679 var_C = dword ptr -0Ch
.text:08048679
.text:08048679 ; __unwind {
.text:08048679 push ebp
.text:0804867A mov ebp, esp
.text:0804867C sub esp, 18h
.text:0804867F sub esp, 4
.text:08048682 lea eax, [ebp+var_10]
.text:08048685 push eax
.text:08048686 lea eax, [ebp+var_C]
.text:08048689 push eax
.text:0804868A push offset aUU ; "%u %u"
.text:0804868F call ___isoc99_scanf
.text:08048694 add esp, 10h
.text:08048697 mov eax, [ebp+var_C]
.text:0804869A sub esp, 0Ch
.text:0804869D push eax
.text:0804869E call complex_function0
.text:080486A3 add esp, 10h
.text:080486A6 mov [ebp+var_C], eax
.text:080486A9 mov eax, [ebp+var_10]
.text:080486AC sub esp, 0Ch
.text:080486AF push eax
.text:080486B0 call complex_function1
.text:080486B5 add esp, 10h
.text:080486B8 mov [ebp+var_10], eax
.text:080486BB mov eax, [ebp+var_C]
.text:080486BE cmp eax, 0D3062A4Ch
.text:080486C3 jnz short loc_80486CF
.text:080486C5 mov eax, [ebp+var_10]
.text:080486C8 cmp eax, 694E5BA0h
.text:080486CD jz short loc_80486E1
.text:080486CF
.text:080486CF loc_80486CF: ; CODE XREF: handle_user+4A↑j
.text:080486CF sub esp, 0Ch
.text:080486D2 push offset s ; "Try again."
.text:080486D7 call _puts
.text:080486DC add esp, 10h
.text:080486DF jmp short loc_80486F1
.text:080486E1 ; ---------------------------------------------------------------------------
.text:080486E1
.text:080486E1 loc_80486E1: ; CODE XREF: handle_user+54↑j
.text:080486E1 sub esp, 0Ch
.text:080486E4 push offset aGoodJob ; "Good Job."
.text:080486E9 call _puts
.text:080486EE add esp, 10h
.text:080486F1
.text:080486F1 loc_80486F1: ; CODE XREF: handle_user+66↑j
.text:080486F1 nop
.text:080486F2 leave
.text:080486F3 retn
.text:080486F3 ; } // starts at 8048679
.text:080486F3 handle_user endp

我们可以看一下complex_function0的函数汇编代码

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
39
40
41
42
43
44
.text:080484A9 complex_function0 proc near             ; CODE XREF: handle_user+25↓p
.text:080484A9
.text:080484A9 arg_0 = dword ptr 8
.text:080484A9
.text:080484A9 ; __unwind {
.text:080484A9 push ebp
.text:080484AA mov ebp, esp
.text:080484AC xor [ebp+arg_0], 0D53642BEh
.text:080484B3 xor [ebp+arg_0], 58FC2926h
.text:080484BA xor [ebp+arg_0], 25596A36h
.text:080484C1 xor [ebp+arg_0], 0A7AFAA43h
.text:080484C8 xor [ebp+arg_0], 1559CAFEh
.text:080484CF xor [ebp+arg_0], 0D8D89C66h
.text:080484D6 xor [ebp+arg_0], 6B8B30B6h
.text:080484DD xor [ebp+arg_0], 0B5E7C180h
.text:080484E4 xor [ebp+arg_0], 1FA429F6h
.text:080484EB xor [ebp+arg_0], 21C70AF4h
.text:080484F2 xor [ebp+arg_0], 0B7261E1Dh
.text:080484F9 xor [ebp+arg_0], 0ADD88AD8h
.text:08048500 xor [ebp+arg_0], 3E16A0F2h
.text:08048507 xor [ebp+arg_0], 0DF2308FBh
.text:0804850E xor [ebp+arg_0], 2273AAFh
.text:08048515 xor [ebp+arg_0], 8E69AC70h
.text:0804851C xor [ebp+arg_0], 0AC8924h
.text:08048523 xor [ebp+arg_0], 561B782h
.text:0804852A xor [ebp+arg_0], 5A64A924h
.text:08048531 xor [ebp+arg_0], 0B118005Bh
.text:08048538 xor [ebp+arg_0], 61461EA2h
.text:0804853F xor [ebp+arg_0], 0E0E04E79h
.text:08048546 xor [ebp+arg_0], 0A8DDACAAh
.text:0804854D xor [ebp+arg_0], 82AF667Dh
.text:08048554 xor [ebp+arg_0], 0B3CB4464h
.text:0804855B xor [ebp+arg_0], 43B7BB1Ah
.text:08048562 xor [ebp+arg_0], 0DF30F25Bh
.text:08048569 xor [ebp+arg_0], 4C0F3376h
.text:08048570 xor [ebp+arg_0], 0B2E462E5h
.text:08048577 xor [ebp+arg_0], 7BF4CFC3h
.text:0804857E xor [ebp+arg_0], 0C2960388h
.text:08048585 xor [ebp+arg_0], 27071524h
.text:0804858C mov eax, [ebp+arg_0]
.text:0804858F pop ebp
.text:08048590 retn
.text:08048590 ; } // starts at 80484A9
.text:08048590 complex_function0 endp

其实可以看出,跟上一题的思路其实相差不大。所以本题,我们也需要控制两个参数。

故,思路如下:

开始地址设置在输入函数之后,传参数给complex之前。通过complex函数内部可以知道,esp寄存器的值给了ebp(两个complex函数都是这样)。然后我们设置两个参数并将其压入栈。之后的操作和上一题大同小异

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
39
40
41
42
import angr
import claripy
from Crypto.Util.number import long_to_bytes
import sys

pro =angr.Project("E:\\LAB\\angr\\angr\program\\04_angr_symbolic_stack")

start_addr = 0x08048697
init_state = pro.factory.blank_state(addr=start_addr)

init_state.regs.ebp = init_state.regs.esp

passwd0 = claripy.BVS("passwd0", 32)
passwd1 = claripy.BVS("passwd1", 32)


init_state.regs.esp -= 8
init_state.stack_push(passwd0)
init_state.stack_push(passwd1)


simulation = pro.factory.simgr(init_state)

def success(state):
output = state.posix.dumps(1)
return b"Good Job." in output

def abort(state):
output = state.posix.dumps(1)
return b"Try again." in output

simulation.explore(find=success, avoid=abort)

if simulation.found:
res = simulation.found[0]
solution0 = res.solver.eval(passwd0)
solution1 = res.solver.eval(passwd1)

solution = " ".join(map(str, [solution0, solution1]))
print("The Answer is :{}".format(solution))
else:
print("No Answer!")

05_angr_symbolic_memory

image-20220310171010963

还是先贴上伪代码

接着看汇编

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
main            proc near               ; DATA XREF: _start+17↑o
.text:080485A8
.text:080485A8 var_C = dword ptr -0Ch
.text:080485A8 var_4 = dword ptr -4
.text:080485A8 argc = dword ptr 8
.text:080485A8 argv = dword ptr 0Ch
.text:080485A8 envp = dword ptr 10h
.text:080485A8
.text:080485A8 ; __unwind {
.text:080485A8 lea ecx, [esp+4]
.text:080485AC and esp, 0FFFFFFF0h
.text:080485AF push dword ptr [ecx-4]
.text:080485B2 push ebp
.text:080485B3 mov ebp, esp
.text:080485B5 push ecx
.text:080485B6 sub esp, 14h
.text:080485B9 sub esp, 4
.text:080485BC push 21h ; '!' ; n
.text:080485BE push 0 ; c
.text:080485C0 push offset user_input ; s
.text:080485C5 call _memset
.text:080485CA add esp, 10h
.text:080485CD sub esp, 0Ch
.text:080485D0 push offset aEnterThePasswo ; "Enter the password: "
.text:080485D5 call _printf
.text:080485DA add esp, 10h
.text:080485DD sub esp, 0Ch
.text:080485E0 push offset unk_9FD92B8
.text:080485E5 push offset unk_9FD92B0
.text:080485EA push offset unk_9FD92A8
.text:080485EF push offset user_input
.text:080485F4 push offset a8s8s8s8s ; "%8s %8s %8s %8s"
.text:080485F9 call ___isoc99_scanf
.text:080485FE add esp, 20h
.text:08048601 mov [ebp+var_C], 0
.text:08048608 jmp short loc_8048637
.text:0804860A ; ---------------------------------------------------------------------------
.text:0804860A
.text:0804860A loc_804860A: ; CODE XREF: main+93↓j
.text:0804860A mov eax, [ebp+var_C]
.text:0804860D add eax, 9FD92A0h
.text:08048612 movzx eax, byte ptr [eax]
.text:08048615 movsx eax, al
.text:08048618 sub esp, 8
.text:0804861B push [ebp+var_C]
.text:0804861E push eax
.text:0804861F call complex_function
.text:08048624 add esp, 10h
.text:08048627 mov edx, eax
.text:08048629 mov eax, [ebp+var_C]
.text:0804862C add eax, 9FD92A0h
.text:08048631 mov [eax], dl
.text:08048633 add [ebp+var_C], 1
.text:08048637
.text:08048637 loc_8048637: ; CODE XREF: main+60↑j
.text:08048637 cmp [ebp+var_C], 1Fh
.text:0804863B jle short loc_804860A
.text:0804863D sub esp, 4
.text:08048640 push 20h ; ' ' ; n
.text:08048642 push offset s2 ; "THNJXTHBJUCDIMEEMLZNGMHISXAIXDQG"
.text:08048647 push offset user_input ; s1
.text:0804864C call _strncmp
.text:08048651 add esp, 10h
.text:08048654 test eax, eax
.text:08048656 jz short loc_804866A
.text:08048658 sub esp, 0Ch
.text:0804865B push offset s ; "Try again."
.text:08048660 call _puts
.text:08048665 add esp, 10h
.text:08048668 jmp short loc_804867A
.text:0804866A ; ---------------------------------------------------------------------------
.text:0804866A
.text:0804866A loc_804866A: ; CODE XREF: main+AE↑j
.text:0804866A sub esp, 0Ch
.text:0804866D push offset aGoodJob ; "Good Job."
.text:08048672 call _puts
.text:08048677 add esp, 10h
.text:0804867A
.text:0804867A loc_804867A: ; CODE XREF: main+C0↑j
.text:0804867A mov eax, 0
.text:0804867F mov ecx, [ebp+var_4]
.text:08048682 leave
.text:08048683 lea esp, [ecx-4]
.text:08048686 retn
.text:08048686 ; } // starts at 80485A8
.text:08048686 main endp

思路跟上一题差不多,函数开始地址还是设在相应的位置0x08048601

然后是初始化符号位向量,且注意四个数据的地址,然后是一如既往的设置成功的路径和避免的路径。

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
39
40
41
42
43
44
45
46
47
48
import angr
import claripy
from Crypto.Util.number import long_to_bytes
import sys

project =angr.Project("E:\\LAB\\angr\\angr\program\\05_angr_symbolic_memory")

start_address = 0x8048601
initial_state = project.factory.blank_state(addr=start_address)

password0 = claripy.BVS('password0', 8 * 8)
password1 = claripy.BVS('password1', 8 * 8)
password2 = claripy.BVS('password2', 8 * 8)
password3 = claripy.BVS('password3', 8 * 8)

password0_address = 0x9FD92A0
initial_state.memory.store(password0_address, password0)#初始化地址中的数据
password1_address = 0x9FD92A8
initial_state.memory.store(password1_address, password1)
password2_address = 0x9FD92B0
initial_state.memory.store(password2_address, password2)
password3_address = 0x9FD92B8
initial_state.memory.store(password3_address, password3)

simulation = project.factory.simgr(initial_state)

def is_successful(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return 'Good Job' in str(stdout_output)

def should_abort(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return 'Try again' in str(stdout_output)

simulation.explore(find=is_successful, avoid=should_abort)

if simulation.found:
solution_state = simulation.found[0]
solution0 = solution_state.se.eval(password0)
solution1 = solution_state.se.eval(password1)
solution2 = solution_state.se.eval(password2)
solution3 = solution_state.se.eval(password3)
solution = ' '.join(map('{:x}'.format, [ solution0, solution1,solution2,solution3 ]))

solution = " ".join(map(str, [solution0, solution1]))
print("The Answer is :{}".format(solution))
else:
print("No Answer!")

06_angr_symbolic_dynamic_memory

image-20220312101104842

伪代码如上。

看一下程序逻辑,输入两个数据,经过处理,比较,相等则通过。要注意buffer两个数据是存放在堆区的。、

所以在

main函数部分汇编代码如下

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
39
.text:08048691                 call    ___isoc99_scanf
.text:08048696 add esp, 10h
.text:08048699 mov [ebp+var_C], 0
.text:080486A0 jmp short loc_8048706
.text:080486A2 ; ---------------------------------------------------------------------------
.text:080486A2
.text:080486A2 loc_80486A2: ; CODE XREF: main+FE↓j
.text:080486A2 mov edx, ds:buffer0
.text:080486A8 mov eax, [ebp+var_C]
.text:080486AB lea ebx, [edx+eax]
.text:080486AE mov edx, ds:buffer0
.text:080486B4 mov eax, [ebp+var_C]
.text:080486B7 add eax, edx
.text:080486B9 movzx eax, byte ptr [eax]
.text:080486BC movsx eax, al
.text:080486BF sub esp, 8
.text:080486C2 push [ebp+var_C]
.text:080486C5 push eax
.text:080486C6 call complex_function
.text:080486CB add esp, 10h
.text:080486CE mov [ebx], al
.text:080486D0 mov edx, ds:buffer1
.text:080486D6 mov eax, [ebp+var_C]
.text:080486D9 lea ebx, [edx+eax]
.text:080486DC mov eax, [ebp+var_C]
.text:080486DF lea edx, [eax+20h]
.text:080486E2 mov ecx, ds:buffer1
.text:080486E8 mov eax, [ebp+var_C]
.text:080486EB add eax, ecx
.text:080486ED movzx eax, byte ptr [eax]
.text:080486F0 movsx eax, al
.text:080486F3 sub esp, 8
.text:080486F6 push edx
.text:080486F7 push eax
.text:080486F8 call complex_function
.text:080486FD add esp, 10h
.text:08048700 mov [ebx], al
.text:08048702 add [ebp+var_C], 1
.text:08048706

再来看看complex的代码

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
39
40
41
42
43
44
45
46
47
48
49
50
51
.text:080485A9 complex_function proc near              ; CODE XREF: main+BA↓p
.text:080485A9 ; main+EC↓p
.text:080485A9
.text:080485A9 arg_0 = dword ptr 8
.text:080485A9 arg_4 = dword ptr 0Ch
.text:080485A9
.text:080485A9 ; __unwind {
.text:080485A9 push ebp
.text:080485AA mov ebp, esp
.text:080485AC sub esp, 8
.text:080485AF cmp [ebp+arg_0], 40h ; '@'
.text:080485B3 jle short loc_80485BB
.text:080485B5 cmp [ebp+arg_0], 5Ah ; 'Z'
.text:080485B9 jle short loc_80485D5
.text:080485BB
.text:080485BB loc_80485BB: ; CODE XREF: complex_function+A↑j
.text:080485BB sub esp, 0Ch
.text:080485BE push offset s ; "Try again."
.text:080485C3 call _puts
.text:080485C8 add esp, 10h
.text:080485CB sub esp, 0Ch
.text:080485CE push 1 ; status
.text:080485D0 call _exit
.text:080485D5 ; ---------------------------------------------------------------------------
.text:080485D5
.text:080485D5 loc_80485D5: ; CODE XREF: complex_function+10↑j
.text:080485D5 mov eax, [ebp+arg_0]
.text:080485D8 lea ecx, [eax-41h]
.text:080485DB mov edx, [ebp+arg_4]
.text:080485DE mov eax, edx
.text:080485E0 add eax, eax
.text:080485E2 add eax, edx
.text:080485E4 shl eax, 2
.text:080485E7 add eax, edx
.text:080485E9 add ecx, eax
.text:080485EB mov edx, 4EC4EC4Fh
.text:080485F0 mov eax, ecx
.text:080485F2 imul edx
.text:080485F4 sar edx, 3
.text:080485F7 mov eax, ecx
.text:080485F9 sar eax, 1Fh
.text:080485FC sub edx, eax
.text:080485FE mov eax, edx
.text:08048600 imul eax, 1Ah
.text:08048603 sub ecx, eax
.text:08048605 mov eax, ecx
.text:08048607 add eax, 41h ; 'A'
.text:0804860A leave
.text:0804860B retn
.text:0804860B ; } // starts at 80485A9
.text:0804860B complex_function endp

本题跟上面一题脚本的主要区别在于,要设置数据保存的地址。而malloc是在堆区中分配的,而没有明确地址。故脚本中我们要指出地址来存放数据(随意指定,只要不在代码区等影响程序运行的地方就行)。以及需要指定数据在内存中是以小端序存储的(angr默认是大端序)。

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
39
40
41
42
43
44
45
46
import angr
import claripy
from Crypto.Util.number import long_to_bytes
import sys

project =angr.Project("E:\\LAB\\angr\\angr\program\\06_angr_symbolic_dynamic_memory")

start_addr = 0x08048699
initial_state = project.factory.blank_state(addr=start_addr)

password0 = claripy.BVS('password0', 64)
password1 = claripy.BVS('password1', 64)
fake0_addr = 0x4444440
fake1_addr = 0x4444450

buffer0_addr = 0x09FD92AC
buffer1_addr = 0x09FD92B4
initial_state.memory.store(buffer0_addr,fake0_addr,endness=project.arch.memory_endness)#endness=...就是指定存储端序
initial_state.memory.store(buffer1_addr,fake1_addr,endness=project.arch.memory_endness)

initial_state.memory.store(fake0_addr, password0)
initial_state.memory.store(fake1_addr, password1)

simulation = project.factory.simgr(initial_state)

def is_successful(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return b'Good Job.' in stdout_output

def should_abort(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return b'Try again.' in stdout_output

simulation.explore(find=is_successful, avoid=should_abort)

if simulation.found:
solution_state = simulation.found[0]

solution0 = solution_state.se.eval(password0)
solution1 = solution_state.se.eval(password1)
solution = ' '.join(map('{:x}'.format, [ solution0, solution1 ]))

print("The Answer is :{}".format(solution))
else:
print("No Answer!")

07_angr_symbolic_file

image-20220312112246653

伪代码整体逻辑为输入数据后,经过ignoerme处理,并保存在文件里MRXJ…txt,然后读取文件的数据,经过计算然后比较。

再看看ignoreme的伪代码

image-20220312113844308

main函数部分汇编代码

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
39
40
.text:080488B2                 push    offset buffer
.text:080488B7 push offset a64s ; "%64s"
.text:080488BC call ___isoc99_scanf
.text:080488C1 add esp, 10h
.text:080488C4 sub esp, 8
.text:080488C7 push 40h ; '@' ; n
.text:080488C9 push offset buffer ; int
.text:080488CE call ignore_me
.text:080488D3 add esp, 10h
.text:080488D6 sub esp, 4
.text:080488D9 push 40h ; '@' ; n
.text:080488DB push 0 ; c
.text:080488DD push offset buffer ; s
.text:080488E2 call _memset
.text:080488E7 add esp, 10h
.text:080488EA sub esp, 8
.text:080488ED push offset aRb ; "rb"
.text:080488F2 push offset name ; "MRXJKZYR.txt"
.text:080488F7 call _fopen
.text:080488FC add esp, 10h
.text:080488FF mov ds:fp, eax
.text:08048904 mov eax, ds:fp
.text:08048909 push eax ; stream
.text:0804890A push 40h ; '@' ; n
.text:0804890C push 1 ; size
.text:0804890E push offset buffer ; ptr
.text:08048913 call _fread
.text:08048918 add esp, 10h
.text:0804891B mov eax, ds:fp
.text:08048920 sub esp, 0Ch
.text:08048923 push eax ; stream
.text:08048924 call _fclose
.text:08048929 add esp, 10h
.text:0804892C sub esp, 0Ch
.text:0804892F push offset name ; "MRXJKZYR.txt"
.text:08048934 call _unlink
.text:08048939 add esp, 10h
.text:0804893C mov [ebp+var_C], 0
.text:08048943 jmp short loc_8048972
.text:08048945 ; ----------------------------------------------------------------------
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
import angr
import claripy
from Crypto.Util.number import long_to_bytes
import sys

project =angr.Project("E:\\LAB\\angr\\angr\program\\07_angr_symbolic_file")

start_addr = 0x080488EA#这个位置并没有符号化文件名

filename = 'MRXJKZYR.txt'
symbolic_file_size_bytes = 64

password = claripy.BVS('password', symbolic_file_size_bytes * 8)
password_file = angr.SimFile(filename, content=password, size=symbolic_file_size_bytes)
#模拟读取文件

initial_state = project.factory.blank_state(addr=start_addr,fs{filename:password_file}) #构建状态上下文里的文件数据系统
simulation = project.factory.simgr(initial_state)

def is_successful(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return b'Good Job.' in stdout_output

def should_abort(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return b'Try again.' in stdout_output

simulation.explore(find=is_successful, avoid=should_abort)

if simulation.found:
solution_state = simulation.found[0]

solution = long_to_bytes(solution_state.solver.eval(password))
print("The Answer is :{}".format(solution))

else:
print("No Answer!")

08_angr_constraints

image-20220312115905211

惯例伪代码

逻辑很简单输入数据经过变化后与passwd比较,不过是在函数中比较。

main部分汇编

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
.text:08048603                 push    offset aEnterThePasswo ; "Enter the password: "
.text:08048608 call _printf
.text:0804860D add esp, 10h
.text:08048610 sub esp, 8
.text:08048613 push offset buffer
.text:08048618 push offset a16s ; "%16s"
.text:0804861D call ___isoc99_scanf
.text:08048622 add esp, 10h
.text:08048625 mov [ebp+var_C], 0
.text:0804862C jmp short loc_8048663
.text:0804862E ; ---------------------------------------------------------------------------
.text:0804862E
.text:0804862E loc_804862E: ; CODE XREF: main+B4↓j
.text:0804862E mov eax, 0Fh
.text:08048633 sub eax, [ebp+var_C]
.text:08048636 mov edx, eax
.text:08048638 mov eax, [ebp+var_C]
.text:0804863B add eax, 804A050h
.text:08048640 movzx eax, byte ptr [eax]
.text:08048643 movsx eax, al
.text:08048646 sub esp, 8
.text:08048649 push edx
.text:0804864A push eax
.text:0804864B call complex_function
.text:08048650 add esp, 10h
.text:08048653 mov edx, eax
.text:08048655 mov eax, [ebp+var_C]
.text:08048658 add eax, 804A050h
.text:0804865D mov [eax], dl
.text:0804865F add [ebp+var_C], 1
.text:08048663
.text:08048663 loc_8048663: ; CODE XREF: main+79↑j
.text:08048663 cmp [ebp+var_C], 0Fh
.text:08048667 jle short loc_804862E
.text:08048669 sub esp, 8
.text:0804866C push 10h
.text:0804866E push offset buffer
.text:08048673 call check_equals_MRXJKZYRKMKENFZB
.text:08048678 add esp, 10h
.text:0804867B test eax, eax
.text:0804867D jnz short loc_8048691
.text:0804867F sub esp, 0Ch
.text:08048682 push offset s ; "Try again."
.text:08048687 call _puts
.text:0804868C add esp, 10h
.text:0804868F jmp short loc_80486A1
.text:08048691 ; ---------------------------------------------------------------------------
.text:08048691
.text:08048691 loc_8048691: ; CODE XREF: main+CA↑j
.text:08048691 sub esp, 0Ch
.text:08048694 push offset aGoodJob ; "Good Job."
.text:08048699 call _puts
.text:0804869E add esp, 10h

complex的代码跟刚开始两道差不多,就不放了

思路:

开始地址还是设在输入后调用complex前,0x8048625

然后构造输入,从后面的比较函数的参数来看,buffer是16(0x10)个字节,80比特大小。再者就是buffer的地址

然后是循环的计算,在8048669位置结束,从下一行开始,就是往检查函数传参了。

需要注意的是,在检查函数里,是一个一个字符进行比较,这样就会让程序每次执行if语句16次,然后计算该走的分支,如此一算就是2^16,这就会产生路径爆炸问题。故,通过添加约束条件来避免这个问题。

列出脚本

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
import angr
import claripy
from Crypto.Util.number import long_to_bytes
import sys

project =angr.Project("E:\\LAB\\angr\\angr\program\\08_angr_constraints")

start_addr = 0x08048625
initial_state = project.factory.blank_state(addr=start_addr)

buffer = claripy.BVS('buffer', 16*8)
buffer_addr = 0x0804A050
initial_state.memory.store(buffer_addr, buffer)

simulation = project.factory.simgr(initial_state)

addr_to_check_constraint = 0x08048669
simulation.explore(find=addr_to_check_constraint)

if simulation.found:
solution_state = simulation.found[0]

constrained_parameter_addr = 0x0804A050
constrained_parameter_size_bytes = 16
constrained_parameter_bitvector = solution_state.memory.load(constrained_parameter_addr, constrained_parameter_size_bytes) # 从内存中加载buffer

constrained_parameter_desired_value = 'MRXJKZYRKMKENFZB'

constrained_expression = constrained_parameter_bitvector == constrained_parameter_desired_value # 约束表达式

solution_state.add_constraints(constrained_expression) # 添加约束

solution = long_to_bytes(solution_state.se.eval(buffer))

print("The ANSWER is :{}".format(solution.decode("utf-8")))
else:
print("No Answer!")

09_angr_hooks

image-20220312155034471

程序逻辑大致为先后输入两个数据,各自经过一系列变化,然后比较。

可以看到本题依旧有同样的检查函数,那么同上题一样,相同原理也会产生路径爆炸问题,本题如其名,我们通过angr的hook来避免路径爆炸,所以,我们钩取检查函数,定义一个自己的检查函数来作为替换。

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
39
40
41
42
43
44
45
46
47
48
import angr
import claripy
from Crypto.Util.number import long_to_bytes
import sys

project =angr.Project("E:\\LAB\\angr\\angr\program\\09_angr_hooks")

initial_state = project.factory.entry_state()

check_equals_called_address = 0x80486B3 # 检查函数地址
instruction_to_skip_length = 0x5 # call check_equals_MRXJKZYRKMKENFZB这条指令的长度

@project.hook(check_equals_called_address, length=instruction_to_skip_length)#钩取
def skip_check_equals_(state):
user_input_buffer_address = 0x804A054 # 输入的地址
user_input_buffer_length = 0x10 # 输入的长度

user_input_string = state.memory.load( # 加载输入的数据
user_input_buffer_address,
user_input_buffer_length
)

check_against_string = 'MRXJKZYRKMKENFZB'

state.regs.eax = claripy.If( # 添加约束
user_input_string == check_against_string, # 条件
claripy.BVV(1, 32), # 真则返回
claripy.BVV(0, 32) # 假则0
)

simulation = project.factory.simgr(initial_state)

def is_successful(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return 'Good Job' in str(stdout_output)

def should_abort(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return 'Try again' in str(stdout_output)

simulation.explore(find=is_successful, avoid=should_abort)

if simulation.found:
solution_state = simulation.found[0]
solution = solution_state.posix.dumps(sys.stdin.fileno())
print("The ANSWER is :{}".format(solution.decode("utf-8")))
else:
print("No Answer!")

10_angr_simprocedures

image-20220312163054462

程序逻辑大同小异,不说了。

虽然说上一道和这一道没放汇编代码,但还是要看的,比如这道

image-20220312164107756

可以看到检查函数被调用了多次,那么我们无法通过准确的地址来进行钩取,故,我们需要通过函数名来钩取。

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
39
40
41
42
43
import angr
import claripy
from Crypto.Util.number import long_to_bytes
import sys

project =angr.Project("E:\\LAB\\angr\\angr\program\\10_angr_simprocedures")

initial_state = project.factory.entry_state()

class ReplacementCheckEquals(angr.SimProcedure):

def run(self, check_data_address, check_data_length): #参数后俩个是要hook函数的参数
check_input_string = self.state.memory.load(
check_data_address,
check_data_length
)

check_against_string = 'MRXJKZYRKMKENFZB'

return claripy.If(check_input_string == check_against_string, claripy.BVV(1, 32), claripy.BVV(0, 32))

check_equals_symbol = 'check_equals_MRXJKZYRKMKENFZB'
project.hook_symbol(check_equals_symbol, ReplacementCheckEquals()) #通过函数名钩取检查函数并替换为我们的检查函数

simulation = project.factory.simgr(initial_state)

def is_successful(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return 'Good Job' in str(stdout_output)

def should_abort(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return 'Try again' in str(stdout_output)

simulation.explore(find=is_successful, avoid=should_abort)

if simulation.found:
solution_state = simulation.found[0]

solution = solution_state.posix.dumps(sys.stdin.fileno())
print("The ANSWER is :{}".format(solution.decode("utf-8")))
else:
print("No Answer!")

本题脚本与上一题整体差别不大。主要在函数钩取方式上。

11_angr_sim_scanf

image-20220312164721082

程序逻辑很明了,将原有数据变化,然后输入两个数据(每个四字节)与变化的数据作比较。

查看汇编代码发现,有多次调用输入函数,并比较的代码。这就是本题的关键点。

其实跟上一题差不多,这次很多调用的是输入,不是检查,相同的方法,钩取输入函数。不过参数设置上需要注意,数据大小,以及将数据以小端形式保存到地址中。并且需要将两个输入数据设置为全局变量。(需要重新设置两个变量来存储,因为我们在函数中设置的只是自定义的输入函数的局部变量,要想传到main中,得设置全局变量)

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
39
40
41
42
43
44
45
46
47
import angr
import claripy
from Crypto.Util.number import long_to_bytes
import sys

project =angr.Project("E:\\LAB\\angr\\angr\program\\11_angr_sim_scanf")

initial_state = project.factory.entry_state()

class ReplacementScanf(angr.SimProcedure):

def run(self, format_string, scanf0_address, scanf1_address ):
scanf0 = claripy.BVS('scanf0', 4 * 8)#4字节,32比特
scanf1 = claripy.BVS('scanf1', 4 * 8)

self.state.memory.store(scanf0_address, scanf0, endness=project.arch.memory_endness)#保存到内存中
self.state.memory.store(scanf1_address, scanf1, endness=project.arch.memory_endness)

self.state.globals['solution0'] = scanf0#全局变量的设置
self.state.globals['solution1'] = scanf1

scanf_symbol = '__isoc99_scanf'
project.hook_symbol(scanf_symbol, ReplacementScanf())

simulation = project.factory.simgr(initial_state)

def is_successful(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return 'Good Job' in str(stdout_output)

def should_abort(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return 'Try again' in str(stdout_output)

simulation.explore(find=is_successful, avoid=should_abort)

if simulation.found:
solution_state = simulation.found[0]
stored_solutions0 = solution_state.globals['solution0']
stored_solutions1 = solution_state.globals['solution1']
solution0 = solution_state.se.eval(stored_solutions0)
solution1 = solution_state.se.eval(stored_solutions1)
print("The ANSWER is :{}".format(solution0))
print("The ANSWER is :{}".format(solution1))
else:
print("No Answer!")

12_angr_veritesting

image-20220312170347821

输入数据,经过祖传complex函数变化,然后作比较。

其实这个for循环和if语句加起来相当于一个检查函数,而且会执行2^32次,路径爆炸是必须的。

这题除了避免路径爆炸外,其余的跟刚开始几道题差不多。

放脚本

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
import angr
import claripy
from Crypto.Util.number import long_to_bytes
import sys

project =angr.Project("E:\\LAB\\angr\\angr\program\\12_angr_veritesting")

initial_state = project.factory.entry_state()

simulation = project.factory.simgr(initial_state,veritesting = True)

def is_successful(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return 'Good Job.' in str(stdout_output) # :boolean

def should_abort(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return 'Try again.' in str(stdout_output) # :boolean

simulation.explore(find = is_successful,avoid = should_abort)

if simulation.found :
solution_state = simulation.found[0]
print(solution_state.posix.dumps(sys.stdin.fileno()))
else:
print("No Answer!")

本题主要是利用了project.factory.simgr() 函数提供的veritesting 参数来指定是否自动合并路径。

贴上大佬的简单原理解释

image-20220312172250586

13_angr_static_binary

image-20220312172829883

伪代码如上。也是祖传complex函数变化。

乍一看,跟第一题还很像。但是用第一题的脚本跑半天出不来。而且还报错。

点进printf,scanf这类函数可以发现,呈现的是函数的源代码,不再是之前的跳转命令。

这个程序是静态链接编译的,故才会包含printf,scanf这类libc函数的实现。

因此我们需要hook掉这些实现的libc函数,避免angr在这些libc函数里东跑西跑迷失。

本题的libc函数有printf,scanf,puts,还有一个glibc函数__libc_start_main

hook掉之后,脚本就跟第一题的一样了

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
import angr
import claripy
from Crypto.Util.number import long_to_bytes
import sys

project =angr.Project("E:\\LAB\\angr\\angr\program\\13_angr_static_binary")

initial_state = project.factory.entry_state()

simulation = project.factory.simgr(initial_state,veritesting = True)

project.hook(0x804ed40, angr.SIM_PROCEDURES['libc']['printf']()) # 获取Angr 内部实现的系统函数
project.hook(0x804ed80, angr.SIM_PROCEDURES['libc']['scanf']())
project.hook(0x804f350, angr.SIM_PROCEDURES['libc']['puts']())
project.hook(0x8048d10, angr.SIM_PROCEDURES['glibc']['__libc_start_main']())

def is_successful(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return 'Good Job.' in str(stdout_output)

def should_abort(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return 'Try again.' in str(stdout_output)

simulation.explore(find = is_successful,avoid = should_abort)

if simulation.found :
solution_state = simulation.found[0]
print(solution_state.posix.dumps(sys.stdin.fileno()))
else:
print("No Answer!")

14_angr_shared_library

image-20220312180011638

…….

这题还有一个对应的.so文件,这就需要用angr来动态链接了。

反编译的时候会报错,寄。

image-20220312180837996

原因摘自其他大神博客

这是因为generate.py 里面有一个Bug ,在最后的一个gcc 编译命令因为-L 参数缺少了指定当前目录,导致在寻找lib14_angr_shared_library.so 的时候找到了系统库目录,所以gcc 抛出了这个找不到14_angr_shared_library: No such file or directory 的问题,代码修改如下:

1
2
3
4
5
  with tempfile.NamedTemporaryFile(delete=False, suffix='.c') as temp:
temp.write(c_code)
temp.seek(0)
- os.system('gcc -m32 -I . -L ' + '/'.join(output_file.split('/')[0:-1]) + ' -o ' + output_file + ' ' + temp.name + ' -l' + output_file.split('/')[-1])
+ os.system('gcc -m32 -I . -L . ' + '/'.join(output_file.split('/')[0:-1]) + ' -o ' + output_file + ' ' + temp.name + ' -l' + output_file.split('/')[-1])

回到函数本身,这个validate是验证函数哈,但是不在v这个程序里,在.so文件动态链接着

所以我们用IDA打开.so文件,查看validate代码。

image-20220312181710748

…祖传complex,没什么好说的。

这题考点在于这个动态链接。我们对这个.so文件进行符号执行。(加载文件时加载.so文件)

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
from msilib.schema import Binary
import angr
import claripy
from Crypto.Util.number import long_to_bytes
import sys



base = 0x400000 # base 基址是随意定的,可以随意修改
project = angr.Project("E:\\LAB\\angr\\angr\program\\lib14_angr_shared_library.so", load_options={
'main_opts' : {
'custom_base_addr' : base
}
})

buf_pointer = claripy.BVV(0x03000000, 32) # 变量地址
validate_addr = base+0x6d7 # 函数地址
# validate(char* buffer, int length)
init_state = project.factory.call_state(validate_addr, buf_pointer, claripy.BVV(8, 32))

flag = claripy.BVS("flag", 8*8) # 要求解的输入
init_state.memory.store(buf_pointer, flag)

good = base+0x783 # validate返回地址
simu = project.factory.simgr(init_state)
simu.explore(find=good)

if simu.found:
solu_state = simu.found[0]
# 限制返回时的寄存器值不为0才是正确结果
solu_state.add_constraints(solu_state.regs.eax != 0)
flag = solu_state.solver.eval(flag, cast_to=bytes)
print(flag)
else:
print("No result!")

15_angr_arbitrary_read

image-20220312183629337

接下来重点看汇编代码

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
.text:080484C9 main            proc near               ; DATA XREF: _start+17↑o
.text:080484C9
.text:080484C9 var_1C = byte ptr -1Ch
.text:080484C9 s = dword ptr -0Ch
.text:080484C9 var_4 = dword ptr -4
.text:080484C9 argc = dword ptr 8
.text:080484C9 argv = dword ptr 0Ch
.text:080484C9 envp = dword ptr 10h
.text:080484C9
.text:080484C9 ; __unwind {
.text:080484C9 lea ecx, [esp+4]
.text:080484CD and esp, 0FFFFFFF0h
.text:080484D0 push dword ptr [ecx-4]
.text:080484D3 push ebp
.text:080484D4 mov ebp, esp
.text:080484D6 push ecx
.text:080484D7 sub esp, 24h
.text:080484DA mov eax, try_again
.text:080484DF mov [ebp+s], eax
.text:080484E2 sub esp, 0Ch
.text:080484E5 push offset aEnterThePasswo ; "Enter the password: "
.text:080484EA call _printf
.text:080484EF add esp, 10h
.text:080484F2 sub esp, 4
.text:080484F5 lea eax, [ebp+var_1C]
.text:080484F8 push eax
.text:080484F9 push offset key
.text:080484FE push offset aU20s ; "%u %20s"
.text:08048503 call ___isoc99_scanf
.text:08048508 add esp, 10h
.text:0804850B mov eax, ds:key
.text:08048510 cmp eax, 228BF7Eh
.text:08048515 jz short loc_8048531
.text:08048517 cmp eax, 3AD516Ah
.text:0804851C jnz short loc_8048542
.text:0804851E mov eax, try_again
.text:08048523 sub esp, 0Ch
.text:08048526 push eax ; s
.text:08048527 call _puts
.text:0804852C add esp, 10h
.text:0804852F jmp short loc_8048553
.text:08048531 ; ---------------------------------------------------------------------------
.text:08048531
.text:08048531 loc_8048531: ; CODE XREF: main+4C↑j
.text:08048531 mov eax, [ebp+s]
.text:08048534 sub esp, 0Ch
.text:08048537 push eax ; s
.text:08048538 call _puts
.text:0804853D add esp, 10h
.text:08048540 jmp short loc_8048553
.text:08048542 ; ---------------------------------------------------------------------------
.text:08048542
.text:08048542 loc_8048542: ; CODE XREF: main+53↑j
.text:08048542 mov eax, try_again
.text:08048547 sub esp, 0Ch
.text:0804854A push eax ; s
.text:0804854B call _puts
.text:08048550 add esp, 10h
.text:08048553
.text:08048553 loc_8048553: ; CODE XREF: main+66↑j
.text:08048553 ; main+77↑j
.text:08048553 nop
.text:08048554 mov eax, 0
.text:08048559 mov ecx, [ebp+var_4]
.text:0804855C leave
.text:0804855D lea esp, [ecx-4]
.text:08048560 retn
.text:08048560 ; } // starts at 80484C9
.text:08048560 main endp

输入的两个参数,key,v4,分别对应汇编代码中的s(var_c),var_1c,由此可见,两个参数相距0x10(16个字节大小,其实也就是说,var_1c(v4)是16个字节大小。)而输入会输入20个字节。必定会将key变量覆盖部分甚至全部。(其实%u就是占四个字节,这里key就刚好全部被覆盖了)

再会看程序逻辑,将key与0x228BF7E比较,相等则不输出tryagain,至此我们找到了输出s的方法

我们使用hook,钩取scanf

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
from msilib.schema import Binary
import angr
import claripy
from Crypto.Util.number import long_to_bytes
import sys



binary_path = "E:\\LAB\\angr\\angr\program\\15_angr_arbitrary_read"
pro = angr.Project(binary_path)

init_state = pro.factory.entry_state()

# replace scanf
class ReplaceScanf(angr.SimProcedure):

def run(self, format_str, param0, param1):
scanf0 = claripy.BVS("0", 32)
scanf1 = claripy.BVS("1", 20*8)
# 限制单个字符的求解范围
for ch in scanf1.chop(bits=8):
self.state.add_constraints(ch >= "A", ch <= "Z")

self.state.memory.store(param0, scanf0, endness=pro.arch.memory_endness)
self.state.memory.store(param1, scanf1)
self.state.globals["solutions"] = (scanf0, scanf1)

scanf_sym = "__isoc99_scanf"
pro.hook_symbol(scanf_sym, ReplaceScanf())

# 通过检查puts的输出来确定是否是想要的结果
def check_puts(state):
puts_para = state.memory.load(state.regs.esp+4, 4, endness=pro.arch.memory_endness)
if state.solver.symbolic(puts_para):
good_str = 0x4D52584B
copy_state = state.copy()
copy_state.add_constraints(puts_para == good_str)
if copy_state.satisfiable():
# 先通过拷贝的状态判断是否满足,然后再直接在原状态增加限制
state.add_constraints(puts_para == good_str) # 如果有解的话就保存到我们执行的那个状态对象
return True
else:
return False
else:
return False

def success(state):
puts_plt = 0x08048370 # 当程序执行到puts() 函数时,我们就认为路径探索到了这里,然后再去通过check_puts() 判断这里是否存在漏洞,告诉Angr这是不是我们需要找的那条执行路径
if state.addr == puts_plt:
return check_puts(state)
else:
return False

simu = pro.factory.simgr(init_state)
simu.explore(find=success)

if simu.found:
solu_state = simu.found[0]
(scanf0, scanf1) = solu_state.globals["solutions"]
flag = str(solu_state.solver.eval(scanf0)).encode("utf-8")
flag += b" " + solu_state.solver.eval(scanf1, cast_to=bytes)
print(flag)
else:
print("No result!")

16_angr_arbitrary_write

image-20220312190749217

汇编

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
.text:08048569 ; int __cdecl main(int argc, const char **argv, const char **envp)
.text:08048569 public main
.text:08048569 main proc near ; DATA XREF: _start+17↑o
.text:08048569
.text:08048569 input_buffer(s) = byte ptr -1Ch
.text:08048569 target_buffer(dest) = dword ptr -0Ch
.text:08048569 var_4 = dword ptr -4
.text:08048569 argc = dword ptr 8
.text:08048569 argv = dword ptr 0Ch
.text:08048569 envp = dword ptr 10h
.text:08048569
.text:08048569 ; __unwind {
.text:08048569 lea ecx, [esp+4]
.text:0804856D and esp, 0FFFFFFF0h
.text:08048570 push dword ptr [ecx-4]
.text:08048573 push ebp
.text:08048574 mov ebp, esp
.text:08048576 push ecx
.text:08048577 sub esp, 24h
.text:0804857A mov [ebp+target_buffer], offset unimportant_buffer
.text:08048581 sub esp, 4
.text:08048584 push 10h ; n
.text:08048586 push 0 ; c
.text:08048588 lea eax, [ebp+input_buffer]
.text:0804858B push eax ; s
.text:0804858C call _memset ; 清空input_buffer 的内容
.text:08048591 add esp, 10h
.text:08048594 sub esp, 4
.text:08048597 push 0Ch ; n
.text:08048599 push offset src ; "PASSWORD"
.text:0804859E push offset password_buffer ; dest
.text:080485A3 call _strncpy ; 复制PASSWORD 到全局内存password_buffer
.text:080485A8 add esp, 10h
.text:080485AB sub esp, 0Ch
.text:080485AE push offset aEnterThePasswo ; "Enter the password: "
.text:080485B3 call _printf
.text:080485B8 add esp, 10h
.text:080485BB sub esp, 4
.text:080485BE lea eax, [ebp+input_buffer]
.text:080485C1 push eax
.text:080485C2 push offset check_key
.text:080485C7 push offset aU20s ; "%u %20s"
.text:080485CC call ___isoc99_scanf ; scanf("%u %20s",check_key,input_buffer) .注意input_buffer 的大小是20 字节,栈上的input_buffer 默认的大小是16 字节,最后4 字节可以覆盖target_buffer .
.text:080485D1 add esp, 10h
.text:080485D4 mov eax, ds:check_key
.text:080485D9 cmp eax, 1A25D71h
.text:080485DE jz short loc_80485E9
.text:080485E0 cmp eax, 1CB7D43h
.text:080485E5 jz short loc_8048601 ; 根据check_key 的输入来跳转到不同的_strncpy
.text:080485E7 jmp short loc_8048618
.text:080485E9 ; ---------------------------------------------------------------------------
.text:080485E9
.text:080485E9 loc_80485E9: ; CODE XREF: main+75↑j
.text:080485E9 sub esp, 4
.text:080485EC push 10h ; n
.text:080485EE lea eax, [ebp+input_buffer]
.text:080485F1 push eax ; src
.text:080485F2 push offset unimportant_buffer ; dest
.text:080485F7 call _strncpy
.text:080485FC add esp, 10h
.text:080485FF jmp short loc_804862E
.text:08048601 ; ---------------------------------------------------------------------------
.text:08048601
.text:08048601 loc_8048601: ; CODE XREF: main+7C↑j
.text:08048601 mov eax, [ebp+target_buffer] ; 注意这个是MOV 指令,意思是获取EBP + target_buffer 这个地址的内容保存到EAX 中
.text:08048604 sub esp, 4
.text:08048607 push 10h ; n
.text:08048609 lea edx, [ebp+input_buffer] ; 注意这个是LEA 指令,意思是计算出EBP + input_buffer 的地址保存到EBX 中
.text:0804860C push edx ; src
.text:0804860D push eax ; dest
.text:0804860E call _strncpy ; 漏洞点在这里,strncpy(*target_buffer,input_buffer) ,也就是说input_buffer 最后四字节可以控制对任意地址的_strncpy() .总结起来就是strncpy(input_buffer[ -4 : ],input_buffer,0x10) .
.text:08048613 add esp, 10h
.text:08048616 jmp short loc_804862E
.text:08048618 ; ---------------------------------------------------------------------------
.text:08048618
.text:08048618 loc_8048618: ; CODE XREF: main+7E↑j
.text:08048618 sub esp, 4
.text:0804861B push 10h ; n
.text:0804861D lea eax, [ebp+input_buffer]
.text:08048620 push eax ; src
.text:08048621 push offset unimportant_buffer ; dest
.text:08048626 call _strncpy
.text:0804862B add esp, 10h
.text:0804862E
.text:0804862E loc_804862E: ; CODE XREF: main+96↑j
.text:0804862E ; main+AD↑j
.text:0804862E nop
.text:0804862F sub esp, 4
.text:08048632 push 8 ; n
.text:08048634 push offset key_string ; "KZYRKMKE"
.text:08048639 push offset password_buffer ; s1
.text:0804863E call _strncmp ; 我们知道了上面有一个任意地址写之后,我们就需要改写key_string 或者password_buffer 一致,让_strncmp() 返回0 ,跳转到puts("Good Job")
.text:08048643 add esp, 10h
.text:08048646 test eax, eax
.text:08048648 jz short loc_804865C
.text:0804864A sub esp, 0Ch
.text:0804864D push offset s ; "Try again."
.text:08048652 call _puts
.text:08048657 add esp, 10h
.text:0804865A jmp short loc_804866C
.text:0804865C ; ---------------------------------------------------------------------------
.text:0804865C
.text:0804865C loc_804865C: ; CODE XREF: main+DF↑j
.text:0804865C sub esp, 0Ch
.text:0804865F push offset aGoodJob ; "Good Job."
.text:08048664 call _puts
.text:08048669 add esp, 10h

这题,s和key的关系跟上题v4和key的关系一样。

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
from msilib.schema import Binary
import angr
import claripy
from Crypto.Util.number import long_to_bytes
import sys



binary_path = "E:\\LAB\\angr\\angr\program\\16_angr_arbitrary_write"
project= angr.Project(binary_path)

initial_state = project.factory.entry_state()

class ReplacementScanf(angr.SimProcedure):

def run(self, format_string, check_key ,input_buffer):
scanf0 = claripy.BVS('scanf0', 4 * 8)
scanf1 = claripy.BVS('scanf1', 20 * 8)

for char in scanf1.chop(bits=8):
self.state.add_constraints(char >= '0', char <= 'z')

self.state.memory.store(check_key, scanf0, endness=project.arch.memory_endness)
self.state.memory.store(input_buffer, scanf1, endness=project.arch.memory_endness)

self.state.globals['solution0'] = scanf0
self.state.globals['solution1'] = scanf1

scanf_symbol = '__isoc99_scanf'
project.hook_symbol(scanf_symbol, ReplacementScanf())

def check_strncpy(state):
strncpy_dest = state.memory.load(state.regs.esp + 4, 4, endness=project.arch.memory_endness) # 获取strncpy() 的参数,strncpy_dest ..
strncpy_src = state.memory.load(state.regs.esp + 8, 4, endness=project.arch.memory_endness)
strncpy_len = state.memory.load(state.regs.esp + 12, 4, endness=project.arch.memory_endness)
src_contents = state.memory.load(strncpy_src, strncpy_len) # 因为参数中只保存了地址,我们需要根据这个地址去获取内容

if state.se.symbolic(strncpy_dest) and state.se.symbolic(src_contents) : # 判断dest 和src 的内容是不是符号化对象
if state.satisfiable(extra_constraints=(src_contents[ -1 : -64 ] == 'KZYRKMKE' ,strncpy_dest == 0x4D52584C)): # 尝试求解,其中strncpy_dest == 0x4D52584C 的意思是判断dest 是否可控为password 的地址;src_contents[ -1 : -64 ] == 'KZYRKMKE' 是判断input_buffer 的内容是否可控为'KZYRKMKE' ,因为这块内存是倒序,所以需要通过[ -1 : -64 ] 倒转(contentes 的内容是比特,获取8 字节的大小为:8*8 = 64),然后判断该值是否为字符串'KZYRKMKE'
state.add_constraints(src_contents[ -1 : -64 ] == 'KZYRKMKE',strncpy_dest == 0x4D52584C)
return True
else:
return False
else:
return False

simulation = project.factory.simgr(initial_state)

def is_successful(state):
strncpy_address = 0x8048410

if state.addr == strncpy_address:
return check_strncpy(state)
else:
return False

simulation.explore(find=is_successful)

if simulation.found:
solution_state = simulation.found[0]
solution0 = solution_state.se.eval(solution_state.globals['solution0'])
solution1 = solution_state.se.eval(solution_state.globals['solution1'],cast_to=bytes)

print(solution0,solution1)
else:
print("No result!")

17_angr_arbitrary_jump

image-20220312192050484

image-20220312192112152

这么简单肯定出事儿,没有Goodjob。

汇编

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
.text:4D525886 ; int __cdecl main(int argc, const char **argv, const char **envp)
.text:4D525886 public main
.text:4D525886 main proc near ; DATA XREF: _start+17↑o
.text:4D525886
.text:4D525886 var_C = dword ptr -0Ch
.text:4D525886 var_4 = dword ptr -4
.text:4D525886 argc = dword ptr 8
.text:4D525886 argv = dword ptr 0Ch
.text:4D525886 envp = dword ptr 10h
.text:4D525886
.text:4D525886 ; __unwind {
.text:4D525886 lea ecx, [esp+4]
.text:4D52588A and esp, 0FFFFFFF0h
.text:4D52588D push dword ptr [ecx-4]
.text:4D525890 push ebp
.text:4D525891 mov ebp, esp
.text:4D525893 push ecx
.text:4D525894 sub esp, 14h
.text:4D525897 mov [ebp+var_C], 0
.text:4D52589E sub esp, 0Ch
.text:4D5258A1 push offset aEnterThePasswo ; "Enter the password: "
.text:4D5258A6 call _printf
.text:4D5258AB add esp, 10h
.text:4D5258AE call read_input ;
.text:4D5258B3 sub esp, 0Ch
.text:4D5258B6 push offset aTryAgain ; "Try again."
.text:4D5258BB call _puts
.text:4D5258C0 add esp, 10h
.text:4D5258C3 mov eax, 0
.text:4D5258C8 mov ecx, [ebp+var_4]
.text:4D5258CB leave
.text:4D5258CC lea esp, [ecx-4]
.text:4D5258CF retn

good job

1
2
3
4
5
6
7
8
9
10
11
12
13
14
.text:4D525849 print_good      proc near
.text:4D525849 ; __unwind {
.text:4D525849 push ebp
.text:4D52584A mov ebp, esp
.text:4D52584C sub esp, 8
.text:4D52584F sub esp, 0Ch
.text:4D525852 push offset s ; "Good Job."
.text:4D525857 call _puts
.text:4D52585C add esp, 10h
.text:4D52585F sub esp, 0Ch
.text:4D525862 push 0 ; status
.text:4D525864 call _exit
.text:4D525864 ; } // starts at 4D525849
.text:4D525864 print_good endp

reaad_input

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
read_input      proc near               ; CODE XREF: main+28↓p
.text:4D525869
.text:4D525869 var_2B = byte ptr -2Bh
.text:4D525869
.text:4D525869 ; __unwind {
.text:4D525869 push ebp
.text:4D52586A mov ebp, esp
.text:4D52586C sub esp, 38h
.text:4D52586F sub esp, 8
.text:4D525872 lea eax, [ebp+var_2B]
.text:4D525875 push eax
.text:4D525876 push offset format ; "%s"
.text:4D52587B call ___isoc99_scanf
.text:4D525880 add esp, 10h
.text:4D525883 nop
.text:4D525884 leave
.text:4D525885 retn
.text:4D525885 ; } // starts at 4D525869
.text:4D525885 read_input endp

这道题和上面两道题其实是有相似之处的,输入的长度过长会造成溢出,会覆盖某些东西。

这个函数是当输入足够长时造成的栈溢出会覆盖retn的命令。

其实思路也已经出来了

我们hook输入函数,将输入的参数字节扩大,让它不覆盖retn

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
from msilib.schema import Binary
import angr
import claripy
from Crypto.Util.number import long_to_bytes
import sys



binary_path = "E:\\LAB\\angr\\angr\program\\17_angr_arbitrary_jump"
project= angr.Project(binary_path)

initial_state = project.factory.entry_state()

simulation = project.factory.simgr(
initial_state,
save_unconstrained=True,
stashes={
'active' : [initial_state],
'unconstrained' : [],
'found' : [],
'not_needed' : []
}
)

class ReplacementScanf(angr.SimProcedure):

def run(self, format_string, input_buffer_address):
input_buffer = claripy.BVS('input_buffer', 64 * 8) # 设置一个较大的input_buffer

for char in input_buffer.chop(bits=8):
self.state.add_constraints(char >= '0', char <= 'z')

self.state.memory.store(input_buffer_address, input_buffer, endness=project.arch.memory_endness)

self.state.globals['solution'] = input_buffer

scanf_symbol = '__isoc99_scanf'
project.hook_symbol(scanf_symbol, ReplacementScanf()) # 对scanf() 做Hook

while (simulation.active or simulation.unconstrained) and (not simulation.found): #
for unconstrained_state in simulation.unconstrained:
def should_move(s):
return s is unconstrained_state

simulation.move('unconstrained', 'found', filter_func=should_move) # 保存

simulation.step() # 步进执行

if simulation.found:
solution_state = simulation.found[0]

solution_state.add_constraints(solution_state.regs.eip == 0x4D525849) # 判断EIP 地址是否可控

solution = solution_state.se.eval(solution_state.globals['solution'],cast_to = bytes) # 生成Payload
print(solution)

总结

丫的,终于写完了

感觉这个实验写完,能入个门,更多的还是得多多琢磨官方文档。

Boomlab

(感觉这个实验也可以直接丢进IDA当逆向做,相对简单;本次以汇编语言下手;所用工具为VS2022社区版)

先在Linux里用如下命令把文件反汇编出来。

1
objdump -d bomb > bomb.asm

查找到main函数方式多样,此处直接搜索main即可找到位置。

image-20220228164722822

大致浏览一遍main函数,有几个phase,就是需要拆除的炸弹了。

本文着重描述过程,运算只涉及最后的结果。

本文着重描述过程,运算只涉及最后的结果。

本文着重描述过程,运算只涉及最后的结果。(水平有限)

Phase_1

根据函数对应的地址跳转

image-20220228165317069

可以看到该函数中,先将一段地址赋给esi,然后调用strings_not_equal;然后测试eax值,为0就跳过了引爆炸弹的一步。由此可见关键在于strings_not_equal这个函数。

image-20220228170012668

可以看到还是比较长的,不过容易读懂。首先是传参,将rbx,rbp作为string_length的参数,然后调用。可以根据最开始的两个mov猜测,rdi,rsi就是string_not_equal的两个参数。

再看本函数结尾,会有一个给eax赋值的操作,然后返回。这时候就需要知道edx被赋了什么值。

浏览代码可以看出大致逻辑(也可以根据函数名直接猜)两参数的字符串相等则返回0,不相等则返回1

查看esi里的地址所对应的字符串如下:(用gdb调起来才能看到,不熟悉gdb也可以拖进IDA,看汇编还是伪代码就自己选择了)

image-20220228174801745

“Border relations with Canada have never been better.”

Phase_2

首先是代码

image-20220228171547222

可以看到调用read_six_numbers函数

image-20220228172152067

代码大致逻辑为读取6个数,没有六个就引爆炸弹。

接着看phase_2代码,将rsp的值与1作比较,不相等就爆;然后将rsp+0x4和rsp+0x18的值给了rbx,rbp然后是把rbx-0x4地址处的内容给了eax,然后eax+eax,再做比较,相等就不爆;再继续,将rbx值加4字节(一个int的大小),到了下一个数,再进行比较。依照逻辑,输入的六个数分别为:1 2 4 8 16 32.即可拆弹

Phase_3

代码

image-20220228173849867

先查看一下0x4025cf处是啥,结合下面的输入函数,这是两个输入的参数。这里可以假设一下rcx和rdx就是这两个参数。

image-20220228181405909

将0给eax,然后调用参数后将eax的值与1作比较,大于就不爆。然后将rsp+0x8的值,也就是rdx的值与7作比较,大于就跳转,小于就爆,回到上一步向下看,会将rdx的值给eax,然后跳到402470这个地址

image-20220228182641784

不知道这是个啥

image-20220228182734910

用IDA发现是到了switch语句。(其实是汇编语言的switch实现)

看后面的代码,都会跳到400fbe的位置,将eax的值与rsp+0xc的值作比较,相等则不爆。

这题答案不固定,根据第一个数不同,第二个数也会不同随机选取一组符合条件的即可。

Phase_4

代码

image-20220228183854762

前面几行跟上一个阶段一样,就是输入两个值作为参数。

再将eax的值与2作比较,不相等就爆。然后将0xe和rsp+0x8的值作比较,大于0xe就不跳,故此处需小于。

然后是三个赋值操作,再接着调用func4函数,然后验证eax的值,不为0就爆,接着rsp+0xc的值等于0,炸弹拆除。

func4:

image-20220228200230163

由上面的分析可知,此处要让eax的值为0。这玩意儿还挺复杂,自己调用自己,递归了。

大致逻辑:上面phase_4的edx,esi,edi作为func4的三个参数。将edx的值给eax,用eax的值减esi,然后保存在ecx,再将ecx的值逻辑右移31位(0x1f),再将eax的值与ecx的值相加,保存在eax,然后算数右移1位(需要注意与逻辑右移的区别。)然后是ecx=rax+rsi*1,然后是ecx和edi比较,ecx较大就跳。400ff2处,edi的值小于ecx则跳转并结束该函数调用,否则将rcx+0x1的值给esi。从400fe6继续看,将rcx-0x1的值给edx,然后调用func4,然后eax+eax,然后结束调用。

上代码,帮助理解:

1
2
3
4
5
6
7
8
9
10
11
12
13
int fun(int a1, int a2, int a3){
int b = (a1 - a2) >> 31;
int result = ((a1-a2) + b) >> 1;
b = result + a2;
if(b == x) return 0;
if(b < x) {
result = fun(a1, b + 1, x);
return result * 2 + 1;
}else{
result = fun(b - 1, a2, x);
return result * 2;
}
}

此处a1,a2,a3分别为0xe,0x0,rsp+0x8(输入的第一个值)

最后,什么时候返回0。我们将值代入发现,7是能返回0的一个界限,那么7,0可以作为一种答案。

看大佬们的解法才恍然大悟。

1
2
3
4
5
6
7
8
9
int main(void){
for(int i = 0; i <= 0xe; i++){
if(fun(0xe,0,i) == 0){
printf("%d\n",i) ;
return 0;
}
}
return 0;
}

允许的答案由0 1 3 7,只需要简单调用一下函数就行,没想到。

Phase_5

image-20220228210929709

这1-7行刚开始是没看懂,后来看伪代码是v4 = __readfsqword(0x28u),这玩意儿。

eax存储了字符串长度,必须为6,否则就炸。然后再把0给eax,将输入保存在ecx,因为rax刚开始为0.

将cl的值(ecx的一个子寄存器)给rdx,然后获取edx的低四位。然后所得到的edx的值作为指针访问0x4024b0地址下的某一位(有点绕),一直到4010ac处的指令,可用代码表示为:

1
2
for ( i = 0LL; i != 6; ++i )
arry[i] = array_0x4024b0[*(a1 + i) & 0xF];//a1是eax的值,即字符串长度,6

0x4024b0处

image-20220228220413822

将rsp+0x16的值化为0,也就是代表该字符串的末尾;0x40245e处:

image-20220228215537273

然后传入两个参数esi,rdi到strings_not_equal函数不相等就炸。

所以根据后续代码逻辑,我们需要在0x4024b0处的字符串中找到0x40245e处的下标。

分别是9 15 14 5 6 7,但是,这些都是答案的低四位

因为有取低四位这个步骤,这里给出C语言代码

1
array[arry_4024b0[i]&0xf] = arry_0x40245e[i];

借用一张图片,可以对着查询,任意选一组即可。

63d9f2d3572c11dff05943a1652762d0f603c2ac

Phase_6

代码有点长,我们分部来看。

首先是将rsi作为参数传入read_six_numbers函数,然后又赋值,将eax的值减1后,与5作比较,小于等于5则不爆。然后将r12d的值加一(刚开始为0),与6比较,等于才跳,接着又有赋值操作,然后将eax与rbp+0作比较,相等就炸。

image-20220301113532571

接着来到401145处,ebx加一,再与5作比较,大于5就不跳。很明显一个循环。我们向下继续。

将r13的值加4,跳到刚刚的401114处,如此看来,r12d是循环次数,6次。从开始截至401151,是一个二重循环。

1
2
3
4
5
6
for (int i=0; i<6; i++){
if (arr[i] - 1 > 5) bomb()
for (int j=i+1; j<=5; j++) {
if(arr[j] == arr[i]) bomb()
}
}

我们从401153处继续,向下还是一堆赋值操作,然后比较,rax和rsi不等就跳,向下看,0x0给esi,然后跳到401197.

image-20220301120225795

来到401197处

image-20220301120702509

首先是赋值,然后与1作比较,小于等于跳到401183否则跳到401176进行循环,然后通过401176的循环再步入401188的循环。

我们从4011ab向下看

image-20220301121122327

依然是一堆赋值,然后接上比较,相等就跳转。将rbx给rcx后,跳到4011bd处,进行循环,要循环8次,然后到4011d2处,接着又是一堆赋值,需要eax小于rbx的值,才跳转,否则炸。最后还有一个从4011df开始的循环(ebp-1不为0则跳转。)。其实是一个值的置换操作,不过是执行6次,按降序排列

需要注意一下4011a4处的命令

image-20220301125119167

看样子是个结构体(用IDA看就很明确)

image-20220301124131818

image-20220301124149305

image-20220301124211115

image-20220301124227196

image-20220301124245570

image-20220301124303447

所以以上逻辑大致为我们输入的序列被7减去后得到的序列,是一个向量,向量每个数字是node的序号,向量的顺序是node的链接顺序,也就是说向量的第一个序号对应的node会变成头节点

最后放上IDA的代码

image-20220301122304972

image-20220301122335913

image-20220301122359866

故,依照上述代码逻辑将6个node的值按降序排列,所得序号为3 4 5 6 1 2,但因为在这之前涉及x=7-x这一操作

故正确答案应为4 3 2 1 6 5,至此扫雷。

secret_phase

image-20220301125404871

image-20220301130109136

wairi,受不了了,直接放反编译出的代码

image-20220301125918989

func7

image-20220301125942372

和汇编代码对照看,第二个if是右移,第三个是左移

发现0x6030f0处的往后是个二叉树,

image-20220301130237470

再往后就是node了。

└─ 36
├─ 8
│ ├─ 6
│ │ ├─ left: 1
│ │ └─ right: 7
│ └─ 22
│ ├─ left: 20
│ └─ right: 35
└─ 50
├─ 45
│ ├─ left: 40
│ └─ right: 47
└─ 107
├─ left: 99
└─ right: 1001

我们需要找的数,是最后返回2的,那么,最后一次就返回0,再上一次返回2 * rax + 1,第一次返回2 * rax

那么两个值可以,20或22.

至此,扫雷完毕。

不算小结的小结

汇编看起来确实麻烦,做最后两个的时候都是结合C语言代码来搞得,对汇编还不是很熟悉。

这个实验如果用IDA当逆向题做的话,难度会降低很多。

bitXor(x,y)用‘&’和‘~’完成异或操作

在此之前,我们先了解一下这三个运算符的运算规则

设A=60,B=13;

则A = 0011 1100;B = 0000 1101。

1
2
3
4
5
“与”--‘&’:
0&0=0;
0&1=0;
1&0=0;
1&1=1;A&B=0000 1100
1
2
3
“取反”--‘~’:
对形式取反
~A = 1100 0011 = -61。
1
2
3
4
5
6
“异或”--‘^’:
0^0=0;
0^1=1;
1^0=1;
1^1=0;
(A ^ B) 将得到 49,即为 0011 0001

可以看到,&和^的区别在于,除两数同时为0时,结果相反。那么我们分而治之,设置两种情况,以上面的A,B为例。

第一种–只考虑不是两个0进行运算的时候:两数直接进行&运算然后取反,得1111 0011;

第二种–只考虑是两个0进行运算的时候:这次我们的目的是在其他数(即上面那个数从左往右的第3、4、5、6,8位)经过运算且发生变化后保持0和0进行运算后结果依然为0,才能让两个最终运算结果在最后&时得到与^相同的数。这里绕个弯,先将A,B各自取反,进行&运算后,再将结果取反。可以发现达到了开始时的目的

最后,将两情况的结果进行与运算,得到答案。

1
2
3
4
int bitXor(int x, int y)
{
return ~(~x&~y)&~(x&y);
}

借用大佬的思路如下:(比我清晰很多,一语即中。)

所谓异或就是当参与运算的两个二进制数不同时结果才为1,其他情况为0。C 语言中的位操作对基本类型变量进行运算就是对类型中的每一位进行位操作。所以结果可以使用“非”和“与”计算不是同时为0情况和不是同时为1的情况进行位与,即~(~x&~y)&~(x&y)

tmin()最小二进制补码整数型

因为整数int型位32位(一般情况下,此处不讨论16位),而最小的补码只需要是第一位为1(1开头表示负数),后面的全是0即可,所以只需要将1左移31位即可(因为机器从0开始读。)

1
2
3
4
int tmin(void) 
{
return 1<<31;
}

isTmax(x)判断当X为最大整型二进制数为真,否则为假

此处先说明一下最大二进制整数,其实是0x7fffffff(二进制整数首位是 0,其余都是1,f代表1111,32位)

F的二进制码为 1111
7的二进制码为 0111;0xffffffff为-1;但最小值是0x80000000

上文说了开头为1表示是负数,为0则为正数。

对最大整型数有以下特征:

Tmax+1 = Tmin=~Tmax

故代码需要让唯一情况为0x7fffffff,并排除0xffffffff。

0x7fffffff+1=0x80000000

1
2
3
4
int isTmax(int x)
{
return !(x+(x+1))|!(x+1);
}

判断所有奇数位是否均为1

这需要用到0xAAAAAAAA来进行判断。

image-20220204145219988

先构造该数

1
2
3
4
5
6
int allOddBits(int x)
{
int i=0xAA+(0xAA<<8);#左移8位,构造0xAAAA
i += i<<16;#构造0xAAAAAAAA
return !((i&x)^i);
}

不用-,求-x的值

这个很简单

1
2
3
4
int negate(int x)
{
return ~x+1;
}

判断是否是0~9的ASCII码值

首先确定范围是0x30~0x39。

从二进制原码上下手会比较好理解。0x30~0x39,只用考虑低四位。(9为0x1001)

但还有一个问题0x300x39的第58位为0011.

所以还得先判断第5~8位是否为0011,再来判断第四位。

1
2
3
4
5
int isAsciiiDigit(int x)
{
int z=0xf,y=3,p=6;
return !(((x>>4)^y)|(x&z)+p)>>4);
}

((x>>4)^y)用于判断第58位是否为0011,先将58位变为1~4位,再异或3,返回0则对。

(x&z)+p)>>4用于判断1~4位:首先我们知道,十六进制数位0123456789ABCDEF,以9来考虑,加上6后是有进位的,故上限可加6然后再左移看进位与否判断,下限自然不用了,毕竟开头的是0不是1就行。

使用位级运算实现C语言中的 x?y:z三目运算符。

首先了解一下何为三目运算符

1
2
3
4
int conditional(int x,int y,int z)
{
return ((x>>31|((~x+1)>>31))&y)|(~x>>31|(~x+1)>>31))&z;
}

有一种特殊情况:当条件为假的时候,而当结果1为零时,返回结果2。

故总共有三种情况。

条件用(x>>31|((~x+1)>>31))判断。后续根据相应结果做最后运算。

使用位级运算符实现<=

1
2
3
4
int isLessOrEqual(int x,int y)
{
return !!((x>>31)&(~y>>31))|(!!((x+(~y+1))>>31)&~((~x>>31)&y>>31))|!(x+(~y+1));
}

我们并不知道两个数正负,所以先对正负,也就是符号位进行判断(四种情况):都为正或负,一正一负。

那么挨个实现即可,难度不大。(>>31是为了对符号位进行比较。)

(一个小技巧:!!x可以把非零数变为1,0变为0。)

计算!x却不用!符号

!:称为逻辑非运算符。用来逆转操作数的逻辑状态。如果条件为真则逻辑非运算符将使其为假。

简而言之,使结果呈相反状态。

1
2
3
4
int logicalNeg(int x)
{
return ~(x>>31)&(((~x+1)>>31)+1);
}

可以从符号位上下手,布尔代数只有0和1。符号位也是只有0和1。

将其本身的符号位与上其相反数的符号位加1(若x为0,则加1后为0反之为1.)最后取反。

计算一个数用补码表示最少需要几位

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int howManyBits(int x)
{
int b16,b8,b4,b2,b1,b0;
int sign=x>>31,ret=0;
x = (sign&~x)|(~sign&x);
ret=1+!!x;
b16=!!(x>>16)<<4;
x=x>>16;
b8=!!(x>>8)<<3;
x=x>>b8;
b4=!!(x>>4)<<2;
x=x>>b4;
b2=!!(x>>2)<<1;
x=x>>2;
b1=!!(x>>1);
return b16+b8+b4+b2+b1+ret;
}

这个题就像一排座位,判断上面有多少是坐了人的。(但是不能直接看见)

所以先将整体一分为二,拆为高低16位,先判断高16位的值是否为0,不是的话将高16位提取出,再一分为二,高8位,低8位。同样的方法判断;若高16位为0,则采用如上相同方法判断。

当然,还得先看符号位是啥,前面说过,!!x可以将非零数变为1,0变为0。

求浮点数(u)f*2

1
2
3
4
5
6
7
8
9
10
unsigned floatScale2(unsigned uf) {
if((uf>=0x7f800000 && uf<=0x7fffffff)||(uf>=0xff800000 && uf<=0xffffffff) || uf==0 || uf==0x80000000)
return uf; //当uf=NAN或无穷或0的时候,返回它本身
else if(uf>0x80000000 && uf<=0x807fffff)
return ((uf<<1)+0x80000000); // uf为负的非规格化数
else if(uf>0 && uf<=0x007fffff)
return uf<<1; //uf为正的非规格化数
else
return (uf+(1<<23)); // uf为规格化数
}

关于浮点数,有必要知道阶阶码,规格化

还是得先排除NaN,无穷大/小,0这些情况。所以当浮点数为无穷,0或NAN时,返回本身,阶码全为1

再就是规格化数:阶码加一和非规格化数(非规格化数有正负之分,且阶码全为0:正数左移一位,负数补最高位一个1)。

将浮点型转换为整型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int floatFloat2Int(unsigned uf) {
int s_ = uf>>31;
int exp_ = ((uf&0x7f800000)>>23)-127;
int frac_ = (uf&0x007fffff)|0x00800000;
if(!(uf&0x7fffffff)) return 0;

if(exp_ > 31) return 0x80000000;
if(exp_ < 0) return 0;

if(exp_ > 23) frac_ <<= (exp_-23);
else frac_ >>= (23-exp_);

if(!((frac_>>31)^s_)) return frac_;
else if(frac_>>31) return 0x80000000;
else return ~frac_+1;
}

首先考虑特殊情况:如果原浮点值为0则返回0;如果真实指数大于31(frac部分是大于等于1的,1<<31位会覆盖符号位),返回规定的溢出值0x80000000u**;如果 [公式] (1右移x位,x>0,结果为0)则返回0。剩下的情况:首先把小数部分(23位)转化为整数(和23比较),然后判断是否溢出:如果和原符号相同则直接返回,否则如果结果为负(原来为正)则溢出返回越界指定值0x80000000u**,否则原来为负,结果为正,则需要返回其补码(相反数)。

求2的x次方

1
2
3
4
5
6
7
8
9
10
11
12
13
unsigned floatPower2(int x) {
if(x>=0x80000000 && x<0xffffff6a)
return 0; // 表示的数小于32位float表示的最小值
else if(x>=0xffffff6a && x<=0xffffff81)
return 1<<(149+x); // 表示的数是非规格化数
else if(x>=0x00000080 && x<0x80000000)
return 0x7f800000; // 表示的数大于32位浮点数表示的最大值
else
{ // 规格化情况
unsigned y=x+127;
return y<<23;
}
}

首先分析什么时候返回0,也就是当小于float最小表示的数的时候返回0。因为2^x必然是大于0的,float表达的最小数就是0x1,此时为2的-126次方乘以2的-23次方,等于2的-149次方,所以第一种情况就是x大于-149,则返回0。
  然后再分析什么时候返回正无穷,float表示的最大值是0x7f7fffff,即当x大于等于128(阶码全为1)的时候表示无穷,返回正无穷。
  然后分析,2^x能表示当float阶码为0,尾数只有一位为1的情况和阶码为0x1,尾数为0的情况即-149<=x<=-126。这个时候的float的表示应该是除了尾数有一位为1,其他位都是0。可以计算出尾数为1的那一位距离尾数最低位的位数m,然后将1<<m即可。这里的m应该用“23-(-x-126)”即m=149+x。所以是1<<(149+x)。
  然后就是规格化数的一种情况,这种情况最简单,就是阶码位加一就可以了。

Hook钩子

首先是一个比较简单的DLL,可以钩取用户从键盘上输入的信息。

这一节的主要目的应该还是后面调试应用程序那一部分。

那着重说一下调试部分:

​ 本次调试重点在于,能够钩取键盘输入的DLL是如何载入到程序当中的。按书中操作,在调试设置中勾选当DLL载入时暂停的选项。通过主函数体可知,主函数体中存在LoadLibrary这一能将DLL文件导入到应用程序中的关键函数。(该函数的作用:将指定的模块加载到调用进程的地址空间中。指定的模块可能会导致其他模块被加载。)

DLL注入

首先是注入的三个方法:

  1. 创建远程线程
  2. 使用注册表
  3. 消息钩取

创建远程线程注入

涉及关键应用InjectDLL.exe,该应用程序的源码在书中已有详细的逐行介绍,此处不再赘述。(这几章的关键都在实现对应功能的程序的代码,理解其代码是重点和难点,实际操作反而容易。)

说一下句柄的意义—-句柄实际是一个指针,他指向一块包含具体信息数据的内存,可以当做索引 ,所以进程句柄是当你要访问该进程时取得的,使用完毕必须释放。

总的过程就是:

先将要注入的DLL路径写入内存(在此之前会先调用函数分配缓冲区,大小为DLL文件路径字符串的长度)—然后获得加载DLL函数的地址—在要被注入DLL的进程中创建远程线程—调用加载DLL文件的函数来加载(注入)DLL

有一处值得注意:

image-20220105162354191

使用注册表注入

image-20220105162917205

修改AppInit_DLLs表项的值,将其值改为待注入DLL文件的路径(最后以该文件命结尾的完整路径),再将LoadAppInit_DLLs的值改为1.最后重启系统,及完成该修改。

消息钩取暂不做讨论。

DLL卸载

还是一样,先读懂相应程序的源代码。其实跟注入差不多的,只是是卸载罢了。亲自看一遍会有更深的印象。

另外,PE文件直接导入的DLL文件是无法被卸载的,该方法仅适用于自己强行注入的DLL文件。

通过修改PE加载DLL

先贴一篇关于HWND,HANDLE,HMODLE,HINSTANCE的区别的博客,有个基础了解即可。

在这一章停了一段时间,说一下我遇到的问题。

首先就是IID整体大小,14为IID结构体大小,5为IID结构体个数

image-20220105182929724image-20220105183003195

此处因为union结构体的缘故,故只有5个IID结构体。

简单说一下union结构体:(union)共用体占用的内存应足够存储共用体中最大的成员。

然后注意在创建新IDT时是设计了RVA to RAW的转换的,此处也就就不再赘述了。

另外,移动到可用的空白区域即可,不是非要移动到RVA为8C80处。

image-20220105185215395

然后一个点,bit OR是位操作 ( | 这个符号),虽然说结果跟异或(bit XOR)一样。4|8==12==4^8,4|0==4==4^0。

代码注入DLL

本章关键在于读懂InjectCode()函数的代码。
其实使用的关键API就那么几个

OpenProcess()
VirtualAllocEx()相对的有VirtualFreeEx()
WriteProcessMemory()相对的有ReadProcessMemory()
CreateRemoteThread()

多加理解,对着敲一遍也不错。

使用汇编语言编写注入代码

前面编写汇编语言部分可以看看,能大致理解就行(本章后面是由逐行解析的)

关键在于后面的CodeInjection2.cpp的代码。

其实看看代码内容,核心的API确实就上面那几个。不过是加入了要注入的代码的十六进制数据。

总结

纸上得来终觉浅,绝知此事要躬行。还是要多动手实践,这部分很注重实操。如果看不太懂,不妨试着结合网络写笔记,写的过程中逐一突破。

image-20220105220107191

最后再上一碗鸡汤,多是一件美事。

image-20220105220319771

UPX部分

众所周知,UPX是一种较为常见的压缩壳。(除压缩壳外,还有加密壳)

先对壳做个基本的了解:壳的初始作用是保护软件,但后来发展的方向不一就出现了各种各样的壳,大致有压缩壳、加密壳、VM 壳的分类。压缩壳故名思意,主要作用是用于压缩方面,可以有效的减小软件的大小;加密壳,其主要作用是保护软件;VM 壳是一种很特殊的壳,它利用了虚拟机技术,可以很有效的保护指定地址代码,但很大的牺牲了效率,所以一般只在关键代码处使用。(其实就是隐藏函数的OEP–主要入口点)

压缩和压缩方式

  1. 压缩又分为无损压缩和有损压缩。

    两种压缩都是为了能节省一定的空间,只是无损不会使数据不完整(如各种压缩包);而有损有时会通过压缩文件(数据)损失一定的信息,来提高压缩率(如mp3、mp4和jpg文件,这也是为什么前两个有音质和画质之分的根源所在吧)。

  2. 压缩方式大体分为两种:普通压缩和运行时压缩。一张图说明二者区别

image-20211224113639435

运行时压缩(也叫PE压缩器)是针对PE文件的。PE文件内部含有解压缩代码,运行瞬间于内存中解压然后执行。

上文说过,压缩可以节约内存空间,还有一点,能够隐藏PE文件的内部代码和资源。

  1. 既然压缩的目的是为了防止内部代码资源泄露,那么为了对PE文件提供更强的保护,PE保护器就存在了。

image-20211224114154673

  1. 压缩文件和正常文件的PE结构的区别(感觉这一块书上已经将的很清楚了,就放原图吧。)
    image-20211224114504354

image-20211224120500126

image-20211224120527464

脱UPX壳

32位的就没啥说的了拖进OD找到PUSH ad和POP ad就行了或者能用ESP定律就更好

64位就需要单步跟踪了,具体情况具体分析(推介X64dbg)。

关于重定位

前面浅谈PE文件的时候,简单说过重定位。

这里再贴一下吧

image-20211224121526295

本次我们注重关注在重定位的原理。

PE文件重定位的原理

image-20211224122210645

查值通过基址重定位表完成,基址定位表地址如下:

image-20211224123046202

基址定位表是IMAGE_BASE_RELOCATION结构体数组

该结构体含有两个成员:1. 基准地址virtualaddrss—RVA值(DWORD型);2.SizeOfBlock—重定位块的大小(DWORD型)还有一个被注释的Typeoffset—表示该结构体下会出现WORD型的数组,该数组元素的值就是硬编码在程序中的地址偏移。

此处易将VA和RVA混淆,贴一个。

image-20211224152619953

通过找到基址重定位表的RVA的值,我们在.reloc节区可以发现

image-20211224151613796

image-20211224151859721

该重定位节区在2AE00开始,那根据其结构体成员可知,RVA的值位1000,重定位块的大小为150(都是小端序标识法。)

image-20211224152136922

我们已经知道Typeoffset(它的低12位是真正的基于VirtualSize位置的偏移。)是WORD型(两字节,16位)的,其由4位的Type和12位的offset合成的。

比如第一个3420,其Type为3,offset为420。

程序中硬编码地址的偏移用如下等式换算:

VirtualSize+Offsize=RVA(该等式中,VirtualSize和RVA易混淆,见上文所贴的图。)

image-20211224155355494

可以看到图片下方的ds里有IAT的地址VA,C010C4(不同设备运行,值不一样。)

使用上述偏移值可以查找硬编码地址值,将该值减去ImageBase,再加上实际加载地址,即可得到VA,完成重定位。

对于程序内硬编码的地址,PE装载器都做如上的处理,根据实际加载的内存地址修正后,将得到的值覆盖到同一位置上。对一个IMAGE_BASE_RELOCATION结构体的所有TypeOffset都做如上处理,且对RVA 1000~2000地址区域对应的所有硬编码地址都要进行PE重定位处理。如果TypeOffset值为0,说明一个IMAGE_BASE_RELOCATION结构体结束。至此,完成重定位流程。

删除重定位节区.reloc

由于该节区对程序正常运行没有影响,且删除后文件大小将缩减。故试着删除:

image-20211224161108321

可以看到,该节区区域为270-297,用00填充该区域;接着删除该节区,根据该节区的其起始偏移位置C000,一直到末尾,删除即可(因为.reloc为最后一个节区,所以直接到拉到文件末尾进行删除);然后修改节区数,该文件原有5个节区,现在删了一个,将其改为4个,在文件头里面修改number os sections

image-20211224161900975

下图为已经更改过的image-20211224161923290

最后就是修改可选头的文件(映像)大小,size of image。

同样的的方法

image-20211224162116107

该重定位节区的大小为E40

这里还需要用到如下知识。(选自加密与解密第四版)

image-20211224162435728

在可选头中,我们找到了两种对齐值

image-20211224162757991

故,E40需对齐到1000.再用原本的size of image将其减去,则为10000,做出修改。

至此,重定位节区算是删除完成了。

UPack PE文件头分析及查找OEP

感觉没啥好说的,对着书看吧…

内嵌补丁(内嵌代码补丁)

先上一张概念图:

image-20211224164128856

整个过程中,值得注意的点:

  1. 程序运行的逻辑
  2. 代码整体结构(借用一下书上的图)image-20211224164351755
  3. 补丁代码设置的位置
    • 设置到文件的空白区域
    • 扩展最后节区后设置
    • 添加新节区后设置
  4. 异或操作和加密区域的查找—(涉及RVA to RAW的转化。)

对该练习有疑惑可以参考一下文章:

内嵌补丁练习_Mi1k7ea-CSDN博客

总结

注重实操。

C语言中函数调用约定

首先定义如下函数 int function(int a, int,b)

我们都知道,机器是无法识别代码的,它只认识0,1。我们只需要通过another_number = function(a,b)就能够条用该函数。而在cpu中,计算机并不知道调用该函数需要传递几个、什么类型的参数。为了完成调用这一过程,计算机提供了“栈”这种数据结构来支持传递参数。

(栈的结构等相关内容不再赘述)

函数调用时,调用者依次把参数压栈,然后调用函数,函数被调用以后,在栈中取得数据,并进行计算。函数计算结束以后,或者调用者、或者函数本身修改栈,使栈恢复原装(初始状态)。

此处面临两个问题:

  • 参数多于1个时,应该按什么顺序把参数压入栈?

  • 函数调用后,由谁来把栈恢复原装?

在高级语言中,通过函数调用约定来说明这两个问题。常见的调用约定有:

  • stdcall
  • cdecl
  • fastcall
  • thiscall
  • naked call

stdcall(很多时候被称为pascal调用约定)

(Win32 API函数绝大部分都是采用_stdcall调用约定的。WINAPI其实也只是_stdcall的一个别名而已。)

stdcall的调用约定意味着:1. 参数从右向左压入栈;2. 函数自身修改栈;3. 函数名自动加”_”,后面紧跟一个”@”符号,其后紧跟着参数的尺寸(字节数)。

在编译时,这个函数的名字按照如上所述,会被翻译成_function@8

函数调用过程的汇编语言如下:

1
2
3
push b      第二个参数入栈
push a 第一个参数入栈
call function 调用参数,注意此时自动把cs:eip入栈

而对于函数自身,则可以翻译为:

1
2
3
4
5
6
7
push ebp     保存ebp寄存器,该寄存器将用来保存栈的栈顶指针,可以在函数退出时恢复
mov ebp,esp 保存栈指针
mov eax,[ebp + 8H] 栈中ebp指向位置之前依次保存有ebp,cs:eip,a,b,ebp +8指向a
add eax,[ebp + 0CH] 栈中ebp + 12处保存了b
mov esp,ebp 恢复esp
pop ebp
ret 8

栈帧

(贴一张栈帧帮助理解)

函数结束后,ret 8表示清理8个字节的栈,函数自己恢复了栈。

cdecl调用约定(C调用约定)

该约定为C语言默认的调用约定:所有参数从右到左依次入栈,这些参数由调用者清除,称为手动清栈。命名规则为在函数名前自动加“_”.

被调用函数不会要求调用者传递多少参数,调用者传递过多或者过少的参数,甚至完全不同的参数都不会产生编译阶段的错误。

1
int (__cdecl)function(int a,int b)   //加不加_cdecl都一样,只是有没有明确指出的区别

因为是手动清栈,所以在最后的ret便不会清理,故无字节数。

函数自身汇编语言如下,

1
2
3
4
5
6
7
push ebp     保存ebp寄存器,该寄存器将用来保存栈的栈顶指针,可以在函数退出时恢复
mov ebp,esp 保存栈指针
mov eax,[ebp + 8H] 栈中ebp指向位置之前依次保存有ebp,cs:eip,a,b,ebp +8指向a
add eax,[ebp + 0CH] 栈中ebp + 12处保存了b
mov esp,ebp 恢复esp
pop ebp
ret 注意,这里没有修改栈

fastcall

改调用与stdcall类似:

  • 函数的第一个和第二个DWORD参数(或者尺寸更小的)通过ecx和edx传递,其他参数通过从右向左的顺序压栈
  • 被调用函数清理栈
  • 函数名修改规则同stdcall

thiscall

thiscall是唯一一个不能明确指明的函数修饰,因为thiscall不是关键字。它是C++类成员函数缺省的调用约定。由于成员函数调用还有一个this指针,因此必须特殊处理,thiscall意味着:

  • 参数从右向左入栈
  • 如果参数个数确定,this指针通过ecx传递给被调用者;如果参数个数不确定,this指针在所有参数压栈后被压入栈。
  • 对参数个数不定的,调用者清理栈,否则函数自己清理栈

(由于此调用约定仅适用于 C++,因此它没有 C 名称修饰方案。)

vectorcall

vectorcall 继承于fastcall 但对于fastcall中的整数仍然按照fastcall规则传递 而浮点以及向量将通过寄存器传递

nakedcall

编译器不会给这种函数增加初始化和清理代码,更特殊的是,你不能用return返回返回值,只能用插入汇编返回结果。这一般用于实模式驱动程序设计。

__pascal

唯一一个从左到右入栈的约定

函数调用约定导致的常见问题

如果定义的约定和使用的约定不一致,则将导致栈被破坏,导致严重问题,下面是两种常见的问题:

  1. 函数原型声明和函数体定义不一致
  2. DLL导入函数时声明了不同的函数约定(所以不能口是心非,会出大问题。)

贴一个微软的对此的解释函数调用约定

再贴一篇关于从汇编语言角度对参数传递的理解的博客 从汇编角度看函数参数传递—汇编函数参数

Java相关

native关键字

native 用来修饰方法,用 native 声明的方法表示告知 JVM 调用,该方法在外部定义,我们可以用任何语言去实现它。 简单地讲,一个native方法就是一个 Java 调用非 Java 代码的接口。

native 语法:

  ①、修饰方法的位置必须在返回类型之前,和其余的方法控制符前后关系不受限制。

  ②、不能用 abstract 修饰,也没有方法体,也没有左右大括号。

  ③、返回值可以是任意类型

System.LoadLibrary

该函数的作用:将指定的模块加载到调用进程的地址空间中。指定的模块可能会导致其他模块被加载。对于其他加载选项,请使用 LoadLibraryEx函数。Ex(extra-额外的)—-总的来说,装载库文件。与之类似的还有System.load

下面对这两个相似的函数做个解析:

  1. 它们都可以用来装载库文件,不论是JNI库文件还是非JNI库文件。在任何本地方法被调用之前必须先用这个两个方法之一把相应的JNI库文件装载。
  2. System.load 参数为库文件的绝对路径,可以是任意路径。
    例如你可以这样载入一个windows平台下JNI库文件:
    System.load(“C://Documents and Settings//TestJNI.dll”);。
  3. System.loadLibrary 参数为库文件名,不包含库文件的扩展名。
    例如你可以这样载入一个windows平台下JNI库文件
    System. loadLibrary (“TestJNI”);

Biginteger

在Java中,由CPU原生提供的整型最大范围是64位long型整数。使用long型整数可以直接通过CPU指令进行计算,速度非常快。

如果我们使用的整数范围超过了long型怎么办?这个时候,就只能用软件来模拟一个大整数。java.math.BigInteger就是用来表示任意大小的整数。BigInteger内部用一个int[]数组来模拟一个非常大的整数

此处仅作简单介绍,详细了解参考如下的教程网站Bitinteger

Weekly Study

(本周由于“积极”备考,所以书看的比较少,稍微写写做题中遇到的一些问题吧。)

  1. 迷宫题

    由于之前在攻防世界练过一道迷宫题,simple的8和14也没有停顿太久

    这类题的关键就在与搞清“怎么走”—迷宫图的正确画出是关键,其次是按钮控制的方向。这些都需要结合具体函数进行判断。

    以simple8为例

    image-20211219174148388

可以猜测,本题迷宫又1152个字符构成,且可组成每行60字符,共19行的迷宫

故先打印处整体的迷宫(实际是1140个字符)

image-20211219180035314

image-20211219181223256

规则如上代码,s为起点,t为终点,移动方式为经典WASD,按图走出即可。

总结:画图和移动规则是关键。

  1. 关于反调试

    对反调试稍微看了一下,主要参考以下文章Windows下反反调试技术汇总 - FreeBuf网络安全行业门户,此次不做总结(还没看完,暂时停留在静态反调试技术。)

  2. 然后就是simple7的爆破法,(头一次用,当时还没看明白学长们的range(33(31开始也可以),127)是啥意思,其实就是ASCII码表对应的可见字符。)

  3. 最后就是继续学了一点汇编,其余的时间都在复习四级了。