# 2019 RoarCTF RE 部分 WriteUp
## 坦克大战
本题虽然放在misc目录但个人感觉是逆向题,下载压缩包打开后是这样的:
直接找到“Assembly-CSharp.dll”使用dnspy进行分析,很容易找到位于MapManager类的游戏判胜的逻辑函数public static void WinGame():
```
public static void WinGame()
{
if (!MapManager.winGame && (MapManager.nDestroyNum == 4 || MapManager.nDestroyNum == 5))
{
string text = "clearlove9";
for (int i = 0; i < 21; i++)
{
for (int j = 0; j < 17; j++)
{
text += MapManager.MapState[i, j].ToString();
}
}
string a = MapManager.Sha1(text);
if (a == "3F649F708AAFA7A0A94138DC3022F6EA611E8D01")
{
FlagText._instance.gameObject.SetActive(true);
FlagText.str = "RoarCTF{wm-" + MapManager.Md5(text) + "}";
MapManager.winGame = true;
}
}
}
```
WinGame函数被游戏每当刷新调用实时判断游戏是否获胜,当Manager.nDestroyNum 的数值等于4或5时,判断sha1(“clearlove9”+mapstate)是否为指定值,如果是则flag为"RoarCTF{wm-" + MapManager.Md5(text) + "}",这个md5是“clearlove9”+mapdata的md5的前十个字符。
MapState为游戏当前的地图数据,观察游戏初始时的地图数据(21x17)可以得出:
8 空的
1 砖头
4 水
5 草
2 钢铁
0 家(炸了之后就是9)
其中只有砖头和家可以打碎,打碎后砖头变成空(1),家变成爆炸状态(9)。
总共有65个砖头,一个家,使用脚本遍历所有情况即可:
```
import hashlib
data = \
[
[
8,
8,
8,
8,
8,
8,
8,
8,
8,
8,
8,
8,
8,
8,
8,
8,
8
],
[
8,
8,
4,
5,
8,
1,
1,
1,
1,
1,
1,
8,
8,
8,
8,
4,
8
],
[
8,
2,
8,
1,
8,
8,
5,
1,
8,
8,
8,
1,
8,
1,
8,
4,
8
],
[
8,
5,
8,
2,
8,
8,
8,
8,
1,
8,
8,
4,
8,
1,
1,
5,
8
],
[
8,
8,
8,
8,
2,
4,
8,
1,
1,
8,
8,
1,
8,
5,
1,
5,
8
],
[
8,
8,
8,
8,
5,
8,
8,
1,
5,
1,
8,
8,
8,
1,
8,
8,
8
],
[
8,
8,
8,
1,
8,
8,
8,
8,
8,
8,
8,
8,
1,
8,
1,
5,
8
],
[
8,
1,
8,
8,
1,
8,
8,
1,
1,
4,
8,
8,
8,
8,
8,
1,
8
],
[
8,
4,
1,
8,
8,
5,
1,
8,
8,
8,
8,
8,
4,
2,
8,
8,
8
],
[
1,
1,
8,
5,
8,
2,
8,
5,
1,
4,
8,
8,
8,
1,
5,
1,
8
],
[
9,#这里是家,正确结果状态下家是被打爆的
1,
4,
8,
8,
8,
8,
8,
8,
8,
8,
8,
8,
8,
8,
8,
8
],
[
1,
1,
8,
1,
8,
8,
2,
1,
8,
8,
5,
2,
1,
8,
8,
8,
8
],
[
8,
8,
8,
8,
4,
8,
8,
2,
1,
1,
8,
2,
1,
8,
1,
8,
8
],
[
8,
1,
1,
8,
8,
4,
4,
1,
8,
4,
2,
4,
8,
4,
8,
8,
8
],
[
8,
4,
8,
8,
1,
2,
8,
8,
8,
8,
1,
8,
8,
1,
8,
1,
8
],
[
8,
1,
1,
5,
8,
8,
8,
8,
8,
8,
8,
8,
1,
8,
8,
8,
8
],
[
8,
8,
1,
1,
5,
2,
8,
8,
8,
8,
8,
8,
8,
8,
2,
8,
8
],
[
8,
8,
4,
8,
1,
8,
2,
8,
1,
5,
8,
8,
4,
8,
8,
8,
8
],
[
8,
8,
2,
8,
1,
8,
8,
1,
8,
8,
1,
8,
2,
2,
5,
8,
8
],
[
8,
2,
1,
8,
8,
8,
8,
2,
8,
4,
5,
8,
1,
1,
2,
5,
8
],
[
8,
8,
8,
8,
8,
8,
8,
8,
8,
8,
8,
8,
8,
8,
8,
8,
8
]
];
text = ''
for i in range(21):
for j in range(17):
text += str(data[i][j])
text = list(text)
def go(data,index,num):
if num == 3:
temp = ''.join(data)
if hashlib.sha1('clearlove9'+temp).hexdigest() == '3f649f708aafa7a0a94138dc3022f6ea611e8d01':
print hashlib.md5('clearlove9'+temp).hexdigest().upper()[:10]
return
if index == 21*17:
return
if data[index] == '1':
temp = list(data)
temp[index] = '8'
go(temp,index+1,num+1)
go(data,index+1,num)
go(text,0,0)
```
最终得到flag:
RoarCTF{wm-805CEC3545}
## polyre
本题使用ollvm进行混淆,使用腾讯实验室的deflat脚本进行处理。
这样可读性就增加了不少,容易看出程序验证逻辑为:
```
For each_8_bytes in input:
For _ in range(64):
if int64(each_8_bytes) >= 0:
each_8_bytes *= 2
Else:
Each_8_bytes *= 2
Each_8_bytes ^= 0xB0004B7679FA26B3
校验每个each_8_bytes == 指定常数
```
这里队友直接用z3解出来了:
```
from z3 import *
x = [BitVec('x[%d]'%i,64) for i in xrange(6)]
s = Solver()
cipher = [0xBC8FF26D43536296, 0x520100780530EE16, 0x4DC0B5EA935F08EC, 0x342B90AFD853F450, 0x8B250EBCAA2C3681, 0x55759F81A2C68AE4]
for i in xrange(6):
for _ in xrange(64):
x[i] = If(x[i] > 0, 2 *x[i], ((2 * x[i]) ^ 0xB0004B7679FA26B3))
s.add(x[i] == cipher[i])
if s.check() == sat:
m = s.model()
import struct
dic = [(str(m[i])[-2:-1], struct.pack('<Q', m[m[i]].as_long().real)) for i in xrange(6)]
dic.sort(key = lambda x: x[0], reverse = False)
flag = ''.join(map(lambda x: x[1], dic))
print flag
else:
print 'no'
```
后来想了一下这个是可以逆向算的,也就是说加密的每个分支通过加密后的数据是可以推算出来的:
因为每次乘以2后,数据最低位为0,而抑或0xB0004B7679FA26B3之后数据最低位必然为1,这样根据最低为是否为1即可反向推出上一次是走了哪个分支。
## driverCuora
本题是linux驱动逆向,但是和逆用户层感觉没啥区别,就是不会调试。。。
大致流程如下:
1. 初始化函数中,从/home/ppprocce/ssooor文件中读取一个17字节的路径,并计算了一个类似于crc32的东西:
```
v2 = -1;
v3 = a1 + a2;
while ( a1 != v3 )
{
++a1;
v5 = 0;
v6 = 0;
do
{
v4 = *(unsigned __int8 *)(a1 - 1);
if ( _bittest(&v4, v6) )
v5 |= 1 << (7 - v6);
++v6;
}
while ( v6 != 8 );
v2 ^= *(unsigned __int8 *)(a1 - 1) << 24;
v7 = 8;
do
{
if ( v2 >= 0 )
v2 *= 2;
else
v2 = 2 * v2 ^ 0x4C11DB7;
--v7;
}
while ( v7 );
}
```
对之前读到的路径字符串每个字节为一轮进行计算,先把每个字节按bit倒序,然后算crc32的一轮,初始为-1,二项式常数0x4C11DB7,这样算出来的“crc32“把低16bit在倒序,然后整个“crc32”取反。
2. 在驱动退出回调中,将之前的路径字符串冒泡排序,然后每个字节^1,看是否等于给定数据,(这里可以获取到乱序的正确路径///CFRpTagframol),然后还会对crc32进行校验,不对则直接退出。
3. 若是正确,会从刚刚的路径中读取对应的文件,并对文件中的内容(即flag),进行校验,校验逻辑为:
```
Data1 = 正确路径*2
Data2 = flag #34字节
For byte1,byte2 in data1,data2:
ROL(byte1^0x55+byte2^0xAA,2) == table1[i]
((Byte2^0x55+byte1^0xAA)>>2) | ((byte1^0x55+byte2^0xAA)<<6) == table2[i]
```
当时的思路是首先算出正确路径的顺序,想到全排序爆破,但是失败了,主要由于:
1.ida的f5出来的crc32那段代码有点问题,后来才发现。
2.实际试了爆破速度爆不完。
```
#include <stdio.h>
#include <stdlib.h>
__int64 sub_F5(__int64 a1, unsigned int a2)
{
int v2; // edx
__int64 v3; // r11
int v4; // esi
int v5; // er8
unsigned int v6; // eax
signed int v7; // eax
int v8; // eax
int v9; // esi
v2 = -1;
v3 = a1 + a2;
while ( a1 != v3 )
{
++a1;
v5 = 0;
v6 = 0;
do
{
v4 = *(unsigned __int8 *)(a1 - 1);
if ( (v4>>v6)&1 )
v5 |= 1 << (7 - v6);
++v6;
} //这里f5给出的伪代码和实际不符
while ( v6 != 8 );
v2 ^= *(unsigned __int8 *)(a1 - 1) << 24;
v7 = 8;
do
{
if ( v2 >= 0 )
v2 *= 2;
else
v2 = 2 * v2 ^ 0x4C11DB7;
--v7;
}
while ( v7 );
}
v8 = 0;
v9 = 0;
do
{
if ( v2 & (1 << v9) )
v8 |= 1 << (15 - v9);
++v9;
}
while ( v9 != 32 );
return (unsigned int)~v8;
}
#define N 17
char a[17] = {47, 47, 47, 67, 70, 82, 84, 97, 97, 102, 103, 108, 109, 111, 112, 114, 116};
void swap(int i, int offset)
{
int temp;
temp = a[offset];
a[offset] = a[i];
a[i] = temp;
}
void print()
{
if(sub_F5((__int64)&a,17)==0x5A9E037C)
{
int i;
for(i = 0; i < 17; i++)
printf(" %c ",a[i]);
printf("\n");
}
}
void perm(int offset)
{
int i, temp;
if(offset == N-1)
{
print();
return;
}
for(i = offset; i < N; i++)
{
swap(i, offset);
perm(offset + 1);
swap(i, offset);
}
}
int main()
{
perm(0);
}
```
最后路径是通过flag猜出来的。。。首先根据flag前面是RoarCTF{反向猜出路径前几个,然后根据猜测补齐。
最终得到/tmp/RoarCTF/flag,根据算法直接解密:
```
res = [0x000000C9,0x0000009B,0x0000000C,0x000000F7,0x0000008D,0x00000014,0x00000098,0x00000014,0x00000010,0x00000077,0x00000022,0x00000063,0x000000F0,0x000000A0,0x000000DC,0x000000DB,0x00000037,0x0000004D,0x00000058,0x000000EF,0x00000013,0x000000BD,0x00000096,0x000000BC,0x00000090,0x00000084,0x000000BB,0x0000001B,0x000000C7,0x00000025,0x000000F3,0x0000005C,0x000000FE,0x00000024]
#这个表就是之前说的table1
for i in range(len(res)):
res[i] = ((res[i]>>2)|(res[i]<<6))&0xff
#这里反过来算就好了
data1 ='/tmp/RoarCTF/flag'*2
data1 = map(ord,data1)
data2 = [0]*34
for i in range(34):
data2[i] = ((res[i]-(data1[i]^0x55))&0xff)^0xAA
print ''.join(chr(i%128) for i in data2)
```
所以这里只用了第一个table就出来了,后来想想其实根本不用猜路径,直接不管crc32把路径和flag都当作符号变量,直接根据两个table表的数据用z3解就行了,虽然没试但感觉应该能解出来。
## math
这题需要解一个浮点数方程用sympy没有解出来,后来用室友的matlab发现可以秒解。
程序首先验证了一个输入是否为sqrt(2),好像测试用的,没啥用。
然后再有两个输入x y,满足如下条件:
```
1.((a-(sqrt(2*sqrt(5)+10)+1))*(sqrt(5)-1)-(b-1)*((sqrt(2*sqrt(5)+10)+1)-(sqrt(2*sqrt(5)+10)+1)))**2/((b-1)**2+(a-(sqrt(2*sqrt(5)+10)+1))**2)==0.38197**2
2. ((a-1)*(sqrt(5)-1)-(b-1)*((sqrt(2*sqrt(5)+10)+1)-1))**2/((b-1)**2+(a-1)**2)==(sqrt(5)-1)**2
3.a=x*cos(y)
4.b=x*sin(y)
```
其中0.38197这个常数是用一系列随机数生成的,具体的生成好像很复杂。。。这个数值我是通过二分法直接手工爆破的。
使用matlab解方程:
```
syms a b x y;
[a_,b_,x_,y_]=vpasolve([((a-1)*(sqrt(5)-1)-(b-1)*((sqrt(2*sqrt(5)+10)+1)-1))^2/((b-1)^2+(a-1)^2)==(sqrt(5)-1)^2,((a-(sqrt(2*sqrt(5)+10)+1))*(sqrt(5)-1)-(b-1)*((sqrt(2*sqrt(5)+10)+1)-(sqrt(2*sqrt(5)+10)+1)))^2/((b-1)^2+(a-(sqrt(2*sqrt(5)+10)+1))^2)==0.38197^2,a==x*cos(y),b==x*sin(y)],[a,b,x,y]);
```
最终得到:
X = -5.2057229787614023021024241671716
Y = 35.228348316759497246192425417098