1、背景
在CTF比赛中,除了分析程序的工作原理外,CTF逆问题还需要根据分析结果进一步得到FLAG。逆向是解题竞赛系统中的一种独立题型,也是PWN题型的必备技能。常与攻防系统中的PWN题相结合。CTF逆向主要涉及逆向分析和破解技巧,也需要有扎实的反汇编、反编译、加解密基础。
CTF中的反向题一般都是常见的考点
1、常用算法和数据结构。
2、各种排序算法、树、图和其他数据结构。
4、代码混淆、代码虚拟化、代码流修改、反调试等。
5、软件加密外壳是软件保护技术的集中应用。
CTF逆向问题两大主题:蛮力破解、算法分析破解
1、蛮力破解:通过修改汇编代码跳过程序内部验证部分,从而改变程序的正常逻辑,最终满足问题要求获取flag。这测试了在反向分析样本时定位样本验证码的能力。
2、 算法破解:这主要需要分析样本中加密部分的汇编代码,还原其加密算法,根据分析结果编写相应的解密程序,最后计算出flag。这就考验了样本分析过程中的耐心和扎实的逆向能力以及一定的代码开发能力。
逆向解题的常用技巧
1、逆向分析与函数猜测相结合,通过逆向分析缩小猜测范围。猜测是反向指出方向,猜测的想法是反向重新验证。
2、结合示例中的汇编代码上下文和整体程序功能,注意程序中给出的文字提示信息。
3、实际比赛中的逆题多为以提问为目的的题,目的性强,功能结构单一,无关代码少。
4、程序代码量极大时,可以先判断是否引用了更多的开源代码,程序的主要逻辑比较简单。
2、逆向基础
CTF逆向常用工具
, IDA, , PE(), CFF , , .
CTF逆向需要涉及知识点
组装知识:X86和X64下;ARM和ARM64下。
文件结构:PE文件下;dex文件和ELF文件。
反调试技术:调试和反调试对抗技术下及下。
装箱和拆箱:PE的装箱和拆箱,ELF和dex的装箱和拆箱。
开发能力:C、C++等语言的开发能力。
CTF样本逆向解题流程
1、违规保护:
获取示例程序时,首先使用工具查看程序属于哪个平台,如X86/X64、、linux等,是否采取了代码保护措施,如代码混淆、保护壳、各种反调试等。在分析样本之前,需要通过样本混淆、解包、反反调试等技术去除或绕过这些保护措施。
2、位置键码:
我们需要对目标软件进行反汇编,然后结合IDA和OD来快速定位关键代码(如验证函数、关键字符串信息、程序导入表)。
3、动静结合:
找到程序的关键代码后,我们需要对其进行详细的逆向分析。如果程序在IDA中可以F5生成伪代码,那么我们先根据伪代码进行静态分析,然后我们可以用工具动态调试不明确的地方,观察来验证我们的猜想。
4、破解验证算法:
经过详细的逆向分析,在程序的关键代码(例如验证算法)之后,需要根据分析结果进行暴力破解或编写算法解密代码来获取或生成flags。
CTF中常见的验证算法
1、 直接对比验证:
密钥一般不加密,直接与内置程序中的密钥进行比较(即硬编码比较)。这类问题比较简单。
2、加密对比验证:
3、 反向自定义实现算法:
4、其他类型的加密问题:
如果实在解决不了,也可以尝试绕过或暴力破解穷举法。
如何找到关键代码
1、序列跟踪方法:
如果得到的样例程序比较少,代码量不大,主函数入口容易找到,可以使用顺序追踪的方法,从程序的主函数入口开始依次追踪,分析一步一步完成程序执行过程,基本了解程序。的验证部分。至于各类程序主要功能的搜索方式,大家可以自行百度。这里需要区分程序入口点和main函数的区别。您通常需要找到主要功能,但不是所有主要功能。到了MFC程序,还是需要根据具体情况来分析。如果有条件,也可以编写相应的程序,实现,然后拆机练习,找到程序的主要功能。这需要更多的实践、积累和总结。
2、 字符串查找:
如果给定的示例程序没有被混淆,并且有比较明显的字符串信息提示,那么可以使用字符串搜索功能,根据程序运行的提示,找到程序提示的字符串,反向搜索字符串程序提示。参考地址。比如IDA中shift+F12的字符串窗口,在OD->所有引用文本字符串中搜索,字符串搜索的优先级很高,很多情况下对我们解决问题非常有效arm汇编指令中的变量,所以得到后程序中,您可以优先考虑字符字符串查找尝试。
3、系统函数断点:
如果程序很大,没有字符串提示信息可以使用,那么我们可以根据示例程序使用的函数来猜测定位关键验证码,这需要C语言或C++语言的知识和核心编程,并且对它有些熟悉。一般函数的功能,如:程序中出现弹窗,则程序可能调用该函数,如果程序有输出,则程序可能调用该函数等,可以表示为该程序。state中设置了对应的函数断点,然后栈回溯到它的参考位置,然后找到关键代码。
3、装配基础
(以下知识点只起到抛砖引玉的作用)
x86 程序集
32位CPU有16个寄存器,32位寄存器存储4个字节的数据。他们的名字是:
4个数据寄存器(EAX、EBX、ECX和EDX);
2个索引和指针寄存器(ESI和EDI);
2个指针寄存器(ESP和EBP);
6个段寄存器(ES、CS、SS、DS、FS和GS);
1个指令指针寄存器(EIP);
1 个标志寄存器()。
X64总成
64位CPU有16个通用寄存器,寄存器存储8字节数据。他们的名字是:
rax,rbx,rcx,rdx,rsi,rdi,rsp,rbp
r8,r9,r10,r11,r12,r13,r14,r15
32位使用堆栈帧作为传递参数的存储位置,而64位使用寄存器,分别使用rdi、rsi、rdx、rcx、r8、r9作为1-6个参数。rax 作为返回值。
64位没有栈帧指针,32位使用ebp作为栈帧指针,64位取消这个设置,rbp作为通用寄存器使用。
rax 用作函数返回值。
rsp栈指针寄存器,指向栈顶
rdi、rsi、rdx、rcx、r8、r9作为函数参数,依次对应第一个参数和第二个参数。. .
rbx, rbp, r12, r13, r14, r15作为数据存储,遵循被调用者的使用规则。简而言之,它们被随意使用。在调用子函数之前,应该备份它,以防止它被修改。
r10 和 r11 用作数据存储,并遵循调用者的使用规则。简单地说,使用前必须保存原始值。
大会知识
函数 (Call) 有 3 种调用约定: ,,
: 是C语言默认的函数调用方式,所有参数从右到左压入栈中,调用者清除这些参数。堆栈恢复常用指令 add,esp,x,x 表示参数占用的字节数
: 是C++标准函数调用方式。所有参数从右到左压入堆栈。如果是调用类成员,最后压入堆栈的是 this 指针。
这些堆栈上的参数在返回时由被调用函数清除,使用指令 ret x,其中 x 是参数占用的字节数。
: 是编译器指定的函数的快速调用方法。由于大多数函数的参数数量很少,因此使用堆栈传递非常耗时。因此,通常规定前两个参数通过寄存器传递,其余参数通过堆栈传递。但是不同编译器编译出来的寄存器是不一样的,返回方式一般都是ret x。
总结三个调用约定:
区分函数参数和局部变量
函数局部变量的存在形式:mov eax,dword ptr[ebp -4]
函数参数表示法:mov eax, [esp+arg_0]
Arm32 组件()
ARM微处理器共有37个32位寄存器,其中31个为通用寄存器,6个为状态寄存器。但是这些寄存器不能同时访问,可以访问哪些寄存器取决于ARM处理器的工作状态和具体的工作模式。但在任何时候,通用寄存器R14~R0、程序计数器PC,一个状态寄存器都是可以访问的。
未分组寄存器R0~R7,共8个;
分组寄存器R8~R12、R13~R14R8~R12:FIQ模式下有单独的一组R8~R12,共5个;
其他6个模式共用一组R8~R12,共5个;共10个;
R13~R14:USR和SYS模式(表格第一列)共用一组2个R13~R14,其他5个模式各有一个单独的10个R13~R14组;共12个;
程序计数器PC为R15arm汇编指令中的变量,共1;
组寄存器 R13、R14
寄存器 R13 通常做堆栈指针 SP
寄存器R14用作子程序链接寄存器(Link-LR),也称为LR,指向函数的返回地址。
Arm64 组件()
汇编中共有 34 个寄存器。包括31个通用寄存器、SP寄存器、PC寄存器、CPSR寄存器。
31 个通用寄存器中:
X0-X30:表示它是一个64位的寄存器。
W0-W30:表示它是一个32位的寄存器。
X31:又称零寄存器(一般用于变量初始化),它也有两种表示形式: XZR:表示一个64位的零寄存器,在内存中存储8个字节。
WZR:表示一个32位的零寄存器,在内存中存储4个字节。
SP:保存栈指针(栈顶指针),使用SP或WSP访问SP寄存器,即操作局部变量地址。
PC:程序计数器(PC ),用于指向下一条即将执行的指令。
CPSR:状态寄存器
FP(X29):保存栈帧地址(栈底指针)
LP(X30):X30通常称为程序的链接寄存器,保存子程序结束后需要执行的下一条指令。
在掌握ARM汇编的基本结构之前,需要复习并记住以下条件指令
1.1、 B 跳转指令
1.2、 BL 带返回的跳转指令
1.3、 带有返回和状态转换的 BLX 跳转指令
1.4、 带状态切换的 BX 跳转指令
2.将跳转地址值直接写入程序计数器 PC。
通过将跳转地址值写入程序计数器PC,可以实现4GB地址空间的任意跳转。跳跃前,使用MOV LR,PC
ARM 函数调用约定使用:ATPCS
ATPCS的英文全称是ARM-THUMB call(ARM-Thumb过程调用标准)
总结:参数1~4分别存放在寄存器R0~R3中,其余参数从右向左压入栈中,被调用者实现栈平衡,返回值存放在R0中。
功能参数:
当参数个数小于等于4时,使用r0~r3这4个寄存器进行参数传输;如果参数个数大于4个,则剩余参数通过sp指向的数据栈传递。
比如有3个参数,那么r0代表函数的第一个参数,r1代表函数的第二个参数,r2代表函数的第三个参数。
比如有6个参数,那么r0-r3代表前4个参数,然后剩下的两个参数通过在栈上开辟一个8字节的空间来传递。
r0-r3:存放传递给函数的参数值,多余的参数在栈上传递。
r4 -r11:存储函数的局部变量,Thumb模式不会使用r8之后的寄存器
r12:是内部过程调用临时寄存器(intra–call)。
r13:存储堆栈指针(sp)。在计算机中,堆栈非常重要。该寄存器保存指向栈顶的指针。更多关于堆栈的信息可以在这里找到。
r14:链接寄存器(link)。存储被调用函数返回时要执行的下一条指令的地址。
r15:用作程序计数器( )。存储当前执行指令的地址。每次执行后,计数器都会递增 (+1).
函数的返回值放在 r0 中。
fp称为帧寄存器,即栈帧指针寄存器;sp称为堆栈寄存器,即堆栈指针寄存器。
在ARM指令系统中,就是地址递减栈。栈操作的参数的堆叠顺序是从右到左,参数的堆叠顺序是从左到右。包括push/pop和LDMFD/STMFD等。
函数返回值
1.当结果为32位整数时,可以通过寄存器R0返回。
2.当结果为64位整数时,可以通过R0和R1返回,以此类推。
3.当结果为浮点数时,可通过浮点运算单元的寄存器f0、d0或s0返回。
4.当结果为复数浮点数时,可通过寄存器f0-fN或d0~dN返回。
5.对于更多位的结果,需要通过调用内存来传递。
4、反调试
反调试技术
反调试技术
1.函数
实现原理:只能用于自身进程的检测。通过查询进程环境块(PEB)中的标志位,如果处于调试状态,则返回非0,如果调试状态未调试,则返回0。
2.处理函数
原理:用于提取给定进程的信息。函数参数1代表进程句柄,参数2代表信息类型。如果第二个参数的值设置为0x7,则可以检查返回句柄标识的进程是否被调试。调试状态将返回调试端口,非调试状态将返回 0。
3.函数
实现原理:可用于自己的进程和其他进程。通过查询进程环境块(PEB)中的flags,如果处于调试状态,则返回值返回非0,如果未调试,则返回0。
4.,
实现原理:通过检测运行环境的调试器的窗口信息。
5.函数
实现原理:调试器在调试应用程序时,通过触发异常来执行调试功能。通过函数设置错误码方法,并使用函数打印出来,如果程序加入调试状态,得到的错误码就是之前的错误码。利用
错误码是一样的,如果没有被调试,错误码可以是任意值。
6.注册表检测
实现原理:通过查找调试器引用的注册表信息来判断,当前环境下的注册表是否有调试器的信息,
下面是注册表中调试器的常用位置。
\NT\(32 位系统)
\\(64 位系统)
此注册表项指定在应用程序中发生错误时触发哪个调试器。默认情况下,它被设置为 Dr.。如果这个表的键值被修改(其他调试器,也一样),应用程序可能会确定它正在被调试。
7.标志检测
实现原理:应用程序运行时,fs:[30h]指向PEB基地址。如果指向的标志为 0,则不调试应用程序,否则调试。
8.复选标记
实现原理:PEB结构中的数组中有一个未公开的位置,位于PEB结构的0x18处,包含一个标志位,可以用来判断是否处于调试状态。
9.复选标记
实现原理:调试器启动的进程和正常创建启动的进程不同,所以它们创建内存推送的方式也不同。标记它未被 记录,在 PEB 偏移 0x68 处,如果值为 0x70,则程序由调试器启动。
10.检测父进程是否为.exe
实现原理:对于正常启动的应用程序,其父进程为.exe。如果进程处于调试状态,那么它的父进程就是调试器进程。所以只要父进程不是.exe进程,就可以识别为调试状态。
反调试检测总结
上述反调试技术是应用比较广泛的技术,反调试技术一般通过各种方案组合。除上述解决方案外,反调试技术还包括对程序中执行代码段的验证、正在运行的调试器检测、调试器签名检测、断点检测等。反调试是逆向工程的第一道门槛,只有跨过这个门槛,才能真正开始逆向工程。
可以借鉴以下调试和反调试的技术总结方案。
参考链接:
java层检测
1.函数
检测原理:在调试应用时,调用.os.Debug。返回值为真,否则返回值为假。
2.:标签
检测原理:如果.xml文件中的属性值设置为true表示可以长时间调试,属性值为false表示不能调试。
3.包签名验证
检测原理:通过检测app的包签名信息,如果包签名不一致,则重新打包。
层检测
1.检测自己进程的值
检测原理:可以通过/proc/pid/或者/proc/pid/task/pid/获取值。默认值为0,附加调试将成为调试的PID值。
2.基于IDA调试器检测
检测原理:端口号(通过/proc/net/tcp检测23946端口)、文件信息、调试器进程名、
3.检测自己的流程图
检测原理:通过检测/proc/pid/maps来判断是否要调试,检测自己的进程是否有保护敏感模块信息。
4.检测父进程
检测原理:因为是所有程序的父进程,所以所有的应用app也是通过fork创建的,通过/proc/pid/获取。如果当前父进程这么长时间都不是进程,说明已经调试过了。
5.查看自己的状态
检测原理:app在被挂载和调试的过程中会被挂起,所以通过/proc/pid/stat或者/proc/pid/task/stat获取。当第三个字段属性为 t 时,表示正在调试应用。调试暂停挂起。
6.抢占
检测原理:一个app在系统中只能使用一次。所以先做你自己。
反调试总结
由于系统是开源的,上述使用系统属性和功能进行反调试可以通过修改系统源代码再重新编译来测试。对于这种修改系统的方式,其实可以用自己的方式。如果本身后面的值还是0,说明系统已经重新修改编译过。
攻防是一个持续的过程,反调试和过度反调试也是如此。这只是高潮和低谷。上面的列表只是一些简单的解决方案。
5、打包和解包
包装方案
专门负责保护软件免受未经授权的修改或编译的程序。它附在原始程序中。被加载程序加载到内存后,在原程序之前执行以获得控制权。在执行过程中,对原始程序进行解密和还原。恢复后,控制权返回原程序,执行原程序。代码。
其作用:可以有效防止破解者非法修改程序文件,也可以防止程序被静态反编译。不同的shell侧重于不同的方面,有的侧重于压缩,有的侧重于加密。比如压缩壳的特点是减小软件体积,加密保护不是它的重点。
常见的shell加载过程:
1.保存入口参数
2.获取shell本身需要使用的API地址
3.解密原程序各块数据
4.IAT 的初始化
5.重定位项处理
6.钩子接口
7.跳转到程序的原始入口点(OEP)
贝壳种类:
压缩壳有:UPX, 等。
UPX 是开源的:
壳牌官网:
加密外壳: , 等
虚拟外壳: , 等。
脱壳程序
手动拆包步骤:
1.找到真正的程序入口点
2.抓取内存镜像文件
3.重建PE文件
定位程序OEP的方法:
1.根据跨段指令搜索OEP
2.使用内存访问断点查找 OEP
方法:先打开内存模块(Alt+M),然后直接在代码段(如.text段)上执行内存访问断点(F2))。此断点是一次性断点。当段位于读取或执行时被中断。中断发生后,断点会被自动删除。
3.根据栈平衡原理找到OEP
在编写软件时,一定要保证shell初始化的寄存器的值与原程序相同。/popad、/popfd 命令通常用于保存和恢复活动环境。
4.根据编译语言的特点寻找OEP
各种语言编译的文件入口点各有特点。用同一个编译器编译出来的程序,入口代码相似,都有一段启动代码,编译程序时编译器会自动与程序连接。完成必要的初始化工作后,调用该函数。
包装方案
目前市面上的加固产品种类有:360加固、Ai加密加固、棒棒加固、腾讯乐谷、网易易盾、奇威加固、顶级形象加固。
一般将App源程序加密存放在那些目录中,一般是:dex文件的尾部、libs目录、目录。
网易易盾:.so、.so
爱加密:.so、.so、.dat
砰砰:.so,.so,.so
阿里居安全:.dat、.so、.so
腾讯安全:..so、.-v7a.so、.x86.so
娜迦:.so、.so、.so
360:.so、.so、.so、.so
传盾:.so, .so
网秦:.so
百度:.so
腾讯:-2.10.6.0.so、.so、.so、.so、.so
:。所以
猕猴桃安全:.so、.so、.so
强化功能
脱壳程序
解包的主要过程是在App程序运行后释放源App的数据内存,并复制释放的数据。
1.内存转储方法
通过使用frida框架结合解包脚本dex-dump
通过暴力搜索应用程序内存中的 dex.035 或 dex.036。
通过读取 /proc/pid/maps 找到后,转储数据。
2.挂钩键功能方法
主要使用frida框架进行脚本开发。
传递钩子键函数 er, l, ,,然后转储解密后的 dex 数据
3.动态调试方法
通过动态调试app的mmap函数的断点,然后去dump源dex数据。
4.自定义系统方法
通过修改和刷机重新编译系统。
挂钩。
枚举所有,对于所有类,调用强制加载
5.法律
ART模式下,生成oat时,内存中的DEX就完成了。
结尾
文章来源:http://news.sohu.com/a/506577424_121124372
感谢您的来访,获取更多精彩文章请收藏本站。

暂无评论内容