彩笔来 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:

easyre

查看交叉引用,找到关键函数 sub_1400118C0:

reverse1

上面的 for 循环将 Str2 中的 o 都替换成 0


reverse2 |x86,elf

反编译 main:

reverse2

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:

helloword


xor | x86,MachO

main 函数:

xor

解密脚本:

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 函数,根据函数行为还原部分变量及函数名:

reverse3

解密脚本:

1
2
3
4
5
6
7
8
9
import base64

rawData = "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 函数:

不一样的flag

一维化的迷宫是 *11110100001010000101111#,根据 main 函数的判断条件,可知二维状态下一行有 5 个元素,还原二维迷宫:

不一样的flag

将 上下左右 映射到 1234 去就得到 flag 了

PS. 这道题出得不严谨,考虑这样一种情况:在一个地方反复先下后上(或者反复左右横跳),然后再走到终点,依然显示顺序正确


SimpleRev | x86,elf

main 函数调用 Decry 函数,下面是对该函数的分析:

SimpleRev

采用爆破的方式获得 flag,脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import string

key = "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 打开:

Java逆向解密

解密脚本:

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 b64decode

flag = [''] * 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 不识别剩下的代码,强转一下:

BJDCTF2nd8086

逻辑很简单:

BJDCTF2nd8086

解密脚本:

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:

GKCTF2020Check_1n

访问桌面上的 flag 文件,将得到的 base64 串解码,得到 Why don’t you try the magic brick game,那就玩一下打砖块,不用动等他死了上方就会出现 flag


findit | JVM,apk,java层

jadx 打开:

findit

变量 b 拼接起来是 pvkq{m164675262033l4m49lnp7p9mnk28k75}

不用想了,无脑 凯撒解密,位移为 10


[GXYCTF2019]luck_guy | x86,elf,patch

关键函数 get_flag:

GXYCTF2019luck_guy

经过分析,得知当执行流依次经过 4、5、1 后会输出正确的 flag。两条路 —— 脚本/patch,那我能惯着 python 吗,patch 安排!

patch 点 1:先去到 case4

GXYCTF2019luck_guy

patch 点 2:接着执行 case5

GXYCTF2019luck_guy

patch 点 3:case5 结束后跳转到 case1

GXYCTF2019luck_guy

patch 点 4:case1 结束后退出函数

GXYCTF2019luck_guy

F5 一下可以看到,现在的执行流是正确的:
GXYCTF2019luck_guy

放到 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 code
code = ['\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 函数检查第一个输入的部分:

CrackRTF

如何判断 sub_40100A 是 sha1 哈希函数呢,点进去发现它调用 sub_401230,继续跟进:

CrackRTF

根据 windows API 的尿性,CryptCreateHash 函数的第二个参数应该用来指定 hash 的类型。在 MSDN 文档 中查找该函数:

CrackRTF

ALG_ID 进去查找 0x8004:

CrackRTF

那密码 1 就能得到了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import hashlib

def 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
# 123321

第二次输入检查:

CrackRTF

同理可以确定 sub_401019 为 md5 哈希函数:

CrackRTF

CrackRTF

这次密码就有点难爆了,不像密码 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:

BJDCTF2020JustRE

查看 DialogFunc:

BJDCTF2020JustRE


[2019红帽杯]easyRE | x86,elf,elf运行流程

shift + F12,查找 “You found me!!!” 的交叉引用,找到 sub_4009C6。第一部分:

2019红帽杯easyRE

脚本:

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`。第二部分:

2019红帽杯easyRE

脚本:

1
2
3
4
5
6
7
import base64

target = "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 函数:

2019红帽杯easyRE

_init_array 地址可以在 __libc_csu_init 函数中找到:

2019红帽杯easyRE

翻一翻 _init_array 和 _fini_array 中的函数,每个都点过去 F5 看一下:

2019红帽杯easyRE

_fini_array 的第二个函数指针 sub_400D35 最为可疑:

2019红帽杯easyRE

结合之前得到的信息 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 函数的逻辑吧:

Youngter-drive

StartAddress 函数:

Youngter-drive

sub_41119F 函数:

Youngter-drive

为啥上面两张图片的分辨率不一样呢?那是因为亲爱的学校又双叒叕停电了,台式机直接暴毙,只能换成笔记本,我 tm***

从这俩函数的行为可以判断出 StartAddress 与 sub_41119F 将会交替执行,且线程 1 会先拿到锁(信号量与锁机制)。再来研究一下 StartAddress 中调用的 sub_41112C,这个函数又调用了 sub_411940,而 sub_411940 因为添加了花指令导致 ida 无法反编译:

Youngter-drive

找找是哪里出问题了,Options -> General 打开栈指针、显示字节码:

Youngter-drive

找到导致 ida 栈跟踪分析出错的花指令:

Youngter-drive

add esp, 8 nop 掉就可以愉快 F5 了(当然也可以 Alt + K 调整栈指针达到栈平衡):

Youngter-drive

反编译:

Youngter-drive

这个函数干的事情就是把输入的大写字母替换成 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 函数,逻辑很简单:

ACTF新生赛2020easyre

脚本:

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 函数逻辑:

SUCTF2019SignIn

用到了一些 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 gmpy2

e = 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)
# 73756374667b50776e5f405f68756e647265645f79656172737d

output 是经过 Encode 函数编码过的,该函数每次取来输入的一个字节,高 4 bit 和低 4 bit 分别到 byte_202010 字符数组中去映射为新的字符,将输入的 32 字节扩展为 64 字节的 output:

SUCTF2019SignIn

逆回去:

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,恺撒密码

恺撒密码,逻辑简单:

ACTF新生赛2020rome

脚本:

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 string

target = [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 函数直接找),逻辑很简单:

GUET-CTF2019re

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 的地方:

VN2020公开赛strangeCpp

随便点一个 byte_… 跟过去,可以发现一个长度为 17 的可疑字符数组:

VN2020公开赛strangeCpp

查找交叉引用,找到关键函数 sub_140013580:

VN2020公开赛strangeCpp

通过 result == 607052314,与逆向 sub_140011384 函数可以确定 key 的值,sub_140011384 逻辑如下:

VN2020公开赛strangeCpp

爆破呗,因为 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 一下,让执行流顺路执行该函数:

BJDCTF2020easy

运行一下得到 flag:

BJDCTF2020easy

不 patch 也是可以的,exe 也没有开基址重定位,OD 动态调试时直接修改 eip 到该函数即可


[ACTF新生赛2020]usualCrypt | x86,exe,变种base64

main 函数逻辑很简单,就是将输入丢到 sub_401080 函数处理,结果与 byte_40E0E4 字符数组逐字节比较。

sub_401080 函数主体是个 base64 编码,这一点从编码表 byte_40E0A0 不难看出。但是在编码开始前,该函数调用了 sub_401000 对编码表进行了变换,OD 可以调出来:

ACTF新生赛2020usualCrypt

编码结束后,调用 sub_401030 对编码的结果进行大小写的翻转:

ACTF新生赛2020usualCrypt

脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import base64

old_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 函数:

VN2020公开赛CSRe

两个 sha1 拿去网站一查,一个是 314159,另一个是 return,所以 str 和 str2 分别是 1415 和 turn


[MRCTF2020]Transform | x86,exe

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 函数分析并还原符号:

GWCTF2019xxor

普通 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 函数解一个三元一次方程组(注意溢出与截断):
GWCTF2019xxor

有:

  • 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 无法解析:

HDCTF2019Maze

Undefine 以后将 0xE8 nop 掉

HDCTF2019Maze

在 0x90 处按 ‘c’ 将数据强转成代码,之后在 main 函数开始处按 ‘p’ 形成函数,就可以 F5 了:

HDCTF2019Maze

反编译:

HDCTF2019Maze

由于没有对迷宫的点是否可以通行进行判断,所以存在很多种路径都输出答案正确的情况

shift + F12 可以找到长度为 70 的迷宫:

HDCTF2019Maze

当顶边 * 侧边为 10 * 7 时,满足终点在 (5,-4) 的要求,还原二维迷宫:
HDCTF2019Maze

所以正确的路径为 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 方法:

BJDCTF2020BJDhamburgercompetition

网站上查下这个 sha1,得到 1001,先看看他写的 Md5:

BJDCTF2020BJDhamburgercompetition

那就取 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 函数的逻辑,根据各变量、函数行为给他们加符号:

crackMe

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 函数里面有三处反调试,检测到有调试痕迹时,会进入误导的分支:

crackMe

把它们 patch 掉以后,先来看上面的 while 循环:

crackMe

好麻烦,不想静态分析了,上 OD 吧,密码一栏输 1234abcdABCD,等 while 跳出看看 v15 里的值:

crackMe

可以看到 12 个字符变成了 6 个字节,每两个字符凑成一个字节。下一个 while 循环:

crackMe

其中 sub_401710 函数经过动态调试后发现不会对 target 数组或者 user 产生任何影响,该循环干的事就是给长度为 8 的 target 数组赋值,每次赋值一字节,根据上一个 while 循环可以推出输入的密码长度为 16。

最后的 sub_401470 函数用来检查 target 数组是否满足要求:

crackMe

该函数对 target 数组进行逐字节检查:

crackMe

需要注意,在检查 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 函数做完,根据函数行为补一下符号:

FlareOn4IgniteMe

GetInput 函数:

FlareOn4IgniteMe

CheckInput:

FlareOn4IgniteMe

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 信息知道是哪句调用分析失败了:

MRCTF2020Xor

这里是地址 0x401095 处的调用分析失败了,此地址处调用了 sub_401020:

MRCTF2020Xor

双击跟过去 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; // rbx
__int64 v3; // rdi
__int64 v4; // rax
int currentPos; // ecx
__int16 *v6; // rsi
__int64 v7; // rbp
__int16 v8; // dx
char v9; // dl
CHAR *v10; // rcx

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 ) // up
{
if ( currentPos & 0xFFFFFFF0 ) // 到达上边界
{
currentPos -= 16; // 由此可知一行 16 个字符
goto LABEL_21;
}
currentPos += 208;
Position = currentPos;
}
if ( v8 == 37 ) // down
{
if ( (currentPos & 0xFFFFFFF0) != 208 ) // 到达下边界
{
currentPos += 16;
goto LABEL_21;
}
currentPos -= 208;
Position = currentPos;
}
if ( v8 == 36 ) // left
{
if ( currentPos & 0xF ) // 到达左边界
{
--currentPos;
goto LABEL_21;
}
currentPos += 15;
Position = currentPos;
}
if ( v8 != 38 ) // right
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) + 3i64) |= 1u;
return *(v2 + 48);
}

实现的功能是监听键盘按键,从而控制目标在迷宫中的移动,maze 拿出来处理一下:

GKCTF2020BabyDriver

需要注意的是,驱动判断按键用的是键盘扫描码,可以在 网站 查询,37 和 38 分别对应 K 和 L,将之前的路径替换一下,再 md5:

1
2
3
4
5
6
7
import hashlib

path = "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

需要用到 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 函数:

firmware

commServer 是 echo.byethost51.com,端口为 36667


[MRCTF2020]hello_world_go | x86,elf,go语言

main.main 函数:

MRCTF2020hello_world_go

unk_4D3C58 即为 flag


[FlareOn6]Overlong | x86,exe,patch

start 函数里弹了一个对话框,对话框的内容由 PrepareText 函数处理:

FlareOn6Overlong

将第三个参数 patch 成 127 再运行就能得到 flag(或者动态调试把传参修改一下)


[WUSTCTF2020]level3 | x86,elf,elf运行流程

main 函数逻辑清晰,要求对 d2G0ZjLwHjS7DmOzZAY0X2lzX3CoZV9zdNOydO9vZl9yZXZlcnGlfD== 进行 base64 解密。但是在 main 函数执行前,base 编码表被函数 O_OLookAtYou 做了手脚,该函数在 _init_array 数组中,会在 main 函数之前被执行:

WUSTCTF2020level3

elf 运行流程详见本文 [2019红帽杯]easyRE writeup

动态调试将新的编码表拿出来:

WUSTCTF2020level3

脚本:

1
2
3
4
5
6
7
8
9
10
import base64

old_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()
{
// r3 -> r2
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);

// r2 -> r1
char r1[24];
Decode2(r3, r1);

// 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 内容。该函数可以理解成一个类似滑动冰壶的游戏,冰壶在冰面上滑动,不碰到墙壁就停不下来:

ACTF新生赛2020Oruga

如果不是很理解,可以玩下这个 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):

MRCTF2020hello_world_go

替换成题目中用到的控制方向的四个字符:

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 函数:

GWCTF2019re3

for 循环表示只有在运行的时候才会解密 sub_402219 函数(该函数逐字节异或 0x99),写个 python 的 idc 脚本直接还原该函数:

1
2
3
4
5
6
7
import idc

funcAddr = 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 反编译:

GWCTF2019re3

至于为什么知道那个函数是 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
# python3
from Crypto.Cipher import AES
from binascii import a2b_hex

def 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 函数,看样子出题人写了个超小型虚拟机:

网鼎杯2020青龙组singal

三种方法

第一种 —— 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]; // [esp-85h] [ebp-85h]
char reg; // [esp-21h] [ebp-21h]
int dsp2; // [esp-20h] [ebp-20h]
int tIdx; // [esp-1Ch] [ebp-1Ch]
int idx; // [esp-18h] [ebp-18h]
int dsp1; // [esp-14h] [ebp-14h]
int ip; // [esp-10h] [ebp-10h]

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()
{
// 初始化代码段
// memdump 文件内容为 0x403040 开始的 0x1C8 个字节
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;

// 初始化target数组
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:
// 原为比较加密结果与target
break;
case 8:
input[dsp2] = reg;
++ip;
++dsp2;
break;
case 10:
// 原为接收长度为15的输入
++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]中途可能会被case 8修改,这里再赋值一次
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 angr

project = angr.Project("./signal.exe", auto_load_libs = False)

@project.hook(0x40179E)
def MyHook(state):
print(state.posix.dumps(0))
project.terminate_execution()

project.execute()

运行截图:

网鼎杯2020青龙组singal

什么叫秒解?


[GUET-CTF2019]number_game | x86,elf,二叉树,数独

根据函数行为还原 main 逻辑:

GUETCTF2019number_game

结点的结构体经分析后有:
GUETCTF2019number_game

Preorder_builder 函数:

GUETCTF2019number_game

Inorder 中序遍历函数:

GUETCTF2019number_game

CheckSudoku 函数检查是否满足数独游戏的要求:

GUETCTF2019number_game

把数独拿出来看看:

GUETCTF2019number_game

顺便手解了:

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 函数在填写数独的时候使用的就是中序遍历得到的数组下标序列:

GUETCTF2019number_game

理解了原理可以照着写个前序、中序来获得填入数独时数组下标的映射序列。我偷懒直接把 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:

Zer0pts2020easystrcmp

该函数将 strcmp 的 plt 表项替换为虚假的 strcmp,main 函数调用 strcmp 时控制流会来到 FakeStrcmp:
Zer0pts2020easystrcmp

FakeStrcmp 首先对输入进行变换,再调用 RealStrcmp:

Zer0pts2020easystrcmp

脚本还原时需要注意小端序的问题:

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 函数逻辑清晰:

FlareOn3Challenge1

不过 base64 编码表被替换,还原:

1
2
3
4
5
6
7
8
9
10
11
12
import base64

ori = ''
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
#coding=utf-8
import re
import execjs

def 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()
# 取出 html 文档中的所有等式
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)

还挺得劲儿:

equation

但是为了方便使用 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
#coding=utf-8
import z3
import re
import execjs

def 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)
# print eq
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 angr

base_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