【转帖】UTCTF逆向题详解
转自:https://xz.aliyun.com/t/4393UTCTF是上周末国外的一个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文件中就是两个字符串之间还有一段没有用的数据:
!(https://xzfile.aliyuncs.com/media/upload/picture/20190311165054-c7f78426-43da-1.png)
观察了一下可以发现,这段数据的长度是有规律的,基本上第一个间隔是13,后面的都是3,所以可以特判直接过滤掉。(我的特判写的比较丑陋就不放出来了,大家可以自己尝试实现)
最后得到8个Verifier的class文件,都是简单的编码或者加密:
Verifier0,异或加密:
!(https://xzfile.aliyuncs.com/media/upload/picture/20190311165055-c89951ca-43da-1.png)
Verifier1,字符串逆序:
!(https://xzfile.aliyuncs.com/media/upload/picture/20190311165057-c9461b44-43da-1.png)
Verifier2,hashCode,Java中爆破:
!(https://xzfile.aliyuncs.com/media/upload/picture/20190311165058-c9e39ce8-43da-1.png)
```
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 == (c + "foo").hashCode()) {
System.out.print(c);
}
}
}
```
Verifier3,凯撒移位:
!(https://xzfile.aliyuncs.com/media/upload/picture/20190311165059-cac0bc40-43da-1.png)
Verifier4,简单数字运算:
!(https://xzfile.aliyuncs.com/media/upload/picture/20190311165100-cb6ae60c-43da-1.png)
Verifier5,MD5,直接查cmd5
!(https://xzfile.aliyuncs.com/media/upload/picture/20190311165101-cc04b390-43da-1.png)
Verifier6,SHA1,同上
!(https://xzfile.aliyuncs.com/media/upload/picture/20190311165102-cc97d120-43da-1.png)
Verifier7,flag直接送了
!(https://xzfile.aliyuncs.com/media/upload/picture/20190311165103-cd2398ea-43da-1.png)
连接起来,得到完整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__+"hl"+(3)...
getattr(temp, "update")(getattr(flag, "encode")("utf-8"))
if getattr(__import__("difflib"), "SequenceMatcher")(None, getattr(getattr(temp, "hexdigest")(), "lower")(), getattr(inputs, "decode")("utf-8").lower()).ratio() != 1.0:
exit()
print("correct")
```
可以看到使用了多种混淆方式:
1. `getattr(class, method)`就相当于`class.method`
2. `"".__class__.__name__`那些用来隐藏字符串(即"String","S")
3. 最后的`SequenceMatcher(...).ratio() == 1`其实就是比较相等
于是整理出以下代码:
```
inputs = []
(lambda ... # 这段代码更新了inputs
temp = hashlib.new(... # 这里应该是使用了一种hashlib里的哈希
temp.update(flag.encode("utf-8") ) # 对flag每5位做一次哈希
print(inputs.decode("utf-8").lower()) # 这段代码是我新增的,因为最终与flag哈希值比较的和就是inputs,所以打印出来
if temp.hexdigest().lower() == inputs.decode("utf-8").lower(): # 原来应为!=,这里改成==防止exit
exit()
```
运行可以打印出几个哈希值:
```
26d33687bdb491480087ce1096c80329aaacbec7
1c3bcf656687cd31a3b486f6d5936c4b76a5d749
11a3e059c6f9c223ce20ef98290f0109f10b2ac6
6301cb033554bf0e424f7862efcc9d644df8678d
95d79f53b52da1408cc79d83f445224a58355b13
```
在CMD5上可以查到其中一部分,哈希算法是SHA1,剩余的可以在(https://hashtoolkit.com/)上查到,连接得到完整flag:
`puppyp1zzaanimetoruskitty`
## MOV
IDA加载发现全是MOV指令,可以大概知道使用了(https://github.com/xoreaxeaxeax/movfuscator)进行了混淆处理。
对于复杂一些的Movfuscator程序,可以尝试根据程序中字符串等信息,配合trace工具和下断点来追踪程序流程,并猜测程序逻辑(一般来说逻辑不会特别复杂)。此外,也可以尝试使用(https://github.com/kirschju/demovfuscator)进行反混淆,运气好的话说不定会解得比较好看。
不过对于这道题,程序中搜不到字符串信息,运行也没有反应,在strace和ltrace时发现很多SIGILL信号:
!(https://xzfile.aliyuncs.com/media/upload/picture/20190311165104-cdc8b370-43da-1.png)
于是尝试使用gdb进行调试,在发出SIGILL信号时,gdb会断住,这是可以观察到栈顶有一个`u`字符:
!(https://xzfile.aliyuncs.com/media/upload/picture/20190311165105-ce7ea978-43da-1.png)
继续跟踪下去,发现每次SIGILL时栈顶都会添加一个字符,逐渐形成一个完整的flag:
!(https://xzfile.aliyuncs.com/media/upload/picture/20190311165106-cf2556a6-43da-1.png)
```
utflag{sentence_that_is_somewhat_tangentially_related_to_the_challenge}
```
## UTCTF adventure ROM
一道gameboy游戏逆向题,使用的工具是bgb(可以debug,非常方便)和IDA。
首先使用bgb运行游戏,可以看到有四个框,分别可以输入ABCD,输入错误会死掉,显示LOSER:
!(https://xzfile.aliyuncs.com/media/upload/picture/20190311165109-d092921a-43da-1.png)
此外,地图上还有不可见的线(在题目描述中可知),碰到后也会死掉,显示DEAD:
!(https://xzfile.aliyuncs.com/media/upload/picture/20190311165108-cfe672dc-43da-1.png)
大体了解游戏逻辑后,我们就可以开始逆向了。在IDA中打开,处理器选择`z80`(具体可以参考[这份wp](https://github.com/VoidHack/write-ups/tree/master/Square%20CTF%202017/reverse/gameboy))。
首先搜索字符串,可以找到LOSER和DEAD:
!(https://xzfile.aliyuncs.com/media/upload/picture/20190311165110-d1458564-43da-1.png)
搜索第一个`DEAD`的地址`71E`,可以找到判断撞线死亡的逻辑:
!(https://xzfile.aliyuncs.com/media/upload/picture/20190311165111-d1e60e9e-43da-1.png)
这里我们直接把这部分nop掉就不会再撞死了(注意这里的nop是\x00)
同样的方法搜索`LOSER`的地址`568`:
!(https://xzfile.aliyuncs.com/media/upload/picture/20190311165112-d2a61086-43da-1.png)
找到了判断输入是否正确的判断,于是我们使用bgb在这里下断点:
!(https://xzfile.aliyuncs.com/media/upload/picture/20190311165114-d36df1b4-43da-1.png)
可以看到我们的输入和正确值分别保存在`a`和`c`寄存器中。
于是我们就可以反复运行,随便输入一个值,然后修改我们的输入值为正确值,并记录下来,即可获得完整flag:
`AABDCACBBDBCDCAD`
## 其他
剩下的几道题比较简单,有问题可以留言交流
## 官方源码
<https://github.com/UTISSS/UTCTF/> 老哥,打扰了,请教个问题。动态加载类这个反复的过程,它的终点是什么,是动态加载的新类不含有该string array的时候吗? 谢谢分享 感谢楼主分享,刚好在复现,解决了我很多疑惑! 对于拓展思路还是不错的 感谢楼主分享,有所得、谢谢!
对于拓展思路还是不错的 谢谢分享,参考学习一下 多多分享 学习学习 学习了。
页:
[1]
2