Previously
我是真的会谢,明明vsCTF的知名度还算可以吧,结果国内搜索引擎基本没有Wp,外网一翻,全是Cry和Web的题解,Re的题解基本都是针对在线题,静态题题解寥寥无几。
麻了,虽然菜,自己动手丰衣足食,就算煮出来的效果堪比雷电将军特殊料理,也好过没东西吃。
Reverse
Binary Flood
焯,又是结构一毛一样巨量文件(我为甚么要说又)
但是好像就是一套单纯的输入数据->加密->判断的机制,并不是地图题(上次复现那套多文件地图题给我整出emotional damage了)。最终,每组数据的结果是一个加密过的串,存储在相对文件头偏移4020的位置:
好嘛,虽然不知道怎么加密的,但是我知道,肯定得先把从file_ 0到file_ 1336这一堆东西导出来:
s = []
for i in range(1337):
with open(f'file_{i}', 'rb') as f:
l = f.read()
s.append(l[0x4020:0x4020+39].decode())
f.close()
fw = open('pickedata.txt', 'w')
for i in range(1337):
fw.write(str(s[i]))
fw.close()
结果就是导出来的这一大串东西具有强烈的base64既视感……
NzPxDxwEv9TLmgFVNjCRjcEJgKMiCJBBvcY0mKoVM0wbgGMlNuvZCXZARwgIGFtIV+……
但是你先别急,虽然能确定完整的加密其中一步应该是base64,但还是先看看这玩意的加密到底是怎么回事。考虑到逆向后的代码有极强的不可读性,我们尝试动态调试。
判定用的串长度为39,我们索性构造一个39长,辨识度还高的串进去测试,比方说abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLM,然后再把断点下在判定过后的任意位置,于是从rdi寄存器得到变换后的结果:
再与原串进行比对:
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLM
FpGhHqIdJrKiLsMbtjuevkwaxlyfzmAcBnCgDoE
不难发现本体并没有进行base64的操作,而是进行了一个次序的变换。顺着这个思路,我们把原数据每39位进行如上规律的移位。考虑到python的list变量天然分割了这些数据,我们只需要在提取程序上稍作改造(以下代码部分参考Jarosław Keller的题解):
s = []
for i in range(1337):
with open(f'file_{i}', 'rb') as f:
l = f.read()
s.append(l[0x4020:0x4020+39].decode())
f.close()
l = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLM"
a = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLM"
b = "FpGhHqIdJrKiLsMbtjuevkwaxlyfzmAcBnCgDoE"
# 干脆在动态调试结果的基础上机操规律,省人工
d = []
for c in l:
d.append((a.find(c), b.find(c)))
# ----------------------------------
x = b''
for t in s:
w = [''] * 39
for a, b in d:
w[a] = t[b]
x += ''.join(w).encode()
x += b'=='
# --标记部分摘自Jarosław Keller dalao--
fw = open('result.txt', 'w')
fw.write(str(x))
fw.close
然后把搓出来的合成肉扔进赛博厨房烧烤:
最后得到一张pdf,往下多划拉划拉就可以找到这个flag:
vsctf{templated_binaries_are_1337}
FunctionalCPP
所以为什么这帮子老外总想着光看几张破图就能让人明白什么(
看外网的题解,不知道为什么把断点下在0x555555555914那里动调,我们打开程序瞅瞅:
好嘛,经典诈骗链接。
die看不出什么,直接搬进ida瞅一眼:
这下真看不懂了(看我凌乱的注释就知道),还得请密码人出山。不过有没有什么让Re手也轻松做出来的办法呢?
有肯定有,那就是暴力硬破,就是花的时间长了点。那么思路很明显了,暴力脚本+从断点输入数据,直接创碎这玩意。问题是……怎么知道自己创没创对位置呢?正常的判断只会对整个串判定T or F,如果要直接比对寄存器,总不能凭空掏出一个地址吧?!而且每一次动态调试出现的输入函数的地址都不相同,什么5582D32C5360、563611541050的都敢冒出来,我真的会谢。
直到我去查找了一下资料,发现gdb可以直接输入start来读取程序的实际入口地址——
看来很接近了。于是我们看看在主函数的判断语句(忽略我凌乱的注释,那会误导你):
如果我们要通过修改被判断字串的值来爆破,看来是这了。对应判断条件secret位置在主函数偏移约0x497~0x498的位置,刚好是题解里说的玩意——0x555555555914。然后尝试用Python的gdb拓展尝试疯狂爆破。问题来了,用什么爆破呢?我们根据前面的代码可以得知:
输入的是个字符串,至少输出去判断的也是个字符串,所以flag至少是可打印字符集。我们拿不易产生冲突的字符集浅试一手。
下面的代码参考了acdwas和spacewander的架构:
# move.py
# 1. 导入gdb模块来访问gdb提供的python接口
import gdb
# 2. 用户自定义命令需要继承自gdb.Command类
class Move_Py(gdb.Command):
# 3. docstring里面的文本是不是很眼熟?gdb会提取该类的__doc__属性作为对应命令的文档
def __init__(self):
# 4. 在构造函数中注册该命令的名字
super(Move_Py, self).__init__("mv", gdb.COMMAND_USER)
# 5. 在invoke方法中实现该自定义命令具体的功能
# args表示该命令后面所衔接的参数,这里通过string_to_argv转换成数组
def invoke(self, _unicode_args, _from_tty):
strcomb = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_+?{}' #经典base64字符集
w = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
a = 0
# 6. 使用gdb.execute来执行具体的命令
while a < 58:
i = 0
while True:
wline = list(w)
wline[a] = strcomb[i]
gdb.execute(f"r <<< {''.join(wline)}") #直接运行程序,同时把我们要输入的串塞进去
for j in range(a):
gdb.execute('c') #原代码这个判断的断点是个循环,绕过断点继续运行,像极了你疯狂按enter急急国王的样子
if int(gdb.parse_and_eval("$rbx")) == int(gdb.parse_and_eval("$rax")): #当场比较寄存器看看前缀是否相同
w = ''.join(wline)
print(w)
a += 1
break #行的话加入flag豪华午餐
i += 1
# 7. 向gdb会话注册该自定义命令
Move_Py()
于是乎,在把电脑干冒烟之后,你会得到一个flag:
vsctf{1_b37_y0u_w1LL_n3v3r_u53_Func710n4L_C++_1n_ur_L1F3?}