转自:https://xz.aliyun.com/t/4393
UTCTF是上周末国外的一个CTF比赛,逆向题中有几道质量还不错,简单整理了一下供大家参考。
Super Sucure Authentication
这是一道Java逆向题。Java逆向题在CTF里比较少见,主要是因为Java反编译太容易,没有太多trick。
其中考察比较多的有反射和动态加载类等,这道题就是使用动态加载类对代码进行了保护。
首先使用Jd-gui反编译Authenticator类,可以发现flag被分成了8份,并分别通过8个Verifier类进行检查(Verifier0 - Verifier7):
if (!candidate.substring(0, 7).equals("utflag{")) {
return false;
}
if (candidate.charAt(candidate.length() - 1) != '}') {
return false;
}
StringTokenizer st = new StringTokenizer(candidate.substring(7, candidate.length() - 1), "_");
if (!Verifier0.verifyFlag(st.nextToken())) {
return false;
}
if (!Verifier1.verifyFlag(st.nextToken())) {
return false;
}
if (!Verifier2.verifyFlag(st.nextToken())) {
return false;
}
if (!Verifier3.verifyFlag(st.nextToken())) {
return false;
}
if (!Verifier4.verifyFlag(st.nextToken())) {
return false;
}
if (!Verifier5.verifyFlag(st.nextToken())) {
return false;
}
if (!Verifier6.verifyFlag(st.nextToken())) {
return false;
}
if (!Verifier7.verifyFlag(st.nextToken())) {
return false;
}
随便点开几个Verifier,发现逻辑都是一样的:
private static byte[] arr = jBaseZ85.decode(new String("+kO#^0000Q0ZE7[5DJ%U0u.ZH0S:wG0u.WG0S:CK00ifB2MU+E0v4*I...");
public static boolean verifyFlag(String paramString)
{
Verifier0 localVerifier0 = new Verifier0();
Class localClass = localVerifier0.defineClass("Verifier0", arr, 0, arr.length);
Object localObject = localClass.getMethod("verifyFlag", new Class[] { String.class }).invoke(null, new Object[] { paramString });
return ((Boolean)localObject).booleanValue();
}
可以看到这里使用了Java的动态加载类的方法,将一串常量字符串通过Base85解码,并加载为Verifier0
类,并调用其中的verifyFlag
函数。
这里我们直接将代码在Java IDE中执行,发现Base85解码后得到的字符串开头就是Class文件头CAFEBABE
。
将其保存到文件,然后用Jd-gui打开,发现代码跟上面基本一样,只是常量字符串发生了变化。由于文件有3MB之大,猜测之后还有很多层,于是需要写代码自动化脱壳。
这里我们需要做的就是从Class文件中提取出该字符串,使用Base85进行解码,然后再提取字符串,不断重复该过程。于是就需要从Class文件中提取字符串。
为了实现这个目标,我们可以考虑使用一些相关的库来Parse Class文件,但对于这种简单的字符串提取,也可以研究一下文件结构,手动把字符串从Class文件中提取出来。
首先观察到字符串的开头都是相同的+kO#
,对应了Java Class文件的文件头,这样我们就可以定位到字符串开头。
但是实际上最后的字符串是由多个字符串拼起来的,即类似于new String("+kO#..") + new String("B2MU..") + ... + new String("F9Kl..")
,体现在Class文件中就是两个字符串之间还有一段没有用的数据:
观察了一下可以发现,这段数据的长度是有规律的,基本上第一个间隔是13,后面的都是3,所以可以特判直接过滤掉。(我的特判写的比较丑陋就不放出来了,大家可以自己尝试实现)
最后得到8个Verifier的class文件,都是简单的编码或者加密:
Verifier0,异或加密:
Verifier1,字符串逆序:
Verifier2,hashCode,Java中爆破:
private static int[] encrypted = { 3080674, 3110465, 3348793, 3408375, 3319002, 3229629, 3557330, 3229629, 3408375, 3378584 };
public static void verifyFlag()
{
for(int i = 0; i < 10; i++)
{
for(char c = 32; c < 127; c++)
if (encrypted[i] == (c + "foo").hashCode()) {
System.out.print(c);
}
}
}
Verifier3,凯撒移位:
Verifier4,简单数字运算:
Verifier5,MD5,直接查cmd5
Verifier6,SHA1,同上
Verifier7,flag直接送了
连接起来,得到完整flag:
utflag{prophets_anxious_demolition_animatronic_herald_fizz_stop_goodbye}
Simple python script
给出了python源代码:
flag = input("> ")
for i in range(0, len(flag), int((544+5j).imag)):
inputs = []
(lambda ねこ, _, __, ___, ____, _____, ネコ, q, k: getattr(ねこ, "extend")...
temp = getattr(__import__("ha"+"".__class__.__name__[0]+"hl"+(3)...
getattr(temp, "update")(getattr(flag[i:i + 5], "encode")("utf-8"))
if getattr(__import__("difflib"), "SequenceMatcher")(None, getattr(getattr(temp, "hexdigest")(), "lower")(), getattr(inputs[i // 5], "decode")("utf-8").lower()).ratio() != 1.0:
exit()
print("correct")
可以看到使用了多种混淆方式:
getattr(class, method)
就相当于class.method
"".__class__.__name__[0]
那些用来隐藏字符串(即"String"[0],"S")
- 最后的
SequenceMatcher(...).ratio() == 1
其实就是比较相等
于是整理出以下代码:
inputs = []
(lambda ... # 这段代码更新了inputs
temp = hashlib.new(... # 这里应该是使用了一种hashlib里的哈希
temp.update(flag[i:i + 5].encode("utf-8") ) # 对flag每5位做一次哈希
print(inputs[i // 5].decode("utf-8").lower()) # 这段代码是我新增的,因为最终与flag哈希值比较的和就是inputs[i // 5],所以打印出来
if temp.hexdigest().lower() == inputs[i // 5].decode("utf-8").lower(): # 原来应为!=,这里改成==防止exit
exit()
运行可以打印出几个哈希值:
26d33687bdb491480087ce1096c80329aaacbec7
1c3bcf656687cd31a3b486f6d5936c4b76a5d749
11a3e059c6f9c223ce20ef98290f0109f10b2ac6
6301cb033554bf0e424f7862efcc9d644df8678d
95d79f53b52da1408cc79d83f445224a58355b13
在CMD5上可以查到其中一部分,哈希算法是SHA1,剩余的可以在hashtoolkit上查到,连接得到完整flag:
puppyp1zzaanimetoruskitty
MOV
IDA加载发现全是MOV指令,可以大概知道使用了Movfuscator进行了混淆处理。
对于复杂一些的Movfuscator程序,可以尝试根据程序中字符串等信息,配合trace工具和下断点来追踪程序流程,并猜测程序逻辑(一般来说逻辑不会特别复杂)。此外,也可以尝试使用Demovfuscator进行反混淆,运气好的话说不定会解得比较好看。
不过对于这道题,程序中搜不到字符串信息,运行也没有反应,在strace和ltrace时发现很多SIGILL信号:
于是尝试使用gdb进行调试,在发出SIGILL信号时,gdb会断住,这是可以观察到栈顶有一个u
字符:
继续跟踪下去,发现每次SIGILL时栈顶都会添加一个字符,逐渐形成一个完整的flag:
utflag{sentence_that_is_somewhat_tangentially_related_to_the_challenge}
UTCTF adventure ROM
一道gameboy游戏逆向题,使用的工具是bgb(可以debug,非常方便)和IDA。
首先使用bgb运行游戏,可以看到有四个框,分别可以输入ABCD,输入错误会死掉,显示LOSER:
此外,地图上还有不可见的线(在题目描述中可知),碰到后也会死掉,显示DEAD:
大体了解游戏逻辑后,我们就可以开始逆向了。在IDA中打开,处理器选择z80
(具体可以参考这份wp)。
首先搜索字符串,可以找到LOSER和DEAD:
搜索第一个DEAD
的地址71E
,可以找到判断撞线死亡的逻辑:
这里我们直接把这部分nop掉就不会再撞死了(注意这里的nop是\x00)
同样的方法搜索LOSER
的地址568
:
找到了判断输入是否正确的判断,于是我们使用bgb在这里下断点:
可以看到我们的输入和正确值分别保存在a
和c
寄存器中。
于是我们就可以反复运行,随便输入一个值,然后修改我们的输入值为正确值,并记录下来,即可获得完整flag:
AABDCACBBDBCDCAD
其他
剩下的几道题比较简单,有问题可以留言交流
官方源码
https://github.com/UTISSS/UTCTF/