默小白 发表于 2019-3-18 14:31

【转帖】UTCTF逆向题详解

转自: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文件中就是两个字符串之间还有一段没有用的数据:

!(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/>

qiuTruth 发表于 2019-3-19 23:22

老哥,打扰了,请教个问题。动态加载类这个反复的过程,它的终点是什么,是动态加载的新类不含有该string array的时候吗?

woshicn 发表于 2019-3-18 14:38

谢谢分享

qiuTruth 发表于 2019-3-18 14:45

感谢楼主分享,刚好在复现,解决了我很多疑惑!

qdam 发表于 2019-3-18 17:12

对于拓展思路还是不错的

ts0001 发表于 2019-3-18 18:07

感谢楼主分享,有所得、谢谢!

一叶知夏 发表于 2019-3-18 21:26


对于拓展思路还是不错的

GaiaDC 发表于 2019-3-18 21:27

谢谢分享,参考学习一下

rickw 发表于 2019-3-19 07:16

多多分享

fentian_85 发表于 2019-3-19 07:52

学习学习

WA自动机 发表于 2019-3-19 08:57

学习了。
页: [1] 2
查看完整版本: 【转帖】UTCTF逆向题详解