This page looks best with JavaScript enabled

强网杯 2021 Re 部分题解

 ·  ☕ 5 min read · 👀... views

不愧是强网杯,题目质量确实很高。不像现在很多某某杯,要么是卷大量大量的网上抄的代码,要么是为了难而偏各种奇奇怪怪的新架构

ezmath

这题就离谱,纯数学题,直接见识了各位师傅带数学家的数学功底

刚开始想的是用爆破,但发现不管是patch原程序用程序本身爆还是动态插桩还是模拟执行都爆不出来。猜测程序本身的check逻辑就是有问题的。直接扣算法算发现因为精度问题到后面全部截断了。

发现程序在main函数前做了一些操作。在init函数里会算出V3,因为精度问题,用它是算不出flag的。将init里整个表达式列出来,带回到main的check里,做二分趋近可得flag。
Sub_1301里是泰勒展开式,Sub_11C9是辛普森积分,以下是我的数学推导过程(三个师傅算了一晚上才算出来 呜呜呜)

ezmath_1

 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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
#include <stdio.h>
#include <math.h>
#include <iostream>

double cipher[] = { 0.00009794904266317233, 0.00010270456917442, 0.00009194256152777895, 0.0001090322021913372, 0.0001112636336217534, 0.0001007442677411854, 0.0001112636336217534, 0.0001047063607908828, 0.0001112818534005219, 0.0001046861985862495, 0.0001112818534005219, 0.000108992856167966, 0.0001112636336217534, 0.0001090234561758122, 0.0001113183108652088, 0.0001006882924839248, 0.0001112590796092291, 0.0001089841164633298, 0.00008468431512187874 };
char flag[38] = {0};

int N;

// e^x · x^n
double Content(double t) {
	return exp(t)*pow(t,N);
}

double Algorithm(double x, double y, double eps) {
	double t1, t2, l1, l2;
	int n = 1; 
	double h = y - x;
	t1 = h * (Content(x) + Content(y)) / 2.0;
	l1 = t1;
	double ep = eps + 1.0;
	while (ep >= eps) {
		double p = 0.0;
		for (int k = 0; k <= n - 1; k++) {
			double tmp = x + (k + 0.5) * h;
			p = p + Content(tmp);
		}
		t2 = (t1 + h * p) / 2.0;
		l2 = (4.0 * t2 - t1) / 3.0;
		ep = fabs(l2 - l1);
		t1 = t2; l1 = l2; 
		n += n; 
		h /= 2.0;
	}
	return l2;
}

int main()
{
	for (int i = 0; i < 19; i++) {
		int l = 0x3030;
		int r = 0x100000;
		// 做二分趋近
		while (l < r) {
			if (r - l <= 1) {
				N = l;
				double r1 = Algorithm(0.0, 1.0, 4e-10);
				N = r;
				double r2 = Algorithm(0.0, 1.0, 4e-10);
				if (std::abs(r2 - cipher[i]) < std::abs(r1 - cipher[i])) {
					N = r;
					break;
				}
				else {
					N = l;
					break;
				}
			}
			N = (l + r) / 2;
			double result = Algorithm(0.0, 1.0, 4e-10);
			if (result > cipher[i])
				l = N;
			else
				r = N;
		}
		flag[i * 2] = N & 0xff;
		flag[i * 2 + 1] = N >> 8;
        printf("%c%c", flag[i * 2], flag[i * 2 + 1]);
	}
	printf("\n%s\n", flag);
	return 0;
}

看了别的师傅的解法,我直接膜拜。这个式子可以抽象成数列
ezmath_2

然后…一行代码解决(tqltql
ezmath_3

StandOnTheGiants

apk逆向,jadx打开看到所有check逻辑都在so里。IDA打开so发现里面逻辑挺复杂。动调附加so,发现断不下来,看Modules列表发现so被隐藏了,base.apk就是so文件(有点像Windows下的反射dll或者说无模块注入)。Rebase过去就可以正常调试了。
在check函数开始处会先对输入做hex,然后初始化RSA的各种向量最后对输入做RSA,后面是一个换表的base64。RSA p和q可以直接用在线工具质数分解出来。但是解的时候发现正向输进去的输入和预期完全相同,但是密文怎么都解不出来。来回调了很久最后发现这tm表有二义性。魔改后的Base64的表具有二义性,带了两个+两个-。我的解决办法是找出密文种所有的‘+’和‘-’,然后脚本遍历密文,遇到这两个符号就分裂出两种状态,分别用4个别的符号填充他们
这样能分裂得到2^14条密文(因为±共有14个),对这些密文解RSA,即可找到flag

 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
import copy, base64
import libnum
from gmpy2 import invert

def gen():
    cipher = list("bborOT+ohG*,U:;@/gVIAZ-,t++LaZkOrk?UcSOKJ?p-J+vuSN?:e,Kc/?h-oH?:tthoqYYSPp-ZC+Yw:*jrxPymGYO/PvDOIivNYtvJ?Mi*GG+/lmqEysrTdSD+eP+moP+l?+Np/oK=")
    p=[[6, 0], [22, 1], [25, 0], [26, 0], [43, 1], [45, 0], [59, 1], [74, 1], [77, 0], [110, 0], [123, 0], [126, 0], [130, 0], [133, 0]]
    box=[['[',']'],['(',')']]
    for i in range(2**14):
        for j in range(14):
            cipher[p[j][0]]=box[p[j][1]][(i>>j)&1]
        yield "".join(cipher)
        



# 质数分解 http://factordb.com/
p=33372027594978156556226010605355114227940760344767554666784520987023841729210037080257448673296881877565718986258036932062711
q=64135289477071580278790190170577389084825014742943447208116859632024532344630238623598752668347708737661925585694639798853367
n=p*q
def rsa(m):
    m=libnum.s2n(m)
    c=pow(m,invert(0x10001,(p-1)*(q-1)),n)
    c=libnum.n2s(int(c))
    if c.startswith(b'flag{'):
        print(c)
        return True
    return False


fakebox = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ*[,(./:;?@])"
box = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" 
table = ''.maketrans(fakebox, box)
for l in gen():
    if rsa(base64.b64decode(l.translate(table))):
        break

LongTimeAgo

exe程序,IDA打开后可以看到里面代码特别乱,有很多垃圾指令,还用了大量的结构体。前后跟了几次,在main函数里会先对输入每8字节一组做hex,然后将这组密文连同正确的加密密文填入结构体
LongTimeAgo_1

填充完毕后下面四个一样的函数sub_403460是做初始化key,分别保存在参数结构体的第二个成员里
LongTimeAgo_2

然后下面就是对前32字节输入做XTEA加密,后32字节输入做TEA加密。(这TEA和XTEA的代码跟奥里给一样,还把好多运算符写成函数,人都看傻去
LongTimeAgo_3
LongTimeAgo_4

最后再根据奇偶做了分别的异或

 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
#include <stdio.h>
#include <windows.h>
void XTeaDecrypto(DWORD v[2], DWORD const key[4]) {
    unsigned int i;
    DWORD v0 = v[0], v1 = v[1], delta = 0x8f3779e9, sum = 0xE6EF3D20;
    for (i = 0; i < 32; i++) {
        v1 -= (((v0 << 4) ^ (v0 >> 5)) + v0) ^ (sum + key[(sum >> 11) & 3]);
        sum -= delta;
        v0 -= (((v1 << 4) ^ (v1 >> 5)) + v1) ^ (sum + key[sum & 3]);
    }

    v[0] = v0; v[1] = v1;
}
void TeaDecrypto(DWORD* v, DWORD* k) {
    DWORD v0 = v[0], v1 = v[1], i;  
    DWORD delta = 0x3D3529BC;           
    DWORD sum = delta*32;         
    DWORD k0 = k[0], k1 = k[1], k2 = k[2], k3 = k[3];   
    for (i = 0; i < 32; i++) {                        
        v1 -= ((v0 << 4) + k2) ^ (v0 + sum) ^ ((v0 >> 5) + k3);
        v0 -= ((v1 << 4) + k0) ^ (v1 + sum) ^ ((v1 >> 5) + k1);
        sum -= delta;
    }                                           
    v[0] = v0; v[1] = v1;
}
int main()
{
    DWORD key[] = { 0x0FFFD, 0x1FFFD, 0x3FFFD, 0x7FFFD };
    DWORD cipher[] = { 0x1F306772, 0x0B75B0C29, 0x4A7CDBE3, 0x2877BDDF, 0x1354C785, 0x357C3A3A, 0x738AF36C, 0x89B7F337 };

    for(int i=0;i<8;++i)
        if(i%2==0)
            cipher[i] = cipher[i]^0xfd;
        else
            cipher[i] = cipher[i]^0x1fd;

    XTeaDecrypto(cipher, key);
    XTeaDecrypto(&cipher[2], key);
    printf("flag1:%X%X%X%X\n", cipher[0], cipher[1], cipher[2], cipher[3]);

    TeaDecrypto(&cipher[4], key);
    TeaDecrypto(&cipher[6], key);
    printf("flag2:%X%X%X%X\n", cipher[4], cipher[5], cipher[6], cipher[7]);
    return 0;
}

unicorn_like_a_pro

这题比赛时没整出来,赛后复盘了。题目内部静态编译了 unicorn 框架模拟执行一段 x64代码。因为静态编译进去的,函数实在太多了,需要先用bindiff修复符号表。

因为框架并没有被魔改,可以git下unicorn的源码,然后make出来,编译一个demo然后做bindiff。我是make unicorn后,直接将目录下的 libunicorn.so.1 (unicorn的库文件)给拖出来与二进制文件做了bindiff,因为该库文件中包含了几乎全部的unicorn框架代码,故可以修复得到比较全的符号表(但并不是全部),有些函数例如 uc_open, uc_reg_write这些不知道是不是因为unicorn版本原因还是什么的,题目文件中的代码与库中代码相差较大,只能人工识别辨认。

Share on

Qfrost
WRITTEN BY
Qfrost
CTFer, Anti-Cheater, LLVM Committer