Writeup - ASIS CTF 2022 Quals reverse [partial]

traditional

analysis

附件是一个无壳elf64和一个flag.enc文件,猜测是要对flag.enc文件进行解密。

main函数有一个call cs:off_564C6050EB00,猜测是rust、nim之类的语言,通过搜索字符串发现有.rs可以判断是rust的程序。

直接进入到sub_564C604BFA40分析,发现这个函数汇编层面比较复杂,F5也不是很清晰。

在Linux上尝试运行elf文件:

发现触发了rust的panic,其中'We lost the flag!: Os { code: 2, kind: NotFound, message: "No such file or directory" }'引起了我们的注意,应该是程序在读取文件时发现没有找到flag文件。

我们重新在ida搜索字符串,发现了flag.txt,由此可以推断:程序读取flag.txt,将其加密,然后将加密结果存入flag.enc。另外使用findcrypt发现程序里面有base64 table。

接下来就是动态调试,在sub_564C604BFA40开头下好断点,在开始之前我还在程序同级目录下创建了flag.txt文件,写入了helloworld123321

此处在flag.txt文件不存在时会跳转到loc_555870DE126D,也就是文件不存在的情况。

此处执行完call cs:off_555870E2FF90后,会发现rsp+148h+flag_txt_data存储了文件内容的指针,即helloworld123321,说明该函数负责读取文件。

另外,call b64encode_564C604C19A0执行完之后在rsp+148h+src里发现了helloworld123321base64编码后的字符串aGVsbG93b3JsZDEyMzMyMTAw

call cs:memcpy_ptr是将This is the flag:aGVsbG93b3JsZDEyMzMyMTAw进行拼接,

接下来还将 Just decode it :P拼接到新字符串末尾,并将整体字符串进行逆序。

得到了字符串This is the flag: aGVsbG93b3JsZDEyMzMyMTAw Just decode it :P,逆序后就是P: ti edoced tsuJ wATMyMzMyEDZsJ3b39GbsVGa :galf eht si sihT

接下来是一个循环,对字符串的每一个字符进行依次遍历,并根据各种情况对字符进行计算,将计算结果组成新的字符串。

大致的计算方法是:

1
2
3
4
5
' ' -> '/'
':' -> '+'
'a' - 'z' -> (x - 78) % 26 + 97
'A' - 'Z' -> (x - 51) % 26 + 65
'0' - '9' -> (x - 43 - 5 * ((((x - 43) / 5) / 5) & 0xFE)) | 0x30

循环结束后,就是本题思路最有意思的地方了,程序将上面循环计算得到的结果作为base64解码的输入,并将解码的结果写入flag.enc文件中。

script

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
import base64
import string


with open('../flag.enc', 'rb') as f:
buffer = f.read()

data = base64.b64encode(buffer)
res = []
# [0, 17] L, U and spaces
# [-1, -18]
# [-17] is ':'
# [1] is ':'

for i, v in enumerate(data):
if chr(v) in string.ascii_uppercase:
for c in string.ascii_uppercase:
if (ord(c) - 51) % 26 + 65 == v: res += c
elif chr(v) in string.ascii_lowercase:
for c in string.ascii_lowercase:
if (ord(c) - 78) % 26 + 97 == v: res += c
elif chr(v) in '0123456789':
for c in '0123456789':
x = ord(c)
if (x - 43 - 5 * ((((x - 43) // 5) // 5) & 0xFE)) | 0x30 == v:
res += c
else:
if chr(v) == '/':
res += ' '
elif chr(v) == '+':
res += ':'
else:
res += chr(v)

print(data.decode('ascii'))
print(''.join(res))
print(''.join(res)[::-1])

因为时间匆忙,这个脚本写的不是很好,最后需要自己手动补一个}

Figole

analysis

一个apk文件,直接在手机上安装可以看到就是一个输入并检测。

使用jadx打开。

通过manifest文件发现入口在com.example.shctf.DActivity,手机点击check按钮之后会执行m210lambda$onCreate$0$comexampleshctfDActivity,另外secret、key通过l1、l2两个方法计算得到。

我们先看l1方法,cn通过base64解码得到,com.example.shctfdex.UT是很明显的java类,s是app的timesnewroman.ttf文件的路径,可以在jadx的资源文件/assets下发现。try代码块可以发现是利用java反射获取并调用com.example.shctfdex.UTgf方法。

l2方法同理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public String l1(Context context) {
String cn = base64Decode(Util.pn1); // com.example.shctfdex.UT
String s = context.getExternalFilesDir(null).getAbsolutePath() + "/" + Util.an;
try {
ClassLoader dLoader = getInst(s);
Class<?> loadedClass = dLoader.loadClass(cn);
Object obj = loadedClass.newInstance();
Method m = loadedClass.getMethod(base64Decode("Z2Y="), Context.class); // gf
String re = (String) m.invoke(obj, context);
return re;
} catch (Exception e) {
e.printStackTrace();
return e.getMessage();
}
}

我们把timesnewroman.ttf用jadx导出,可以发现是一个dex文件。那么我们直接用jadx打开:

com.example.shctfdex.UTgf方法调用了gd方法,从数据库中读取数据,也就是apk里面的calibri.ttf文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public String gd() {
String r = "null";
try {
Cursor mCur = this.mDb.rawQuery("SELECT * FROM calibri", null);
if (mCur != null) {
mCur.moveToPosition(0);
r = mCur.getString(0);
mCur.close();
return r; // 7mePfqpM6Wd1El2sj4dlUboU6PieF7La8IJ1e76cfp4=
}
return r; // 7mePfqpM6Wd1El2sj4dlUboU6PieF7La8IJ1e76cfp4=
} catch (SQLException mSQLException) {
mSQLException.printStackTrace();
return r; // 7mePfqpM6Wd1El2sj4dlUboU6PieF7La8IJ1e76cfp4=
}
}

读取数据库的方法如下:(脑子抽了把calibri.ttf重命名成calibri.ttf.dex,可以忽略)

通过以上方法分析l1、l2可以找到secret和key的值,接下来分析加密部分。

m210lambda$onCreate$0$comexampleshctfDActivity里的result通过l3计算得到,里面调用了com.example.shctfdex.DXech方法。另外,l3的最后一个参数需要cdec方法计算得到,就是一个xor操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
public String ech(String message, String s) {
try {
byte[] srcBuff = message.getBytes(StandardCharsets.UTF_8);
SecretKeySpec keySpec = new SecretKeySpec(getK(0, s + UT.gb("Mzg2OTM3NjEzNDc0MzYzMTM1MzUzMjM2MzMzMjMxMzA=")), "AES");
IvParameterSpec ivSpec = new IvParameterSpec(getK(1, s + UT.gb("Mzg2OTM3NjEzNDc0MzYzMTM1MzUzMjM2MzMzMjMxMzA=")));
Cipher ecipher = Cipher.getInstance("AES/CBC/PKCS7Padding");
ecipher.init(1, keySpec, ivSpec);
return Base64.encodeToString(ecipher.doFinal(srcBuff), 0);
} catch (Exception ex) {
ex.printStackTrace();
return "encrypt :" + ex.getMessage();
}
}

可以看出就是AES加密,Mode为CBC,aes key和aes iv需要通过getK计算得到。

在动手之前先整理下程序的流程:

  1. secret 通过 l1 计算得到
  2. key 通过 l2 计算得到
  3. 将 secret、key传递给m210lambda$onCreate$0$comexampleshctfDActivity
  4. flag 就是我们的输入
  5. result 通过 l3 计算得到,将flag和cdec(q)以参数传递,这里的q就是key
    1. cdec函数的参数是一个hexstring,将参数转换成整数数组,并将每一个元素进行异或加密
  6. l3 调用了 ech 方法,AES的Key和IV需要getK方法进行计算
  7. 将 y (也就是secret)与result进行比较

script

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
import base64
from Crypto.Cipher import AES

salt = 'Mzg2OTM3NjEzNDc0MzYzMTM1MzUzMjM2MzMzMjMxMzA='


def get_key():
k = [chr(b) for b in bytes.fromhex(
'58504e58564f5d504a59534a58544e59564d5d504a5e524d585c4f5a564f5c57')]
result = ''
for i in range(len(k)):
result += chr(ord(k[i]) ^ ord('key'[i % 3]))

return result


def do_salt(p, s, mode: int) -> str:
def t(q):
return int(q, 16)

def h(q):
a = t(q[0])
b = t(q[1])
return ((a << 4) + b) & 0xff

def g(q):
result = [0] * (len(q) // 2)
for i in range(0, len(q), 2):
result[i // 2] = h(q[i:i+2])

return result

total = g(p + s)
n = len(total)
result = [0] * (n // 2)
for i, v in enumerate(total):
if i % 2 == mode:
result[i // 2] = v

return result


aes_key = do_salt(get_key(), base64.b64decode(salt).decode('ascii'), 0)
aes_iv = do_salt(get_key(), base64.b64decode(salt).decode('ascii'), 1)
print(''.join(map(chr, aes_key)))
print(''.join(map(chr, aes_iv)))

aes = AES.new(key=bytes(aes_key), mode=AES.MODE_CBC, iv=bytes(aes_iv))
data = aes.decrypt(base64.b64decode('7mePfqpM6Wd1El2sj4dlUboU6PieF7La8IJ1e76cfp4='))
print(data)