彩笔来 buu 做题了 QAQ,每道做了的我都从不同角度为其添加了关键词,方便 ctrl + F 查找你感兴趣的内容(搜索时请勿区分大小写),持续更新中…
以下是本文涉及到的关键词:
架构:x86, JVM, PVM
文件类型:exe, elf, apk, MachO, pyc, javascript, sys
考点:upx, java层, base64, maze, 图形界面, DOS, patch, sha1, md5, elf运行流程, 花指令, RSA, 恺撒密码, native层, 变种base64, .NET, 混淆, tea, Unity游戏, 反调试, go语言, 固件分析, xxtea, SMC, AES_ECB, 二叉树, 数独, vm虚拟机, z3, angr, JsFuck混淆
PS. 用到的 python 脚本运行环境除特殊标注外均为 python2 ,如果内容有误或者图片缺失,欢迎联系我修正 。
剩下也没啥好说的了,送大家一首歌吧,歌名就叫 re
easyre |x86,exe shift + F12
reverse1 |x86,exe shift + F12:
查看交叉引用,找到关键函数 sub_1400118C0:
上面的 for 循环将 Str2 中的 o 都替换成 0
reverse2 |x86,elf 反编译 main:
fork 创建了一个子进程将 flag 中的 i 和 r 都替换成 1
内涵的软件 | x86,exe shift + F12
新年快乐 | x86,exe,upx exe 被 upx 压缩,到 网站 下载upx,使用命令 upx.exe -d xnkl.exe -o xnkl_unpack.exe
解压
ida 打开 xnkl_unpack.exe,找到 main 函数:
[BJDCTF 2nd]guessgame | x86,exe shift + F12
helloword | JVM,apk,java层 jadx 打开类 com.example.helloword.MainActivity:
xor | x86,MachO main 函数:
解密脚本:
1 2 3 4 5 6 7 8 rawData = "66 0A 6B 0C 77 26 4F 2E 40 11 78 0D 5A 3B 55 11 70 19 46 1F 76 22 4D 23 44 0E 67 06 68 0F 47 32 4F" .split(' ' ) target = [int (i, 16 ) for i in rawData] flag = [target[0 ]] + [0 ] * 32 for i in range (32 , 0 , -1 ): flag[i] = target[i] ^ target[i-1 ] print '' .join(map (chr , flag))
reverse3 | x86,exe,base64 找到 main_0 函数,根据函数行为还原部分变量及函数名:
解密脚本:
1 2 3 4 5 6 7 8 9 import base64rawData = "e3nifIH9b_C@n@dH" target = map (ord , rawData) for i in range (len (target)): target[i] -= i src = '' .join(map (chr , target)) print base64.b64decode(src)
不一样的flag | x86,exe,maze 迷宫题,main 函数:
一维化的迷宫是 *11110100001010000101111#,根据 main 函数的判断条件,可知二维状态下一行有 5 个元素,还原二维迷宫:
将 上下左右 映射到 1234 去就得到 flag 了
PS. 这道题出得不严谨,考虑这样一种情况:在一个地方反复先下后上(或者反复左右横跳),然后再走到终点,依然显示顺序正确
SimpleRev | x86,elf main 函数调用 Decry 函数,下面是对该函数的分析:
采用爆破的方式获得 flag,脚本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import stringkey = "ADSFKNDCLS" .lower() key = map (ord , key) text = "killshadow" text = map (ord , text) flag = '' charSet = string.uppercase + string.lowercase charSet = map (ord , charSet) for i in range (len (key)): for char in charSet: if text[i] == (char - 39 - key[i] + ord ('a' )) % 26 + ord ('a' ): flag += chr (char) break print flag
Java逆向解密 | JVM,java层 jadx 打开:
解密脚本:
1 2 3 4 5 flag = '' encrypt = [180 , 136 , 137 , 147 , 191 , 137 , 147 , 191 , 148 , 136 , 133 , 191 , 134 , 140 , 129 , 135 , 191 , 65 ] for e in encrypt: flag += chr ((e ^ 32 ) - ord ('@' )) print flag
刮开有奖 | x86,exe,图形界面,base64 ida 打开……好家伙,又逮到一个不用框架写图形化界面的,想办法干他一炮!
WinMain 中可以看到该程序就是弹一个对话框,对话框对应的函数是 DialogFunc:
DialogFunc 大致的逻辑就是从一个输入框中读取输入,经过一些操作后与特定的值比较:
但是运行起来发现找不到应有的输入框和按钮,只有一张图……当我 Resource Hacker 吃素的吗?不能惯着他,打开 RH,exe 拖进去,Dialog -> 103,弄他:
修改后:
File -> Save As,另存为 2.exe,再运行:
输入的问题解决了,接下来细看代码逻辑,遇到的第一个难关是 sub_4010F0:
这个函数看起来有点麻烦,还有递归的要素在里面,反正就是对 v7 开始的 10 个整数进行变换。两条路 —— 要么伪代码复制出来写个 C 跑一下,要么动态调试。这我能惯着 Visual Studio?上 OD
重定位一波 call sub_4010F0
指令的内存地址,下个断,输入框随便整个 8 位数。断下来在数据窗口中跟随 ecx :
步过,再看:
关闭 OD,继续怼 ida:
下面的判断语句中要求 v4 = "ak1w"
,v5 = "V1Ax"
,那 flag[2] ~ flag[7] 都知道了。再往下看:
现在的 v7 和 v11 经过 sub_4010F0 的变换已经变成了 ‘3’ 和 ‘J’,OK 游戏结束:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 from base64 import b64decodeflag = ['' ] * 8 v4 = "ak1w" v5 = "V1Ax" t1 = b64decode(v4) flag[5 ] = t1[0 ] flag[7 ] = t1[2 ] flag[6 ] = t1[1 ] t2 = b64decode(v5) flag[3 ] = t2[1 ] flag[2 ] = t2[0 ] flag[4 ] = t2[2 ] flag[0 ] = chr (ord ('3' ) + 34 ) flag[1 ] = 'J' print '' .join(flag)
芜湖🛫:
[BJDCTF 2nd]8086 | x86,exe,DOS MS DOS 的程序我是没想到的……ida 打开,因为一个 jmp 死循环导致 ida 不识别剩下的代码,强转一下:
逻辑很简单:
解密脚本:
1 2 3 4 5 6 7 encode = "]U[du~|t@{z@wj.}.~q@gjz{z@wzqW~/b;" encode = map (ord , encode) flag = [] for char in encode: flag.append(chr (char ^ 0x1F )) print '' .join(flag)
[GKCTF2020]Check_1n | x86,exe PS. 绝了这题作者,真有精力嗷,主办方是帮你把下个月的花呗还完了还是咋的……
开机让输密码,ida shift + F12,查找字符串 “密码错误” 的交叉引用,找到函数 sub_404DF0,往上找找看到密码 HelloWorld:
访问桌面上的 flag 文件,将得到的 base64 串解码,得到 Why don’t you try the magic brick game,那就玩一下打砖块,不用动等他死了上方就会出现 flag
findit | JVM,apk,java层 jadx 打开:
变量 b 拼接起来是 pvkq{m164675262033l4m49lnp7p9mnk28k75}
不用想了,无脑 凯撒解密 ,位移为 10
[GXYCTF2019]luck_guy | x86,elf,patch 关键函数 get_flag:
经过分析,得知当执行流依次经过 4、5、1 后会输出正确的 flag。两条路 —— 脚本/patch,那我能惯着 python 吗,patch 安排!
patch 点 1:先去到 case4
patch 点 2:接着执行 case5
patch 点 3:case5 结束后跳转到 case1
patch 点 4:case1 结束后退出函数
F5 一下可以看到,现在的执行流是正确的:
放到 linux 下去跑就能出 flag(等待输入那里输一个2就行)
简单注册器 | JVM,apk,java层 jadx 打开,逻辑很简单:
直接把生成 flag 的部分拿出来改一下吧:
1 2 3 4 5 6 7 8 9 10 11 12 13 x = "dd2940c04462b4dd7c450528835cca15" x = map (ord , x) x[2 ] = ((x[2 ] + x[3 ]) - 50 ) x[4 ] = ((x[2 ] + x[5 ]) - 48 ) x[30 ] = ((x[31 ] + x[9 ]) - 48 ) x[14 ] = ((x[27 ] + x[28 ]) - 97 ) for i in range (16 ): tmp = x[31 - i] x[31 - i] = x[i] x[i] = tmp flag = '' .join(map (chr , x)) print flag
rsa | rsa 额,纯密码学的题,走错分区了吧老哥,题解见 链接
[GWCTF 2019]pyre | PVM,pyc 用 uncompyle6 或者 在线网站 反编译 pyc 文件,得到 python 源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 print 'Welcome to Re World!' print 'Your input1 is your flag~' l = len (input1) for i in range (l): num = ((input1[i] + i) % 128 + 128 ) % 128 code += num for i in range (l - 1 ): code[i] = code[i] ^ code[i + 1 ] print codecode = ['\x1f' , '\x12' , '\x1d' , '(' , '0' , '4' , '\x01' , '\x06' , '\x14' , '4' , ',' , '\x1b' , 'U' , '?' , 'o' , '6' , '*' , ':' , '\x01' , 'D' , ';' , '%' , '\x13' ]
不想想了,爆破吧:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 code = ['\x1f' , '\x12' , '\x1d' , '(' , '0' , '4' , '\x01' , '\x06' , '\x14' , '4' , ',' , '\x1b' , 'U' , '?' , 'o' , '6' , '*' , ':' , '\x01' , 'D' , ';' , '%' , '\x13' ] code = map (ord , code) for i in range (len (code) - 2 , -1 , -1 ): code[i] ^= code[i+1 ] flag = '' for i in range (len (code)): for j in range (33 , 127 ): if code[i] == ((j + i) % 128 + 128 ) % 128 : flag += chr (j) break print flag
CrackRTF | x86,exe,sha1,md5 main_0 函数检查第一个输入的部分:
如何判断 sub_40100A 是 sha1 哈希函数呢,点进去发现它调用 sub_401230,继续跟进:
根据 windows API 的尿性,CryptCreateHash 函数的第二个参数应该用来指定 hash 的类型。在 MSDN 文档 中查找该函数:
点 ALG_ID 进去查找 0x8004:
那密码 1 就能得到了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import hashlibdef sha1Encode (plain ): sha = hashlib.sha1(plain.encode('utf-8' )) return sha.hexdigest().upper() passwd1 = '' for i in range (100000 , 1000000 ): plain = str (i) + "@DBApp" if sha1Encode(plain) == "6E32D0943418C2C33385BC35A1470250DD8923A9" : passwd1 = str (i) break print passwd1
第二次输入检查:
同理可以确定 sub_401019 为 md5 哈希函数:
这次密码就有点难爆了,不像密码 1 暗示你它是一个 6 位整数,这里需要考虑所有可打印字符。使用脚本可能会爆很久,所以我选择使用工具 hashcat 来爆破,得到密码 2 为 ~!3a@0
乖乖将这俩密码依次输入程序可以得到一个 dbapp.rtf,打开就是 flag
[BJDCTF2020]JustRE | x86,exe,图形界面 又是个不用框架写图形界面的,WinMain 里调用 sub_4010C0 注册 RE2 类,对应的窗体函数是 sub_4011C0。
Resource Hacker 查看 “测试你的手速” 对话框的资源号为 129,在 sub_4011C0 中找到对应的处理函数是 DialogFunc:
查看 DialogFunc:
[2019红帽杯]easyRE | x86,elf,elf运行流程 shift + F12,查找 “You found me!!!” 的交叉引用,找到 sub_4009C6。第一部分:
脚本:
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 target = [0 ] * 36 target[0 ] = 73 target[1 ] = 111 target[2 ] = 100 target[3 ] = 108 target[4 ] = 62 target[5 ] = 81 target[6 ] = 110 target[7 ] = 98 target[8 ] = 40 target[9 ] = 111 target[10 ] = 99 target[11 ] = 121 target[12 ] = 127 target[13 ] = 121 target[14 ] = 46 target[15 ] = 105 target[16 ] = 127 target[17 ] = 100 target[18 ] = 96 target[19 ] = 51 target[20 ] = 119 target[21 ] = 125 target[22 ] = 119 target[23 ] = 101 target[24 ] = 107 target[25 ] = 57 target[26 ] = 123 target[27 ] = 105 target[28 ] = 121 target[29 ] = 61 target[30 ] = 126 target[31 ] = 121 target[32 ] = 76 target[33 ] = 64 target[34 ] = 69 target[35 ] = 67 input1 = '' for idx, value in enumerate (target): input1 += chr (idx ^ value) print input1
得到的输出是 Info:The first four chars are `flag`。第二部分:
脚本:
1 2 3 4 5 6 7 import base64target = "Vm0wd2VHUXhTWGhpUm1SWVYwZDRWVll3Wkc5WFJsbDNXa1pPVlUxV2NIcFhhMk0xVmpKS1NHVkdXbFpOYmtKVVZtcEtTMUl5VGtsaVJtUk9ZV3hhZVZadGVHdFRNVTVYVW01T2FGSnRVbGhhVjNoaFZWWmtWMXBFVWxSTmJFcElWbTAxVDJGV1NuTlhia0pXWWxob1dGUnJXbXRXTVZaeVdrWm9hVlpyV1hwV1IzaGhXVmRHVjFOdVVsWmlhMHBZV1ZSR1lWZEdVbFZTYlhSWFRWWndNRlZ0TVc5VWJGcFZWbXR3VjJKSFVYZFdha1pXWlZaT2NtRkhhRk5pVjJoWVYxZDBhMVV3TlhOalJscFlZbGhTY1ZsclduZGxiR1J5VmxSR1ZXSlZjRWhaTUZKaFZqSktWVkZZYUZkV1JWcFlWV3BHYTFkWFRrZFRiV3hvVFVoQ1dsWXhaRFJpTWtsM1RVaG9hbEpYYUhOVmJUVkRZekZhY1ZKcmRGTk5Wa3A2VjJ0U1ExWlhTbFpqUldoYVRVWndkbFpxUmtwbGJVWklZVVprYUdFeGNHOVhXSEJIWkRGS2RGSnJhR2hTYXpWdlZGVm9RMlJzV25STldHUlZUVlpXTlZadE5VOVdiVXBJVld4c1dtSllUWGhXTUZwell6RmFkRkpzVWxOaVNFSktWa1phVTFFeFduUlRhMlJxVWxad1YxWnRlRXRXTVZaSFVsUnNVVlZVTURrPQ==" for i in range (10 ): target = base64.b64decode(target) print target
得到输出 [原创]看雪CTF从入门到存活(六)主动防御 ,访问该网站可以在评论区找到其他选手发的 flag。不过出题人的本意不是这样的,在 elf 中可以找到计算 flag 的函数。该函数在 main 函数运行结束后的 __libc_csu_fini 函数中被调用。
众 所 周 知,elf 从 start 函数开始执行(由 Entry point 指定),然后执行顺序是 __libc_start_main -> __libc_csu_init -> _init_array 中的函数 -> main -> __libc_csu_fini -> _fini_array 中的函数。
这道题的 start 函数:
_init_array 地址可以在 __libc_csu_init 函数中找到:
翻一翻 _init_array 和 _fini_array 中的函数,每个都点过去 F5 看一下:
_fini_array 的第二个函数指针 sub_400D35 最为可疑:
结合之前得到的信息 Info:The first four chars are `flag`,不难推测 v8 这个四字节变量的值要拿 byte_6CC0A0 的前四字节来与 “flag” 逐字节异或。解密脚本:
1 2 3 4 5 6 7 8 9 10 11 12 target = "40 35 20 56 5D 18 22 45 17 2F 24 6E 62 3C 27 54 48 6C 24 6E 72 3C 32 45 5B" .split(' ' ) target = [int (i, 16 ) for i in target] xor = map (ord , "flag" ) key = [0 ] * 4 for i in range (4 ): key[i] = xor[i] ^ target[i] flag = '' for i in range (len (target)): flag += chr (key[i%4 ] ^ target[i]) print flag
Youngter-drive | x86,exe,upx,花指令 PS. 这题出的有问题,off_418004 字符数组长度应该为 30,少的最后那个字符是 ‘y’
先 upx.exe -d
对 exe 进行解压,ida 打开,发现这玩意儿还整了个 TLS 回调来检测调试器,我想说原 exe 我都运行不起来……看看 main_0 函数的逻辑吧:
StartAddress 函数:
sub_41119F 函数:
为啥上面两张图片的分辨率不一样呢?那是因为亲爱的学校又双叒叕停电了,台式机直接暴毙,只能换成笔记本,我 tm***
从这俩函数的行为可以判断出 StartAddress 与 sub_41119F 将会交替执行,且线程 1 会先拿到锁(信号量与锁机制)。再来研究一下 StartAddress 中调用的 sub_41112C,这个函数又调用了 sub_411940,而 sub_411940 因为添加了花指令导致 ida 无法反编译:
找找是哪里出问题了,Options -> General 打开栈指针、显示字节码:
找到导致 ida 栈跟踪分析出错的花指令:
将 add esp, 8
nop 掉就可以愉快 F5 了(当然也可以 Alt + K 调整栈指针达到栈平衡):
反编译:
这个函数干的事情就是把输入的大写字母替换成 off_418000[0] 字符数组中的小写字母,把输入的小写字母替换成 off_418000[0] 字符数组中的大写字母,而且是隔一个字符处理一次(上面分析过了)
最后在 sub_411190 -> sub_411880 函数中判断处理后的输入与 off_418004 是否逐字节相等,然而这里题目出错了,off_418004 字符数组长度应该为 30,少的那个字符是 ‘y’,上脚本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 target = "TOiZiZtOrYaToUwPnToBsOaOapsyS" + 'y' table = "QWERTYUIOPASDFGHJKLZXCVBNMqwertyuiopasdfghjklzxcvbnm" target = map (ord , target) table = map (ord , table) flag = ['' ] * len (target) for i in range (len (target)-1 , -1 , -1 ): if i % 2 : if target[i] < 97 : flag[i] = chr (table.index(target[i]) + 96 ) else : flag[i] = chr (table.index(target[i]) + 38 ) else : flag[i] = chr (target[i]) print '' .join(flag)
[ACTF新生赛2020]easyre | x86,exe,upx upx.exe -d
对 exe 进行解压,ida 打开找到 main 函数,逻辑很简单:
脚本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 table = "7E 7D 7C 7B 7A 79 78 77 76 75 74 73 72 71 70 6F 6E 6D 6C 6B 6A 69 68 67 66 65 64 63 62 61 60 5F 5E 5D 5C 5B 5A 59 58 57 56 55 54 53 52 51 50 4F 4E 4D 4C 4B 4A 49 48 47 46 45 44 43 42 41 40 3F 3E 3D 3C 3B 3A 39 38 37 36 35 34 33 32 31 30 2F 2E 2D 2C 2B 2A 29 28 27 26 25 24 23 20 21 22 00" .split(' ' ) table = [int (i, 16 ) for i in table] key = [0 ] * 12 key[0 ] = 42 key[1 ] = 70 key[2 ] = 39 key[3 ] = 34 key[4 ] = 78 key[5 ] = 44 key[6 ] = 34 key[7 ] = 40 key[8 ] = 73 key[9 ] = 63 key[10 ] = 43 key[11 ] = 64 flag = list ("ACTF{" ) + ['' ] * 12 + ['}' ] for i in range (12 ): flag[5 +i] = chr (table.index(key[i]) + 1 ) print '' .join(flag)
相册 | JVM,apk,native层 Android Killer 打开,得知入口类是 cn.baidujiayuan.ver5304.C1:
jadx 打开看看该类的 onCreate 方法:
A2、M2、C2 这几个类都比较可疑,题目让找 email 地址,就 C2 沾点边了:
右键跳到 NativeMethod 的声明处:
都是 native 层的方法,那就把 so 库拿出来(apk 后缀改成 zip,解压后 so 库在 lib 目录下),这里只有一个 arm 32 位的 libcore.so。ida 打开,找到方法 Java_com_net_cn_NativeMethod_m(JNI 开发编译时会在方法名前加上类的路径),该方法返回一个 base64 字符串:
base64 解码一下就是 flag
[SUCTF2019]SignIn | x86,elf,RSA main 函数逻辑:
用到了一些 gmp 大数运算的函数:
gmpz_init_set_str(buffer, str, radix) :将进制为 radix 的 str 表示的数存到 buffer 中去
gmpz_powm(x, y, z):return x**y % z
经典 RSA 加密,把 mod 分解成 p 和 q,用 gmpy2 库解得明文 output(python3 运行 )
1 2 3 4 5 6 7 8 9 10 11 12 13 import gmpy2e = 65537 p = 282164587459512124844245113950593348271 q = 366669102002966856876605669837014229419 phi = (p - 1 ) * (q - 1 ) d = gmpy2.invert(e, phi) c = int ("ad939ff59f6e70bcbfad406f2494993757eee98b91bc244184a377520d06fc35" , 16 ) plain = pow (c, d, p * q) output = "%x" % plain print (output)
output 是经过 Encode 函数编码过的,该函数每次取来输入的一个字节,高 4 bit 和低 4 bit 分别到 byte_202010 字符数组中去映射为新的字符,将输入的 32 字节扩展为 64 字节的 output:
逆回去:
1 2 3 4 5 6 7 8 9 10 11 output = "73756374667b50776e5f405f68756e647265645f79656172737d" flag = ['' ] * (len (output) // 2 ) mapTable = "30 31 32 33 34 35 36 37 38 39 61 62 63 64 65 66" .split(' ' ) mapTable = [chr (int (i, 16 )) for i in mapTable] for i in range (0 , len (output), 2 ): high4Bit = mapTable.index(output[i]) low4Bit = mapTable.index(output[i+1 ]) curByte = high4Bit << 4 | low4Bit flag[i//2 ] = chr (curByte) print ('' .join(flag))
[ACTF新生赛2020]rome | x86,exe,恺撒密码 恺撒密码,逻辑简单:
脚本:
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 import stringtarget = [0 ] * 16 target[0 ] = 81 target[1 ] = 115 target[2 ] = 119 target[3 ] = 51 target[4 ] = 115 target[5 ] = 106 target[6 ] = 95 target[7 ] = 108 target[8 ] = 122 target[9 ] = 52 target[10 ] = 95 target[11 ] = 85 target[12 ] = 106 target[13 ] = 119 target[14 ] = 64 target[15 ] = 108 flag = list ("ACTF{" ) + ['' ] * 16 + ['}' ] for i in range (16 ): if ord ('A' ) <= target[i] <= ord ('Z' ): for j in string.uppercase: if target[i] == (ord (j) - 51 ) % 26 + ord ('A' ): flag[i+5 ] = j break elif ord ('a' ) <= target[i] <= ord ('z' ): for j in string.lowercase: if target[i] == (ord (j) - 79 ) % 26 + ord ('a' ): flag[i+5 ] = j break else : flag[i+5 ] = chr (target[i]) print '' .join(flag)
[GUET-CTF2019]re | x86,elf,upx PS. 题目有误,flag[6] = ‘1’
upx 解压以后,ida 打开,shift + F12 查找 “input your flag” 字符串的交叉引用,找到 main 函数(或者可以通过 start 函数直接找),逻辑很简单:
Check 函数对输入的每一字节进行检查,除回去就能得到 flag 了,这里题目少了一个条件导致 flag[6] 无法确定,经尝试后 flag[6] = ‘1’:
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 flag = [0 ] * 32 flag[0 ] = 166163712 / 1629056 flag[1 ] = 731332800 / 6771600 flag[2 ] = 357245568 / 3682944 flag[3 ] = 1074393000 / 10431000 flag[4 ] = 489211344 / 3977328 flag[5 ] = 518971936 / 5138336 flag[6 ] = ord ('1' ) flag[7 ] = 406741500 / 7532250 flag[8 ] = 294236496 / 5551632 flag[9 ] = 177305856 / 3409728 flag[10 ] = 650683500 / 13013670 flag[11 ] = 298351053 / 6088797 flag[12 ] = 386348487 / 7884663 flag[13 ] = 438258597 / 8944053 flag[14 ] = 249527520 / 5198490 flag[15 ] = 445362764 / 4544518 flag[17 ] = 174988800 / 3645600 flag[16 ] = 981182160 / 10115280 flag[18 ] = 493042704 / 9667504 flag[19 ] = 257493600 / 5364450 flag[20 ] = 767478780 / 13464540 flag[21 ] = 312840624 / 5488432 flag[22 ] = 1404511500 / 14479500 flag[23 ] = 316139670 / 6451830 flag[24 ] = 619005024 / 6252576 flag[25 ] = 372641472 / 7763364 flag[26 ] = 373693320 / 7327320 flag[27 ] = 498266640 / 8741520 flag[28 ] = 452465676 / 8871876 flag[29 ] = 208422720 / 4086720 flag[30 ] = 515592000 / 9374400 flag[31 ] = 719890500 / 5759124 flag = map (chr , flag) print '' .join(flag)
[FlareOn4]login | javascript 又是一个走错片场的题目,文本编辑器打开 html,可以看到一段 javascript 写的检查 flag 的代码:
1 2 3 4 5 6 7 8 9 document .getElementById("prompt" ).onclick = function ( ) { var flag = document .getElementById("flag" ).value; var rotFlag = flag.replace(/[a-zA-Z]/g , function (c ) {return String .fromCharCode((c <= "Z" ? 90 : 122 ) >= (c = c.charCodeAt(0 ) + 13 ) ? c : c - 26 );}); if ("PyvragFvqrYbtvafNerRnfl@syner-ba.pbz" == rotFlag) { alert("Correct flag!" ); } else { alert("Incorrect flag, rot again" ); } }
用正则表达式来匹配所有大小写,将其 “右移” 13 位,妥妥的恺撒加密,懒得写脚本了,上 网站
[V&N2020 公开赛]strangeCpp | x86,exe shift +F12,查找字符串 “flag{…}” 的交叉引用,找到函数 sub_140013AA0,开头有一串 putchar 输出 welcome 的地方:
随便点一个 byte_… 跟过去,可以发现一个长度为 17 的可疑字符数组:
查找交叉引用,找到关键函数 sub_140013580:
通过 result == 607052314
,与逆向 sub_140011384 函数可以确定 key 的值,sub_140011384 逻辑如下:
爆破呗,因为 python 的 int 类型没有位数限制,在左移 8 位和乘 291 时都不存在截断和溢出,所以需要手动给他截断一下(& 0xFFFFFFFF):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 key = 0 for i in range (14549744 ): if ((i << 8 ) ^ (i >> 12 )) * 291 & 0xFFFFFFFF == 607052314 : key = i print "Key: " + str (key) break table = "26 2C 21 27 3B 0D 04 75 68 34 28 25 0E 35 2D 69 3D" .split(' ' ) table = [int (i, 16 ) for i in table] flag = ['' ] * len (table) for i in range (len (table)): flag[i] = chr (table[i] ^ key & 0xFF ) print '' .join(flag)
输出为
Key: 123456
flag{MD5(theNum)}
md5 一下 key 就是 flag 了
[BJDCTF2020]easy | x86,exe,patch main 函数啥事没干,函数列表里翻一翻,找到一个可疑函数 ques。分析后发现该函数不要求输入,却在不停输出,那就在 main 里 patch 一下,让执行流顺路执行该函数:
运行一下得到 flag:
不 patch 也是可以的,exe 也没有开基址重定位,OD 动态调试时直接修改 eip 到该函数即可
[ACTF新生赛2020]usualCrypt | x86,exe,变种base64 main 函数逻辑很简单,就是将输入丢到 sub_401080 函数处理,结果与 byte_40E0E4 字符数组逐字节比较。
sub_401080 函数主体是个 base64 编码,这一点从编码表 byte_40E0A0 不难看出。但是在编码开始前,该函数调用了 sub_401000 对编码表进行了变换,OD 可以调出来:
编码结束后,调用 sub_401030 对编码的结果进行大小写的翻转:
脚本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import base64old_table = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" new_table = "ABCDEFQRSTUVWXYPGHIJKLMNOZabcdefghijklmnopqrstuvwxyz0123456789+/" target = "zMXHz3TIgnxLxJhFAdtZn2fFk3lYCrtPC2l9" res = '' for i in target: if 'a' <= i <= 'z' : tmp = ord (i) - 32 elif 'A' <= i <= 'Z' : tmp = ord (i) + 32 else : tmp = ord (i) res += chr (tmp) general = '' for i in res: general += old_table[new_table.find(i)] print base64.b64decode(general)
[V&N2020 公开赛]CSRe | x86,exe,.NET,混淆 怎么看出有混淆的呢?用 Reflector 打开后发现类名都是不可见字符,还有一堆莫名其妙的计算函数,经验告诉我它被混淆了,当然也可以用诸如 Exeinfo PE 这样的工具来检查
.NET 框架的程序去混淆需要用到 de4dot ,你可以在 github 下载源码来编译最新版,也可以下载别人已经编译好的 稍旧一些的版本 ,使用方法就是 de4dot.exe xxx.exe
,会在当前目录下生成 xxx-cleaned.exe
去混淆后再用 Reflector 打开,找到 main 函数:
两个 sha1 拿去网站一查,一个是 314159,另一个是 return,所以 str 和 str2 分别是 1415 和 turn
main 逻辑挺简单的,直接上脚本吧:
1 2 3 4 5 6 7 8 target = "67 79 7B 7F 75 2B 3C 52 53 79 57 5E 5D 42 7B 2D 2A 66 42 7E 4C 57 79 41 6B 7E 65 3C 5C 45 6F 62 4D" .split(' ' ) target = [int (i, 16 ) for i in target] table = [9 , 10 , 15 , 23 , 7 , 24 , 12 , 6 , 1 , 16 , 3 , 17 , 32 , 29 , 11 , 30 , 27 , 22 , 4 , 13 , 19 , 20 , 21 , 2 , 25 , 5 , 31 , 8 , 18 , 26 , 28 , 14 , 0 ] flag = [0 ] * 33 for i in range (33 ): flag[table[i]] = table[i] ^ target[i] print '' .join(map (chr , flag))
[GWCTF 2019]xxor | x86,elf,tea main 函数分析并还原符号:
普通 tea 解密很简单,完全反过来就行(C++ 32位):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 void TeaDecode (unsigned * cipher, unsigned * key) { int sum{ 0x458BCD42 * 64 }; unsigned lowWord = *cipher; unsigned highWord = cipher[1 ]; for (int i = 0 ; i <= 63 ; ++i) { highWord -= (lowWord + sum + 20 ) ^ ((lowWord << 6 ) + key[2 ]) ^ ((lowWord >> 9 ) + key[3 ]) ^ 0x10 ; lowWord -= (highWord + sum + 11 ) ^ ((highWord << 6 ) + *key) ^ ((highWord >> 9 ) + key[1 ]) ^ 0x20 ; sum -= 0x458BCD42 ; } *cipher = lowWord; cipher[1 ] = highWord; }
Check 函数解一个三元一次方程组(注意溢出与截断):
有:
target[2] = 3774025685
target[3] = 1548802262
target[4] = 2652626477
脚本(C++ 32位):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include <iostream> int main () { unsigned target[6 ]; target[2 ] = 3774025685 ; target[3 ] = 1548802262 ; target[4 ] = 2652626477 ; target[0 ] = 0xDF48EF7E ; target[5 ] = 0x84F30420 ; target[1 ] = 0x20CAACF4 ; unsigned key[]{ 2 ,2 ,3 ,4 }; for (int i = 0 ; i <= 4 ; i += 2 ) { TeaDecode (target + i, key); } for (unsigned i : target) { std::cout << std::hex << i; } }
输出为 666c61677b72655f69735f6772656174217d,python2 中 decode(“hex”) 一下得到 flag
[HDCTF2019]Maze | x86,exe,upx,maze upx 解压缩后,main 函数由于存在花指令,导致 ida 无法解析:
Undefine 以后将 0xE8 nop 掉
在 0x90 处按 ‘c’ 将数据强转成代码,之后在 main 函数开始处按 ‘p’ 形成函数,就可以 F5 了:
反编译:
由于没有对迷宫的点是否可以通行进行判断,所以存在很多种路径都输出答案正确的情况
shift + F12 可以找到长度为 70 的迷宫:
当顶边 * 侧边为 10 * 7 时,满足终点在 (5,-4) 的要求,还原二维迷宫:
所以正确的路径为 ssaaasaassdddw
[BJDCTF2020]BJD hamburger competition | x86,exe,.NET,Unity游戏 作者脑洞可以嗷。玩过 Unity 游戏开发的应该清楚,生成游戏时,开发者写的所有 C# 类会被整合编译到 Assembly-CSharp.dll 里,该动态链接库所在的路径是 xxx_Data\Managed,其中 xxx 为游戏名。
这题的 dll 就在 BJD hamburger competition_Data\Managed 目录下,Reflector 打开该 dll,在 ButtonSpawnFruit 类中找到 Spawn 方法:
网站上查下这个 sha1,得到 1001,先看看他写的 Md5:
那就取 1001 的 md5 大写前 20 个字符作为 flag
[WUSTCTF2020]level1 | x86,elf 1 2 3 4 5 6 7 8 9 10 11 12 with open ("output.txt" ) as f: data = f.read().split('\n' )[:-1 ] data = map (int , data) data = [0 ] + data flag = '' for i in range (1 , 20 ): if i & 1 : flag += chr (data[i] >> i) else : flag += chr (data[i] / i) print flag
[WUSTCTF2020]level2 | x86,elf,upx upx 解压缩后,main 函数的汇编中可以看到 flag
crackMe | x86,exe,反调试 整理一下 main 函数的逻辑,根据各变量、函数行为给他们加符号:
CheckValid 函数基本没啥用,用来检测输入长度和输入字符的合法性,PreparePrint 函数准备输出,Congratulation 存放在 v7,失败的字符串存放在 v5。PrepareTable 函数通过输入的 user 来变换加密表,该函数可以通过 python 模拟:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 def PrepareTable (user ): user = map (ord , user) table = range (256 ) v5, v9, v6 = 0 , 0 , 0 while v5 < 256 : v3 = table[v5] v9 = (v9 + v3 + user[v6 % len (user)]) & 0xFF v4 = table[v9] table[v9] = v3 table[v5] = v4 v5 += 1 v6 += 1 return table userName = "welcomebeijing" table = PrepareTable(userName)
CheckPassword 函数里面有三处反调试,检测到有调试痕迹时,会进入误导的分支:
把它们 patch 掉以后,先来看上面的 while 循环:
好麻烦,不想静态分析了,上 OD 吧,密码一栏输 1234abcdABCD,等 while 跳出看看 v15 里的值:
可以看到 12 个字符变成了 6 个字节,每两个字符凑成一个字节。下一个 while 循环:
其中 sub_401710 函数经过动态调试后发现不会对 target 数组或者 user 产生任何影响,该循环干的事就是给长度为 8 的 target 数组赋值,每次赋值一字节,根据上一个 while 循环可以推出输入的密码长度为 16。
最后的 sub_401470 函数用来检查 target 数组是否满足要求:
该函数对 target 数组进行逐字节检查:
需要注意,在检查 target[5] 时存在反调试的混淆信息。由此函数可得 target = [100, 98, 97, 112, 112, 115, 101, 99],再逆一下第二个 while 循环即可得到 password,完整脚本:
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 def PrepareTable (user ): user = map (ord , user) table = range (256 ) v5, v9, v6 = 0 , 0 , 0 while v5 < 256 : v3 = table[v5] v9 = (v9 + v3 + user[v6 % len (user)]) & 0xFF v4 = table[v9] table[v9] = v3 table[v5] = v4 v5 += 1 v6 += 1 return table userName = "welcomebeijing" table = PrepareTable(userName) target = [100 , 98 , 97 , 112 , 112 , 115 , 101 , 99 ] password = '' v12, v13, v3, v4 = 0 , 0 , 0 , 0 for i in range (8 ): v13 += 1 v12 = (v12 + table[v13]) & 0xFF v3 = table[v13] v4 = table[v12] table[v12] = v3 table[v13] = v4 password += "%02x" % (target[i] ^ table[(v3 + v4) & 0xFF ]) print password
[FlareOn4]IgniteMe | x86,exe 莫得 main 函数,所有事情都在 start 函数做完,根据函数行为补一下符号:
GetInput 函数:
CheckInput:
sub_401000 函数返回一个 key 用于下面异或,该值可以静态分析,也可以动态调试,exe 没有开重定位。逻辑都很简单,直接上脚本:
1 2 3 4 5 6 7 8 9 target = "0D 26 49 45 2A 17 78 44 2B 6C 5D 5E 45 12 2F 17 2B 44 6F 6E 56 09 5F 45 47 73 26 0A 0D 13 17 48 42 01 40 4D 0C 02 69" .split(' ' ) target = [int (i, 16 ) for i in target] flag = [0 ] * 0x27 key = 0x700004 & 0xFF for i in range (len (target)-1 , -1 , -1 ): flag[i] = target[i] ^ key key = flag[i] print '' .join(map (chr , flag))
[MRCTF2020]Xor | x86,exe shift + F12,查找字符串 “Give Me Your Flag String” 的交叉引用,找到 sub_401090,其实这就是 main 函数。F5 发现报错 —— call analysis failed,这个错一般是 ida 无法正确识别 函数参数个数 或 函数调用约定 导致的。尤其是调用如 printf、scanf 这些参数个数可变的函数函数时,ida 会出现这样的错误。
出现这种情况需要看 output window ,通过 log 信息知道是哪句调用分析失败了:
这里是地址 0x401095 处的调用分析失败了,此地址处调用了 sub_401020:
双击跟过去 F5 一下,让 ida 睁大眼睛好好看看这个函数,再回到 sub_401090,此时就可以 F5 了。其他题目如果做到这步还不能 F5,就需要人工干预,将光标放在函数头的位置按 ‘Y’ 强行修改参数个数或参数类型
F5 后事情就变得简单了,直接贴脚本了:
1 2 3 4 5 6 7 target = "4D 53 41 57 42 7E 46 58 5A 3A 4A 3A 60 74 51 4A 22 4E 40 20 62 70 64 64 7D 38 67" .split(' ' ) target = [int (i, 16 ) for i in target] flag = '' for i in range (len (target)): flag += chr (i ^ target[i]) print flag
[GKCTF2020]BabyDriver | x86,sys,maze 拿到手是个驱动,想运行起来的话需要改注册表并把文件放到系统文件夹,然后重启……还好静态分析起来不难
shift + F12 查找字符串 “success! flag is flag{md5(input)}” 的交叉引用,找到函数 sub_140001380。经过分析补上一些符号:
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 __int64 __fastcall sub_140001380 (__int64 a1, __int64 a2) { __int64 v2; __int64 v3; __int64 v4; int currentPos; __int16 *v6; __int64 v7; __int16 v8; char v9; CHAR *v10; v2 = a2; if ( *(a2 + 48 ) >= 0 ) { v3 = *(a2 + 24 ); v4 = *(a2 + 56 ) >> 3 ; if ( v4 ) { currentPos = Position; v6 = (v3 + 2 ); v7 = v4; while ( *(v3 + 4 ) ) { LABEL_28: v6 += 6 ; if ( !--v7 ) goto LABEL_29; } maze[currentPos] = '.' ; v8 = *v6; if ( *v6 == 23 ) { if ( currentPos & 0xFFFFFFF0 ) { currentPos -= 16 ; goto LABEL_21; } currentPos += 208 ; Position = currentPos; } if ( v8 == 37 ) { if ( (currentPos & 0xFFFFFFF0 ) != 208 ) { currentPos += 16 ; goto LABEL_21; } currentPos -= 208 ; Position = currentPos; } if ( v8 == 36 ) { if ( currentPos & 0xF ) { --currentPos; goto LABEL_21; } currentPos += 15 ; Position = currentPos; } if ( v8 != 38 ) goto LABEL_22; if ( (currentPos & 0xF ) == 15 ) currentPos -= 15 ; else ++currentPos; LABEL_21: Position = currentPos; LABEL_22: v9 = maze[currentPos]; if ( v9 == '*' ) { v10 = "failed!\n" ; } else { if ( v9 != '#' ) { LABEL_27: maze[currentPos] = 'o' ; goto LABEL_28; } v10 = "success! flag is flag{md5(v3)}\n" ; } Position = 16 ; DbgPrint(v10); currentPos = Position; goto LABEL_27; } } LABEL_29: if ( *(v2 + 65 ) ) *(*(v2 + 184 ) + 3 i64) |= 1u ; return *(v2 + 48 ); }
实现的功能是监听键盘按键,从而控制目标在迷宫中的移动,maze 拿出来处理一下:
需要注意的是,驱动判断按键用的是键盘扫描码,可以在 网站 查询,37 和 38 分别对应 K 和 L,将之前的路径替换一下,再 md5:
1 2 3 4 5 6 7 import hashlibpath = "RDDDRRDRDDDRRRDDDRRRRRR" path = path.replace('R' , 'L' ) path = path.replace('D' , 'K' ) md5 = hashlib.md5(path) print md5.hexdigest()
firmware | 固件分析,upx 拿到一个路由器固件,binwalk -e
分离得到一个压缩的文件系统 120200.squashfs :
需要用到 firmware-mod-kit 解压,安装方法:
1 2 3 4 5 sudo apt-get install git build-essential zlib1g-dev liblzma-dev python-magic git clone https://github.com/mirror/firmware-mod-kit.git cd firmware-mod-kit/src./configure && make
使用指令 ./unsquashfs_all.sh 120200.squashfs
解压文件系统,得到文件夹 squashfs-root ,翻一下里面的文件,在 tmp 目录下发现可执行文件 backdoor,且被 upx 压缩了。upx 解压后 ida 打开,找到 initConnection 函数:
commServer 是 echo.byethost51.com,端口为 36667
[MRCTF2020]hello_world_go | x86,elf,go语言 main.main 函数:
unk_4D3C58 即为 flag
[FlareOn6]Overlong | x86,exe,patch start 函数里弹了一个对话框,对话框的内容由 PrepareText 函数处理:
将第三个参数 patch 成 127 再运行就能得到 flag(或者动态调试把传参修改一下)
[WUSTCTF2020]level3 | x86,elf,elf运行流程 main 函数逻辑清晰,要求对 d2G0ZjLwHjS7DmOzZAY0X2lzX3CoZV9zdNOydO9vZl9yZXZlcnGlfD== 进行 base64 解密。但是在 main 函数执行前,base 编码表被函数 O_OLookAtYou 做了手脚,该函数在 _init_array 数组中,会在 main 函数之前被执行:
elf 运行流程详见本文 [2019红帽杯]easyRE writeup
动态调试将新的编码表拿出来:
脚本:
1 2 3 4 5 6 7 8 9 10 import base64old_table = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=" new_table = "TSRQPONMLKJIHGFEDCBAUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=" general = '' target = "d2G0ZjLwHjS7DmOzZAY0X2lzX3CoZV9zdNOydO9vZl9yZXZlcnGlfD==" for i in target : general += old_table[new_table.find(i)] print base64.b64decode(general)
[WUSTCTF2020]Cr0ssfun | x86,elf check 函数对 flag 的每个字符进行判断,提出来拼在一起就可以了,使用 sublime 的正则替换功能会高效很多
[GXYCTF2019]simple CPP | x86,exe,反调试 晚点再写~
[2019红帽杯]xx | x86,exe,xxtea PS. 本题所有脚本语言环境为 C/C++
sub_1400011A0 是 main 函数(通过字符串交叉引用找到),干了几件事:
输入 19 个字符
前 4 个字符作为 key
调用 sub_140001AB0 函数(xxtea_encode)加密输入,密钥为 key,加密结果记为 r1
轮换加密 r1,加密结果记为 r2
for 循环从 r2 第三个字符开始逐字节异或加密 r2,加密结果记为 r3
r3 应该与一个长度为 24 字节的数组逐字节相等,输出 You win
r2 加密到 r3 的逻辑用 C 可以写作:
1 2 3 4 5 6 7 8 9 10 11 12 13 void Encode3 (char * input) { for (int i = 1 ; i < 24 ; i++) { int idx = 0 ; if (i / 3 > 0 ) { char tmp = input[i]; do { tmp ^= input[idx++]; } while (idx < i / 3 ); input[i] = tmp; } } }
故r3 还原 r2:
1 2 3 4 5 6 7 8 9 10 11 12 13 void Decode3 (char * input) { for (int i = 23 ; i > 0 ; i--) { int idx = i / 3 - 1 ; if (i / 3 > 0 ) { char tmp = input[i]; do { tmp ^= input[idx--]; } while (idx >= 0 ); input[i] = tmp; } } }
r1 到 r2 的轮换加密 F5 反编译得很清楚,下面是 r2 还原 r1:
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 void Decode2 (char * cipher, char * plain) { plain[2 ] = cipher[0 ]; plain[0 ] = cipher[1 ]; plain[3 ] = cipher[2 ]; plain[1 ] = cipher[3 ]; plain[6 ] = cipher[4 ]; plain[4 ] = cipher[5 ]; plain[7 ] = cipher[6 ]; plain[5 ] = cipher[7 ]; plain[10 ] = cipher[8 ]; plain[8 ] = cipher[9 ]; plain[11 ] = cipher[10 ]; plain[9 ] = cipher[11 ]; plain[14 ] = cipher[12 ]; plain[12 ] = cipher[13 ]; plain[15 ] = cipher[14 ]; plain[13 ] = cipher[15 ]; plain[18 ] = cipher[16 ]; plain[16 ] = cipher[17 ]; plain[19 ] = cipher[18 ]; plain[17 ] = cipher[19 ]; plain[22 ] = cipher[20 ]; plain[20 ] = cipher[21 ]; plain[23 ] = cipher[22 ]; plain[21 ] = cipher[23 ]; }
r1 到明文输入的解密需要 xxtea_decode,这里我使用了别人写好的项目 github xxtea ,VS 里导入就能用了。key 因为取的是开头 4 个字符,所以直接猜 “flag”,下面是解密脚本的 main 函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #include <iostream> #include "xxtea.h" void main () { char r3[]{ 0xCE , 0xBC , 0x40 , 0x6B , 0x7C , 0x3A , 0x95 , 0xC0 , 0xEF , 0x9B , 0x20 , 0x20 , 0x91 , 0xF7 , 0x02 , 0x35 , 0x23 , 0x18 , 0x02 , 0xC8 , 0xE7 , 0x56 , 0x56 , 0xFA }; Decode3(r3); char r1[24 ]; Decode2(r3, r1); size_t len = 24 ; char key[] = { "flag" }; char * plain = (char *)xxtea_decrypt(r1, len, key, &len); std ::cout << plain << std ::endl ; }
[ACTF新生赛2020]Oruga | x86,elf,maze main 函数前面在检查输入前 5 个字符是不是 “actf{“,然后调用 sub_78A 来检查 flag 内容。该函数可以理解成一个类似滑动冰壶的游戏,冰壶在冰面上滑动,不碰到墙壁就停不下来:
如果不是很理解,可以玩下这个 4399 滑动海绵漆
map 中的 0 表示冰面,感叹号表示终点,其他字符表示墙壁。把 map 拿出来处理一下,0 替换成 * 方便观察:
1 2 3 4 5 6 7 maze = "00 00 00 00 23 00 00 00 00 00 00 00 23 23 23 23 00 00 00 23 23 00 00 00 4F 4F 00 00 00 00 00 00 00 00 00 00 00 00 00 00 4F 4F 00 50 50 00 00 00 00 00 00 4C 00 4F 4F 00 4F 4F 00 50 50 00 00 00 00 00 00 4C 00 4F 4F 00 4F 4F 00 50 00 00 00 00 00 00 4C 4C 00 4F 4F 00 00 00 00 50 00 00 00 00 00 00 00 00 00 4F 4F 00 00 00 00 50 00 00 00 00 23 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 23 00 00 00 00 00 00 00 00 00 4D 4D 4D 00 00 00 23 00 00 00 00 00 00 00 00 00 00 4D 4D 4D 00 00 00 00 45 45 00 00 00 30 00 4D 00 4D 00 4D 00 00 00 00 45 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 45 45 54 54 54 49 00 4D 00 4D 00 4D 00 00 00 00 45 00 00 54 00 49 00 4D 00 4D 00 4D 00 00 00 00 45 00 00 54 00 49 00 4D 00 4D 00 4D 21 00 00 00 45 45" .split(' ' ) maze = [int (i, 16 ) for i in maze] maze = [i if i !=0 else ord ('*' ) for i in maze] maze = '' .join(map (chr , maze)) for i in range (0 , len (maze), 16 ): print maze[i:i+16 ]
找到路径(假设 上 - U,下 - D,左 - L,右 - R):
替换成题目中用到的控制方向的四个字符:
1 2 3 4 5 6 path = "DRURDRULDRULD" path = path.replace('D' , 'M' ) path = path.replace('U' , 'W' ) path = path.replace('L' , 'J' ) path = path.replace('R' , 'E' ) print path
[GWCTF 2019]re3 | x86,elf,SMC,md5,AES_ECB main 函数:
for 循环表示只有在运行的时候才会解密 sub_402219 函数(该函数逐字节异或 0x99),写个 python 的 idc 脚本直接还原该函数:
1 2 3 4 5 6 7 import idcfuncAddr = 0x402219 for i in range (224 ): enc = get_bytes(funcAddr + i, 1 ) dec = ord (enc) ^ 0x99 patch_byte(funcAddr + i, dec)
在 ida 中选择 File -> Script File…,选择该脚本即可运行。来到地址 0x402219 处,光标放在 sub_402219 标签上,按 U 取消函数定义,再按 C 数据转汇编代码,最后按 P 创建函数,F5 反编译:
至于为什么知道那个函数是 AES 加密,ida 有个插件叫 findcrypto 谁用谁知道,安装方式请自行百度。常用加密模式一般就 CBC/ECB,CBC 模式需要 iv,这里就俩参数,一个 key, 一个待加密明文,所以加密模式就是 ECB 咯。key 由 sub_40207B 函数准备,可以直接 gdb 动调从内存中拿出来。下面是解密脚本(python3):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 from Crypto.Cipher import AESfrom binascii import a2b_hexdef decrypt (text ): key = "cb 8d 49 35 21 b4 7a 4c c1 ae 7e 62 22 92 66 ce" .split(' ' ) key = [int (i, 16 ) for i in key] key = bytes (key) mode = AES.MODE_ECB cryptor = AES.new(key, mode) plain_text = cryptor.decrypt(a2b_hex(text)) return bytes .decode(plain_text).rstrip('\0' ) raw = "BC 0A AD C0 14 7C 5E CC E0 B1 40 BC 9C 51 D5 2B 46 B2 B9 43 4D E5 32 4B AD 7F B4 B3 9C DB 4B 5B" target = raw.replace(' ' , '' ) piece1 = target[:32 ] piece2 = target[32 :] print (decrypt(piece1) + decrypt(piece2))
[网鼎杯 2020 青龙组]singal | x86,exe,vm虚拟机,angr 打开 vm_operad 函数,看样子出题人写了个超小型虚拟机:
三种方法
第一种 —— code 也不多,手动跟踪每一次 switch,得到 flag 每一位的计算方式,再写脚本逆回去
第二种 —— 稍微分析一下得知 case 1 赋值 mem 数组,case 7 拿 mem 与 target 进行比较,把 vm_operad 从 ida 摘下来,用 VS 改亿小点,写个程序爆破 flag(或者逆着 code 回去,从 target 反推输入)。爆破的脚本我贴这儿了:
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 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 #include <stdio.h> #include <vcruntime_string.h> #include <stdlib.h> #pragma warning (disable : 4996) int code[0x1C8 / sizeof (int )];char target[15 ];char mem[100 ]; char reg; int dsp2; int tIdx; int idx; int dsp1; int ip; char mem_bak[100 ];char reg_bak;int dsp2_bak;int tIdx_bak;int idx_bak;int dsp1_bak;int ip_bak;void env_init () { FILE* fp = fopen ("C:\\memdump" , "r" ); if (fp == NULL ) { puts ("Error memdump file path." ); exit (0 ); } fread (code, sizeof (int ), sizeof (code) / sizeof (int ), fp); fclose (fp); memset (mem, 0 , sizeof (mem)); reg = 0 ; ip = 0 ; dsp1 = 0 ; idx = 0 ; tIdx = 0 ; dsp2 = 0 ; for (int i = 0 ; i < 15 ; i++) { target[i] = (char )code[0x154 / sizeof (int ) + 2 * i]; } } void save_vm_state () { memcpy (mem_bak, mem, sizeof (mem)); reg_bak = reg; dsp2_bak = dsp2; tIdx_bak = tIdx; idx_bak = idx; dsp1_bak = dsp1; ip_bak = ip; } void recover_vm_state () { memcpy (mem, mem_bak, sizeof (mem)); reg = reg_bak; dsp2 = dsp2_bak; tIdx = tIdx_bak; idx = idx_bak; dsp1 = dsp1_bak; ip = ip_bak; } bool vm_operate (int cur, char * input) { while (1 ) { switch (code[ip]) { case 1 : if (cur == tIdx) { return reg == target[cur]; } ++ip; ++tIdx; ++dsp1; break ; case 2 : reg = (char )(code[ip + 1 ]) + input[dsp1]; ip += 2 ; break ; case 3 : reg = input[dsp1] - (char )(code[ip + 1 ]); ip += 2 ; break ; case 4 : reg = code[ip + 1 ] ^ input[dsp1]; ip += 2 ; break ; case 5 : reg = code[ip + 1 ] * input[dsp1]; ip += 2 ; break ; case 6 : ++ip; break ; case 7 : break ; case 8 : input[dsp2] = reg; ++ip; ++dsp2; break ; case 10 : ++ip; break ; case 11 : reg = input[dsp1] - 1 ; ++ip; break ; case 12 : reg = input[dsp1] + 1 ; ++ip; break ; default : break ; } } } int main () { env_init (); char flag[16 ]{}; for (int i = 0 ; i < 15 ; i++) { save_vm_state (); for (int j = 33 ; j < 128 ; j++) { flag[i] = j; if (vm_operate (i, flag)) { flag[i] = j; break ; } recover_vm_state (); } } flag[15 ] = 0 ; puts (flag); }
可以看出前两种方法非常不方便,都需要认真分析每个 opcode 对应干了什么事,边调边写,一血早没了。有没有无脑冲冲冲的方法呢?下面有请第三种方法 —— angr 符号执行 隆重登场
经过几个版本的更新,angr 的 API 现在用起来非常方便,不了解的可以阅读 系列文章 ,这里直接贴脚本:
1 2 3 4 5 6 7 8 9 10 import angrproject = angr.Project("./signal.exe" , auto_load_libs = False ) @project.hook(0x40179E ) def MyHook (state ): print (state.posix.dumps(0 )) project.terminate_execution() project.execute()
运行截图:
什么叫秒解?
[GUET-CTF2019]number_game | x86,elf,二叉树,数独 根据函数行为还原 main 逻辑:
结点的结构体经分析后有:
Preorder_builder 函数:
Inorder 中序遍历函数:
CheckSudoku 函数检查是否满足数独游戏的要求:
把数独拿出来看看:
顺便手解了:
1 2 3 4 5 6 7 8 9 10 (0, 2) = 0 (1, 2) = 4 (1, 4) = 2 (2, 1) = 1 (2, 4) = 4 (3, 0) = 2 (3, 2) = 1 (3, 3) = 4 (4, 2) = 3 (4, 3) = 0
FillSudoku 函数在填写数独的时候使用的就是中序遍历得到的数组下标序列:
理解了原理可以照着写个前序、中序来获得填入数独时数组下标的映射序列。我偷懒直接把 CheckValid 函数 patch 掉,然后输个 ABCDEFGHIJ,动态调试看看被映射到哪里去了,得到被填写后的数独为 14H2330D1I0B23JE3AF042CG1,故有:
1 2 3 4 5 6 7 8 9 10 (0, 2) = 0 -> H (1, 2) = 4 -> D (1, 4) = 2 -> I (2, 1) = 1 -> B (2, 4) = 4 -> J (3, 0) = 2 -> E (3, 2) = 1 -> A (3, 3) = 4 -> F (4, 2) = 3 -> C (4, 3) = 0 -> G
按照 A-J 的顺序组织好输入就是 flag 了
[Zer0pts2020]easy strcmp | x86,elf,elf运行流程 main 函数简单地通过 strcmp 来判断输入,那肯定没这么简单。直接找 init_array,发现可疑函数 sub_795:
该函数将 strcmp 的 plt 表项替换为虚假的 strcmp,main 函数调用 strcmp 时控制流会来到 FakeStrcmp:
FakeStrcmp 首先对输入进行变换,再调用 RealStrcmp:
脚本还原时需要注意小端序的问题:
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 target = "zer0pts{********CENSORED********}" target = map (ord , target) nums = [] for i in range (0 , len (target), 8 ): tmp = target[i:i+8 ] num = 0 try : for j in range (8 ): num += tmp[j] << j * 8 except : pass nums.append(num) res = [] qword_201060 = [0 , 0x410A4335494A0942 , 0xB0EF2F50BE619F0 , 0x4F0A3A064A35282B , 0 ] for i in range (5 ): res.append(qword_201060[i] + nums[i]) flag = '' for i in range (5 ): tmp = "%016X" % res[i] for j in range (14 , -1 , -2 ): flag += chr (int (tmp[j:j+2 ], 16 )) print flag.strip('\x00' )
[FlareOn3]Challenge1 | x86,exe,变种base64 main 函数逻辑清晰:
不过 base64 编码表被替换,还原:
1 2 3 4 5 6 7 8 9 10 11 12 import base64ori = '' target = "x2dtJEOmyjacxDemx2eczT5cVS9fVUGvWTuZWjuexjRqy24rV29q" oldTable = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" newTable = "ZYXABCDEFGHIJKLMNOPQRSTUVWzyxabcdefghijklmnopqrstuvw0123456789+/" for i in target: idx = newTable.find(i) ori += oldTable[idx] print base64.b64decode(ori)
equation | javascript, JsFuck混淆 可以直接用 js 写正则替换,但最后还是要到 python 里来用 z3 求解器,所以干脆就全用 python 好了,python 里执行 js 代码需要用到 PyExecJS 库。所有等式反混淆脚本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import reimport execjsdef ExecFront (string ): num = execjs.eval (string.group(1 )) return "l[%s]" % num def ExecBehind (string ): num = execjs.eval (string.group(1 )) return " == %s" % num with open ("equation.html" ) as f: data = f.read() equations = re.search(r"if\((.+)\){" , data).group(1 ).split("&&" ) for equation in equations: tmp = re.sub(r"(?:l\[)([!\+\[\]]+)(?:\])" , ExecFront, equation) print re.sub(r"(?:==)(.+)" , ExecBehind, tmp)
还挺得劲儿:
但是为了方便使用 z3,还需要小改亿些地方:
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 import z3import reimport execjsdef ExecFront (string ): num = execjs.eval (string.group(1 )) return "flag_char[%s]" % num def ExecBehind (string ): num = execjs.eval (string.group(1 )) return " == %s" % num with open ("equation.html" ) as f: data = f.read() flag_len = 0x2a solver = z3.Solver() flag_char = [z3.Int("char%d" % i) for i in range (flag_len)] equations = re.search(r"if\((.+)\){" , data).group(1 ).split("&&" ) for equation in equations: tmp = re.sub(r"(?:l\[)([!\+\[\]]+)(?:\])" , ExecFront, equation) eq = re.sub(r"(?:==)(.+)" , ExecBehind, tmp) eval ( "solver.add( " + eq + " )" ) flag = '' if solver.check() == z3.sat: s = solver.model() for i in range (flag_len): flag += chr (s[flag_char[i]].as_long()) print flag
[ACTF新生赛2020]Universe_final_answer | x86,elf,z3,angr sub_860 十个未知数,十个方程,未知数对应输入的 key,可以使用 python z3-solver 库来求解多元一次方程组,但我懒得从 ida 上摘方程了,angr 最喜欢这种了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import angrbase_addr = 0x400000 project = angr.Project("./UniverseFinalAnswer" , main_opts = {"base_addr" : base_addr}, auto_load_libs = False ) state = project.factory.entry_state(add_options = {angr.options.LAZY_SOLVES}) simManager = project.factory.simgr(state) simManager.explore(find = base_addr + 0x71A , avoid = base_addr + 0x6EF ) flag = simManager.found[0 ].posix.dumps(0 ) print (flag[:10 ].decode())
得到 F0uRTy_7w@,运行程序,输入该 key 得到 flag