逆向部分
0x400796处把输入的32字符解析成16字节,题目要求输入为数字和小写,这里要求字母只能是0~f。
0x400960这里是核心。整理一下有三个数组,下面记为a,b,c。它们长度都是0x4000(即题目的16384),a和c已经有初值,b被赋值0~0x3fff。先进入下一层函数,回头再分析。
0x400875此函数接收4个参数,除去最后一个是长度(0x4000)外,就是3个。此函数命名为Add,后面会解释。
void __fastcall Add(int *a1, int *a2, void *a3, int len)
{
int v4; // [sp+4h] [bp-2Ch]@1
int i; // [sp+24h] [bp-Ch]@1
_DWORD *src; // [sp+28h] [bp-8h]@1
v4 = len;
src = malloc(4LL * len);
for ( i = 0; i < v4; ++i )
src[i] = a1[a2[i]];
memcpy(a3, src, 4LL * v4);
free(src);
}
顺次以a2作下标,按下标拿a1的值,放到新数组里面,然后复制给a3。注意到我们给Add函数传参的时候,a和b的值和下标全都是0~0x3fff的不重复的值,所以这将是一次置换操作。记置换操作为~
,Add(a, b, c, len)
意味着c = a~b
。
回到上一层,函数命名为Change,因为是基于输入对a,b两个数组进行变换。
void __fastcall Change(const void *a4, char *input, void *a3, unsigned int len)
{
void *v5; // [sp+8h] [bp-38h]@1
signed int i; // [sp+24h] [bp-1Ch]@1
signed int v7; // [sp+28h] [bp-18h]@2
signed int j; // [sp+2Ch] [bp-14h]@2
int *b; // [sp+30h] [bp-10h]@1
void *a; // [sp+38h] [bp-8h]@1
v5 = a3;
b = malloc(4LL * len);
initial(b, len); //把b赋值0~0x3fff
a = malloc(4LL * len);
memcpy(a, a4, 4LL * len);
for ( i = 0; i <= 15; ++i )
{
v7 = input[i];
for ( j = 0; j <= 7; ++j )
{
if ( v7 & 1 )
Add(a, b, b, len);
Add(a, a, a, len);
v7 >>= 1;
}
}
memcpy(v5, b, 4LL * len);
free(b);
free(a);
}
Add函数只有两种使用,把从输入得到的值逐位取出,若为0,则a = a~a
,否则b = a~b
然后再a = a~a
。
回到main,按上面的方法变换结束后拿b的值和c对比,完全相同则答案正确。
算法部分
置换群
前面已经说过Add操作为置换,其元素构成置换群,即标题的s16384
。群的乘法运算在这里我要用Add来表述,不是因为满足交换律,是后面要用,还请注意。把此群的加法还是记为~。
这个群的单位元就是b的初值,记为O;记a的初值为A,c的值为C。
之前提到我们有两种操作,一个是b = a~b
,一个是a = a~a
,后面的式子可以记为a = 2a
。
先假设输入的比特都是0,会怎样?
1. a=A , b=O
2. a=2A, b=O
3. a=4A, b=O
…………
可见b不会变,a则每次翻倍。
再假设依次输入比特0,1,0,1,会怎样?
1. a=A, b=O
2. a=2A, b=O
3. a=4A, b=2A
4. a=8A, b=2A
5. a=16A,b=10A
显然无论如何a和b都是A的倍数。其中利用到pA+qA=qA+pA
,记作(p+q)A
,这是群的性质之一,和交换律无关。
现在我们来看看b的系数10,2进制表示刚好就是0x1010。是不是想到了什么?其实b的值就等于输入比特构成的二进制数字,很容易证明的。
最后输入若正确,b=c,由于b是a的倍数,假设b=Na,N就是我们的输入。
最后目标:解方程c=Na。
一次同余方程组
要解这个方程,当然可以遍历N。但请注意,N是个128bit的数字,根本跑不来!
注意到置换群是可能被分解为更小的置换群的,其中任一小群各元素的阶都相同,大群的阶就是小群阶的最小公倍数。不过我不知道应该怎么分解,只是从前20个元素着手给出一种方法(一开始写的0x200,后来发现根本没必要):
round=[0]*20 #周期的集合
remain=[0]*20 #到C的余数
def change(a,b): #一次变换 c=a~b
c=[]
for i in range(0x4000):
c.append(a[b[i]])
return c
for i in range(20):
A=[...] #太长了
C=[...]
temp=A
count=0
value=A[i] #取个值
place=i #此值在temp中的位置
while True:
if value==C[place]: #如果此值的位置和
remain[i]=count
temp=change(temp,A)
count+=1
place=temp.index(value)
if A[place]==value:
round[i]=count
break
print(i)
open("1.txt","w").write(str(round)+'\n'+str(remain)).close()
即提取前20个元素,设分别是
m1,m2,…,m20
作为周期,即
mi
A的位置和A的位置相同,而
r1,r2,…,r20
作为余数,即
ri
A的位置刚好和C一样,我们的N应该满足
N=ri(modmi)
。
这些
mi
可能不互素,比较麻烦,网上没找到现成工具,听说mathematical可以,手头没有,http://wolframalpha.com/试了下,数字太大好像不行。
比赛结束后自己写了个类:
from fractions import gcd
class ChineseRemainder:
def __init__(self, M, R):
assert(len(M)==len(R))
self.M=M
self.R=R
def egcd(self,a, b):
x,y, u,v = 0,1, 1,0
while a != 0:
q,r = b//a,b%a; m,n = x-u*q,y-v*q # use x//y for floor "floor division"
b,a, x,y, u,v = a,r, u,v, m,n
return b, x, y
def modinv(self,a, m):
g, x, y = self.egcd(a, m)
if g != 1:
return None
else:
return x % m
def CR(self, r1, r2, m1, m2):
return (r1*self.modinv(m2,m1)*m2+r2*self.modinv(m1,m2)*m1)%(m1*m2)
def Add(self, m, r):
self.M.append(m)
self.R.append(r)
def Union(self):
M=self.M
R=self.R
assert(len(M)>1)
m1=M[-1]
m2=M[-2]
r1=R[-1]
r2=R[-2]
m=gcd(m1,m2)
assert((r1-r2)%m==0)
z=self.CR(0,(r2-r1)//m,m1//m,m2//m)
M=m1*m2//m
x=(z*m+r1)%M
self.R[-2]=x
self.M[-2]=M
self.R.pop(-1)
self.M.pop(-1)
def UnionAll(self):
while len(self.M)>1:
self.Union()
print(self.M)
return (self.M[0],self.R[0])
后来就解出来了,刚好32字符。然后又扒了20个数据,解出来还是一样,就对了。