咕咕咕咕咕
连续若干CTF以后就是各种考试作业DDL催命_(:з」∠)_
等这两周各种考察课完了慢慢补各种活儿吧~
先交一下之前的DDWP-0-
还请各位大佬多指正~
Windows Reverse1
通过段名发现是UPX壳,upx -d脱壳后进行分析
核心函数只是通过data数组做一个转置,反求index即可
值得一说的是data的地址与实际数组有一些偏移
由于输入的可见字符最小下标就是空格的0x20,因此data这个地址实际上也是真正的表地址(0xb03018)-0x20=0xb02ff8,在实际反查的时候需要稍作处理
from ida_bytes import get_bytes
data = get_bytes(0xb03018,0xb03078-0xb03018)
r = "DDCTF{reverseME}"
s = ""
for i in range(len(r)):
s += chr(data.index(r[i])+0xb03018-0xb02ff8)
print(s)
Windows Reverse 2
用PEID查壳发现是aspack壳,直接运行程序,然后用调试器附加上去
通过调用堆栈来追溯到输入函数
具体方法为:
当程序运行到等待输入时,在调试器中按下暂停,然后选择主线程,观察调用堆栈(Stack Trace / Call Stack)窗口
可以看出调用堆栈中存在scanf函数,这是因为接收输入时程序阻塞,必然是在程序调用接收输入的函数过程中阻塞的。而根据栈帧的机制,函数返回值会被存放在各个栈帧中,所以scanf的上一个函数就是用户模块了。
双击即可跟进去,此时看到的是一片数据
这是因为代码由动态解密得到,IDA还没有对他们进行分析。
对着它们按C,即可将其作为Code识别,进行分析了
然后重新看调用堆栈窗口的具体地址即可找到目标
也可以直接在Options-general窗口中找到Reanalyse program
按钮进行重新分析
关键函数是sub_111f0和sub_11240
跟踪数据变化可以直接看出两个函数分别是hexdecode和base64encode
反向计算得到flag
from base64 import b64decode
print(b64decode(b"reverse+").hex().upper())
后来发现可以用看雪的脱壳工具脱壳,可读性强很多
Confused
macOS程序
Main函数中没有逻辑,所以通过字符串查找"DDCTF"取两次交叉引用,找到ViewController checkCode:
函数
前段主要是校验开头结尾的"DDCTF{}"标志,API的各种命名都很清晰,无需多言
关键部分在msgSend
“onSuccess"前的最后一个判断函数sub_1000011d0中
第一个调用sub_100001f60可以看出来是构造函数,将对象的各个成员进行初始化,并把输入即a2的18个字节拷贝到全局变量中
第二个调用sub_100001f00则开始虚拟机的执行循环,初始化IP寄存器后即不断向后执行,直到碰到结尾字节0xf3为止
loop函数内部很清晰,遍历对象的所有opcode与当前IP所指的值相比较,相等时即执行对应函数,函数内部负责后移IP,使VM执行下一条指令
平常做VM可以直接上angr、pintools或者记录运行log来方便地处理,但是本题是mac平台,由于钱包原因就只能乖乖静态逆了
首先整理字节码,位于0x100001984的地方
提出部分以作示例
0xf0, 0x10, 0x66, 0x0, 0x0, 0x0,
0xf8,
0xf2, 0x30,
0xf6, 0xc1,
0xf0, 0x10, 0x63, 0x0, 0x0, 0x0,
0xf8,
0xf2, 0x31,
0xf6, 0xb6,
第一个code是0xf0
,对应的handler为sub_100001d70
通过structures结构体功能可以将对象的成员重命名、整理结构成比较易读的形式
具体方法为在Structures窗口中按Insert创建结构体,按D创建成员
最后将a1的类型按y重定义成结构体指针vm*
即可
可以看出来f0根据第二个字节来决定将后4个字节的Int存入某个寄存器中,例如0x10表示a
然后是0xf8
,对应的handler为sub_100001C60
即对a寄存器的值在字母域内加2,相当于ROT2吧
以此类推,0xf2
是判断a寄存器内的值是否和全局变量中以后一个字节为偏移的值相等,实际上也就是刚才memcpy进来的input
0xf6
则是根据0xf2
判断的结果来决定是否跳转,下同循环
因此整个算法实际上就是逐字符判断定值+2是否与输入相等,只需要将code_array中的定值取出即可
例如一个正则表达式
code="""0xf0, 0x10, ... 略"""
import re
data = re.findall(r"\n?0xf0, \n?0x10, \n?0x(..)", code)
print(data)
for i in data:
v = int(i,16)+2
print(chr(v-26 if (v>ord('Z') and v<ord('a')) or v>ord('z') else v),end='')
obfuscating macros
本题是一个经过OLLVM混淆的较简单算法的程序
基础分析
主函数中通过cin接受输入,传入两个函数中处理后要求皆返回True
两个函数都被OLLVM了
第一个函数通过黑盒可以知道是HexDecode,输出仍保存在原来的位置里,若输入超出数字和大写ABCDEF则Decode失败返回False
第二个函数则是对输入进行了一些比较,输出比较的结果
有下列几种思路解题:
- 动态调试
- 单字节穷举
- 反混淆
动态调试
对于被控制流平坦化处理过的程序,执行流完全打散,所以很难知道各个代码块之间的关系,再加上虚假执行流会污染代码块,使得同一个真实块出现多次,难以分辨真实代码
因此在不deflat的情况下最好的办法就是单步执行慢慢跟,等到执行真实代码,尤其是一些运算的时候稍作注意
本题中通过这样的办法发现了这样一处代码
指针++后取值,很符合字符串的逐字符处理逻辑
点进去看一下可以发现正是hexdecode过后的输入,而v26与输入产生了联系,所以我们下一步要跟着v26的数据走
这里判断的v26的值是否为0,等价于cmp data,input; jz xxx;
因此可以知道要求第一个值为79,同理继续往下跟即可获得所有flag
另外快速一点的方法是使用断点脚本,在0x405fc6处下断并设置下述脚本,则会在output窗口打印出所需值
v = GetRegValue("ecx")
SetRegValue(v,"eax")
print("%X"%v)
另外算法实际上也并不复杂,简单来说是通过一个data数组和另一个数组异或产生的数据,前一个数组是逐个赋值的,所以并不好找出顺序来静态解出flag
单字节穷举
由于该程序的算法是逐字节校验,并且当某一个值错误时就会退出,因此可以应用pintools类的侧信道攻击
但并不能直接上轮子,因为在check的前面还有一个hexdecode,使得输入必须两个一组,并且由于数字和字母处理逻辑不同所以也会产生执行次数的跃变,要做特殊修正
首先字典调整成["%02x"%i for i in range(1,0x100]
然后要判断key中存在的字母个数,经测试发现每个字母大概会使运行次数增加1681-1683次,将误差消除后比较即可
大概在一小时左右可以得到flag,效率虽然比较感人,但优势在于期间不用关注该题,只等躺着拿flag就行了233
在之前的轮子上做了微调的脚本如下
#-*- coding:utf-8 -*-
import popen2,string
INFILE = "test"
CMD = "/root/pin/pin -t /root/pin/source/tools/ManualExamples/obj-intel64/inscount1.so -- /root/Project/obfuscating_macros.out <" + INFILE
#choices = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!#$%&'()*+,-./:;<=>?@[\]^_`{|}~"#自定义爆破字典顺序,将数字和小写字母提前可以使得速度快一些~
choices = ["%02X"%i for i in range(1,0x100)]
def execlCommand(command):
global f
fin,fout = popen2.popen2(</