极客大挑战 2020 Re Write up

Posted by Qfrost on 2020-11-22
Estimated Reading Time 11 Minutes
Words 2.8k In Total
Viewed Times

极客大挑战真是超棒的新生赛,感谢四叶草

No RE no gain

下载并安装学习IDA

我真不会写驱动

别被hint唬到了,这玩意压根就不需要调试驱动。

IDA打开驱动文件,Shift+F12 字符串搜索,发现flag

(就这就这?

WhatsApk

了解apk结构 https://blog.csdn.net/Qiled/article/details/80984385

用ApkTool Decoding这个apk,可以得到其所有的配置文件。用Vscode打开整个文件夹,全局搜索“SYC”即可搜索到flag

HelloAndroid

考apk的反编译

先用dex2jar工具将apk反编译成jar文件,然后用jadx打开jar文件。可以看到有个LoginActivity类,在updateUiWithUser方法中可以看到密码和flag

re00


IDA载入,在main函数F5,可以看到对输入做了一个简单的异或0x44的操作,然后与cipher作比较。异或运算具有可移项的性质,即

1
2
3
a ^ b = c
等同于
a = b ^ c

根据这个性质可以写出解密脚本

1
2
3
4
5
6
7
8
9
10
ciper = [
0x17, 0x1D, 0x07, 0x3F, 0x37, 0x2D, 0x29, 0x34, 0x28, 0x21,
0x1B, 0x37, 0x2D, 0x29, 0x34, 0x28, 0x21, 0x1B, 0x3C, 0x2B,
0x36, 0x36, 0x36, 0x1B, 0x36, 0x2D, 0x23, 0x2C, 0x30, 0x7B,
0x7B, 0x39
]
flag = ""
for i in range(32):
flag += chr(ciper[i] ^ 0x44)
print(flag)

PS:IDA选中数据后按Shift + E键即可按各种格式提取选中的数据

maze

题目名即提示了这道题是个迷宫,有关迷宫的介绍可以参考CTF-wiki:https://wiki.x10sec.org/reverse/maze/maze/


IDA载入可以看到,这个迷宫不是很大,也就是说我们找到了迷宫的行数和列数,将迷宫输出,然后对着可行路线就可以写出正确路径。判断迷宫的行数和列数其实非常简单


可以看到,'a’表示左移,'d’表示有移,'s’表示下移,'w’表示上移。这些操作分别会对place变量做加减操作。而若是向上或者向下place会减31或者加31,也就是说这个迷宫每一行有31个元素,然后我们用迷宫的总长度除以每一行的长度即可得到这是一个 9x31 的迷宫,然后我们就可以用Python脚本将这个迷宫输出了

1
2
3
4
5
6
7
maze = "___________ooooo_____o________o_ooooooooo__ooo__ooo_oooooooooo_oooooooooo__o__oooo_oooooooooo________oooo___ooooo_ooooooooooooooooo_oooooooooooo_ooooooooooooooooo_ooooo_oooooo_ooooooooooooooooo_ooooo_oooooo_oooooooooos_______ooooo_oooooo__________Eooooooooooooooooooooooooooooooo"
for i in range(9): # 9x31
for j in range(31):
print(maze[i*31+j], end="")
print()

# flag: 7*'d' + 4*'w' + 7*'a' + 3*'w' + 10 * 'd' + 'sdsdsdd' + 'wdwdwdddd' + 'sssssss' + 10* 'd'

Hello .NET


用dnSpy载入,再MainWindow类中找到Check函数,里面便是验证算法。可以看到有两个While,第一个While是list的生成,观察可发现这个过程与我们的输入无关,因此我们可以在逆向脚本中直接抄,
第二个While就是一个简单的加减过程,移项直接写出逆向脚本

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
#include <cstdio>
#include <string.h>
int length = 22;
int ciper[] = {18, 14, 40, -14, -2, 30, 10, 42, 35, 48, 43, 49, 52, 72, 57, 68, 86, 145, 115, 128, 115, 86};
int list[100] = {0};

int main() {
int num = 99, cnt = 0; // 第一部分直接抄算法
while (cnt < 22) {
bool flag = true;
for (int i = 3; i < num; i += 2) {
bool flag2 = num % i == 0;
if (flag2) {
flag = false;
break;
}
}
bool flag3 = flag;
if (flag3)
list[cnt++] = num;

num += 2;
}

for(int i=0;i<22;++i)
printf("%c", list[i] - ciper[i]);
}

https://baijiahao.baidu.com/s?id=1668357322820215366&wfr=spider&for=pc

刘壮的BaseXX


IDA载入,可以看到总共对输入做了两次变换,第一次做了一些位运算,第二次做了一个Base64编码,但是可以看到用于Base64的置换表被更换了


根据Base64编码的原理,写脚本先将密文置换为正常base64编码后的密文,然后对其做Base64解密即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import base64
fake_box = "zyxwvutsrqponmlkjihgfedcbaABCDEFGHIJKLMNOPQRSTUVWXYZ9876543210-_"
box = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
ciper = "maj7TmztjquUN8Xm-hKplvaYfEAxrUnIc51qxlKOwCKN4XsdzBmjOd_-"
mid_base64 = ""

ciper_list = list(ciper)
for i in ciper_list:
mid_base64 += box[fake_box.find(i)]
print(mid_base64)
mid = base64.b64decode(mid_base64.encode())
print(mid)

flag = ""
for i in range(len(mid)):
value = mid[i] ^ i
value = ( ((value*16) | (value>>4))&0xFF )
flag += chr(value)
print(flag)

参考资料:

Base64编码 https://blog.csdn.net/aozeahj/article/details/52507352
IDA debug调试 https://www.leadroyal.cn/?p=370

un_snake

考点:pyc反编译,python语法,简单算法逆向

首先拿到题目可以看到是一个pyc文件,用uncompyle6.exe对其进行反编译(我也不知道为什么用在线工具会反编译失败,可能是版本太新了)

  • uncompyle6.exe un_snake.cpython-38.pyc un_snake.py

然后就可以还原出它的源代码

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
# un_snake.py
import this
from base64 import *

def pre(data):
th1s = 'TBESCFSRSAEUITANAIIN'.encode()
if (data_len := len(data)) > (th1s_len := len(th1s)):
th1s = th1s * (data_len // th1s_len) + th1s[:data_len - th1s_len]
return bytes(map(lambda x, y: x ^ y, data, th1s))


def enc(plain):
plain = list(plain)
plain = plain[::-1]
for i in range(len(plain)):
c = plain[i]
plain[i] = (c << 3 | c >> 5) & 255
else:
for i in range(len(plain) - 1):
plain[i] ^= plain[(i + 1)]
else:
return bytes(plain)


def check(a):
return b64encode(a) == b'mEiQCAjJoXJy2NiZQGGQyRm6IgHYQZAICKgowHHo4Dg='


if __name__ == '__main__':
print()
while True:
stuff = input('Now input you flag:')
stuff_ready = pre(stuff.encode())
result = check(enc(stuff_ready))
if result:
print('You get it! Python is so charming right?')
break
else:
print('Failed, try again!')

print('[🐍] Commit you flag, see you next time!')
# okay decompiling .\un_snake.cpython-38.pyc
File 'un_snake.cpython-38.py' doesn't exist. Skipped
#
# Successfully decompiled file

可以看到,pre函数就是对Box 'TBESCFSRSAEUITANAIIN’做拓展,将其的长度拓展到我们输入的长度,然后逐字节对我们的输入进行异或。然后enc函数里先对plain逆序,然后将每字节的前3位和后5位做了交换,最后做了相邻异或的操作。在check函数中将输入再做了一次base64加密后与密文’mEiQCAjJoXJy2NiZQGGQyRm6IgHYQZAICKgowHHo4Dg='做比对。算法还是非常简单的,如果熟悉Python能非常迅速的写出逆向脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import base64
cipher = list(base64.b64decode('mEiQCAjJoXJy2NiZQGGQyRm6IgHYQZAICKgowHHo4Dg='))

def pre(data):
th1s = 'TBESCFSRSAEUITANAIIN'.encode()
if (data_len := len(data)) > (th1s_len := len(th1s)):
th1s = th1s * (data_len // th1s_len) + th1s[:data_len - th1s_len]
print("box:", th1s) # 拓展后的Box
return bytes(map(lambda x, y: x ^ y, data, th1s))


for i in range(len(cipher)-2, -1, -1):
cipher[i] ^= cipher[i+1]
else:
for i in range(len(cipher)):
c = cipher[i]
cipher[i] = (c >> 3 | c << 5) & 255
else:
cipher = cipher[::-1]

flag = pre(cipher) # 再做一次相同的异或即可得到原值
print(flag)

Easy_virus

考点:多线程反调试,文件读写,dll文件头修复

起运行可以看到这个程序是没有输入的,IDA载入后会发现main函数非常非常大,对几千个变量做了赋初始值的操作,并调用了WriteFile写文件API,看到这里基本就可以确定这道题会将关键代码或者字符串先通过赋初始值的方法保存到变量然后用WriteFile将它们写到文件里,那这道题的关键其实就是找到生成文件的路径然后找到那个文件就可以了。

通过查文档就可以知道WriteFile的参数接受一个文件的句柄,而这个文件句柄由前面CreateFileA API得到

其实都已经不用再去查CreateFileA文档了,IDA的自动批准功能都已经告诉你FileName在哪个参数位置上了。选中十六进制Hex按‘R’键可以将其转换为字符串形式,我们就得到了输出Dll的路径:
C:\SYC_DLL.dll

然后这里有个问题,可能你会发现运行后这个程序后C盘根目录下并没有生成这个Dll,这是因为在C盘根目录下创建文件需要管理员权限,而你并没有以管理员权限运行这个程序,因此权限不足导致Dll文件创建失败(动调时可以看到CreateFileA返回值为-1) 解决方法就是以管理员身份运行这个程序就可以了。

其实逆到这一步都不需要去动调了直接去C盘拿Dll就行了。DLL的文件格式与exe可执行文件是一样的,都是PE格式,但是这个Dll不能直接用通过IDA加载。检查发现Dll的头4字节,即PE文件的文件头不对。
用16进制编辑器(010editor,winhex都可以)从别的x86的Dll抄前4字节复制过来就可以用IDA正常解析它了

在SYCfuction中看到Flag

如果一定要动态调试这题也行,可以看到此题所用的反调试代码均在StartAddress这个函数里,然后被CreateThread API注册为多线程函数。并且这个多线程函数内并没有任何影响DLL生成的代码,即,这个函数里的所以代码都是用于实现反调试的。 换而言之,这个函数不执行对我们得到DLL不存在任何影响。那我们可以将这6个push和一个call全部patch掉或者在动调调试的时候用Set IP功能跳过它们,然后就可以正常调试了。

baby_re

首先IDA载入,PDB加载过程就可以看到出题人路径中有反调试这三个字,瞒猜这题考点就是过反调试

果然,在main函数输入的前面,看到了一个函数,里面全部都是反调试代码

但是仔细观察这个反调试函数存在的位置,可以看到它出现在输入的前面。这意味着我们可以先直接运行程序但不输入,在IDA上输入后的任一位置下好断点然后attach上去,就可以轻易过掉这个反调试

而后,进入Algorithm加密函数,这个函数里可以看到出题人故意把代码写的很乱,但仔细观察可以发现,它做的就是单字节一次异或的加密,并且前面那部分运算都是固定死的,其不会随你输入的改变而改变(但是其会因你输入长度的改变而变化)

那怎么解呢?最简单的方法,在这个地方下一个断点,for循环的每一轮记录这个与输入异或的box的值,最后将box的值与密文异或即可得到答案

从汇编的角度看就是在这个异或的地方下断,然后每次记录EAX的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
box = [
0xB7, 0x4C, 0x87, 0x96, 0xF6, 0x45, 0x2E, 0x4F, 0xC8, 0x4C,
0xBC, 0xC2, 0x18, 0x65, 0xA9, 0xE4, 0xD8, 0x85, 0x89, 0xB8,
0x6A, 0x53, 0x3E, 0xF8, 0x69, 0xA1, 0x1A, 0xE1, 0x2F, 0x72,
0x2B, 0xA5, 0xF6, 0xBB, 0x14, 0x03, 0xB1, 0xCC, 0x50, 0xA4,
0x39, 0xA2, 0xDA, 0x97
]

ciper = [
0xE4, 0x15, 0xC4, 0xED, 0xA6, 0x2F, 0x56, 0x10, 0xBB, 0x13,
0xEB, 0xAD, 0x75, 0x56, 0xC7, 0xBB, 0xBB, 0xE9, 0xB9, 0xCC,
0x02, 0x3A, 0x50, 0x9F, 0x36, 0x90, 0x69, 0xBE, 0x7C, 0x42,
0x44, 0xCA, 0xC6, 0xD4, 0x24, 0x5C, 0xD2, 0xB9, 0x24, 0xC1,
0x18, 0x93, 0xB3, 0xEA
]

flag = ""
for i in range(len(ciper)):
flag += chr( box[i]^ciper[i] )
print(flag)

golang11

Go语言编译程序, IDA载入后通过字符串可以看到添加了upx壳

Linux下可以直接通过 “upx -d” 命令脱壳,脱壳后用 “golang_loader_assist.py” 脚本恢复符号表,然后动调跟一下

可以看到,算法就是简单的一次异或,伪代码看起来有点乱,去汇编上下个断点

可以发现就是和0-7做了下异或,很快就能写出exp

1
2
3
4
5
6
7
8
9
10
11
12
13
ciper = [
0x53, 0x58, 0x41, 0x78, 0x70,
0x6D, 0x63, 0x58, 0x62, 0x35,
0x60, 0x7A, 0x5B, 0x62, 0x36,
0x6B, 0x61, 0x6F, 0x65, 0x5C,
0x76, 0x60, 0x70, 0x7A, 0x00
]

flag = ""
box = [i for i in range(8)]
for i in range(len(ciper)):
flag += chr( ciper[i] ^ box[i%8] )
print(flag)