ISCTF 2022 REVERSE 部分题解
去年11月份左右为期一周的比赛,比较适合新手练习提高技能。比赛结束后有两道0解题,官方目前也没放出相应的wp。近期无意间翻到了这两道赛题中的一道就又拿来看了一眼,有一些新的思路和想法。
青春re手不会梦到密码学学姐
官方在赛后放出了源代码,相应降低了逆向的难度,源代码位置:链接: https://pan.baidu.com/s/1SV71L2SYrksLpVJJcZJNVg?pwd=yuki
两道HINT分别提示我们:
- 最后一步均为小写字母
- 不要管细枝末节,将主要内容逆出来分析算法即可
下面将根据源代码给出本道题的逆向思路:
主函数逻辑如上,输入一个格式为 ”admin{**************************}" 且长度为42的字符串,通过加密函数 VillageShopper 得到一个新的数组和 ourArray内容完全一致既表示我们输入的字符串正确,下面研究加密函数 VillageShopper
![image-20230411160722385](https://tp.shunavicii.cn/image-20230411160722385.png)
有一个找素数的函数 GetPrime(),拿得到的素数去初始化key[]数组,调用的rand()函数应该在别处初始化过随机数种子。最后返回的final函数则是加密之后的结果。输入的每一个字符经过五轮相乘然后mod p 取余得到,key[]实时更新保存阶段值。我们先看看GetPrime()的实现。
![image-20230411161829463](https://tp.shunavicii.cn/image-20230411161829463.png)
这个地方先把伪随机数产生的数的范围限制在2^16即65536以内,不断生成新数判断其是不是素数,is_prime()判断素数的原理为费马小定理(为适应本题算法做了少许修改):
如果
p
为素数且
a
<
p
,
则
a
p
−
1
≡
1
m
o
d
(
p
)
如果p为素数且a<p,则a^{p-1}\equiv 1 mod(p)
如果p为素数且a<p,则ap−1≡1mod(p)
费马小定理判断一个数是否为素数是必要不充分条件,因此上图is_prime()通过随机生成的100个a满足定理结论推断n为素数,并客观上排除了如果n为素数但(a,n)!=1的情况(生成a的范围在1–n-1)。
现在主要针对加密过程进行分析:
int final[password.length() + 1] = {};
for(int j = 0;j < password.length();j++){
long long coin = int(password[j]);
for(int k = 0;k < 5;k++){
coin = (key[k] * coin) % p;
key[k] = coin;
}
final[j] = coin;
// cout<<coin<<",";
}
加密结束后key数组保存的是最后一个字符每一次加密后的阶段值,我们需要逆向反推加密过程
-
第5次加密:此时k==4,进入第五次加密时,coin保存的是key[3](最后一个字符第四次加密后的值),此时key[4]还没有被覆盖,保存的是倒数第二个字符第五次加密完的值,也就是final[password.length()-2],我们因此可以得出等式
final[password.length()-1]==(final[password.length()-2]*key[3])%p
但每一次加密需要用到前一次加密保存下来的key,我们不妨开一个新数组lastkey[5],用于保存逆推回来的key值。
因此上式变为:
key[4]==(lastkey[4]*key[3])%p
-
第四次加密:此时k==3,coin保存的是key[2] (最后一个字符第三次加密后的值),此时key[3]还没有被覆盖,保存的是倒数第二个字符第四次加密后的值,但是这个值我们就无从知晓了,但是有如下等式:
key[3]==(lastkey[3]*key[2])%p;
对于上式,我们已知key[3],key[2],p,只需求解lastkey[3]
-
对于第二、三次加密,和第四次加密原理如出一辙
-
第一次加密:此时k==0,我们有:
key[0]==(lastkey[0]*password[password.length()-1])%p;
当时突然愣了,lastkey[0]和最后一次字符不都要我推吗?后来查看了输入的格式最后一个字符为 ‘}’,可以直接逆。
-
可是对于倒数第二个字符这种方式就失效了,即使是爆破也存在问题,根据提示输入都是小写字符,但是为了满足上式 ,每一个不同的输入对应的lastkey[0]的值就不能确定,可能会有26种情况,而要逆的字符长度为42-7=35,即有26^35种情况,不太现实。
从尾巴往前不行那就正向跑,基本思路是每一位枚举26种字符,看看加密后哪一个结果等于最后的结果即可,而基本的思想与上面的分析无大异。
现在的关键点如下:
已知a=(b*c)%p; a,c,p均已知,求解b
这里向一位密码学学霸借阅了专业书籍得出如下求解方法:
我们取的p很大,则有 :
a
≡
(
b
∗
c
)
(
m
o
d
p
)
(
1
)
a\equiv (b*c)(mod p) (1)
a≡(b∗c)(modp)(1)
因为p为质数,有(p,c)=1,令x满足下列条件:
(
c
∗
x
)
≡
1
(
m
o
d
p
)
(
2
)
(c*x)\equiv 1(mod p)(2)
(c∗x)≡1(modp)(2)
则有x和c关于模数p互为模逆元,(1)式左右两侧同时乘以x,有:
(
b
∗
c
∗
x
)
≡
(
a
∗
x
)
(
m
o
d
p
)
(
3
)
(b*c*x)\equiv (a*x)(mod p)(3)
(b∗c∗x)≡(a∗x)(modp)(3)
将(2)式变形:
(
b
∗
c
∗
x
)
≡
b
(
m
o
d
p
)
(
4
)
(b*c*x)\equiv b(mod p)(4)
(b∗c∗x)≡b(modp)(4)
结合(3),(4)得出如下结论:
(
a
∗
x
)
≡
b
(
m
o
d
p
)
(a*x)\equiv b(mod p)
(a∗x)≡b(modp)
可以依据扩展欧几里得算法求解c的模逆元x,进而求解b,至此此题关键点已突破。
但是这题在求解的过程中还是遇到了障碍:
-
首先每一次运行得到的素数都不一样,这也好理解,毕竟是拿时间差作为种子进行初始化。但是p的大小总得比最后的ourArray数组中的所有值都大吧,另外key数组每次也不一样,个人认为并不能确保最后结果的唯一性
-
即使拿cpp文件中注释掉的p值和key数组,最后逆向出来的结果尤其的大,远远超过可见字符的范围。下面是我正向爆破前6个字符的脚本
#include <bits/stdc++.h> using namespace std; const int N=1e5+1; typedef long long ll; char flag[]="admin{***********************************}"; int ans[]={8973,1807,27883,38244,18412,46044,35211,45828,56411,45341,9381,5538,53971,38131,9906,42447,54339,15554,7450,40119,49660,29869,53626,16338,38255,28023,56673,41861,43121,13369,36449,56747,41501,19896,601,28066,39188,9249,14138,43665,13409,8378}; long long key[5] = {12631, 53214, 54180, 30822, 6656}; int p = 59441; ll lastkey[5]={12631, 53214, 54180, 30822, 6656}; int exgcd(int m,int n,int &x,int &y) { if(n==0) { x=1; y=0; return m; } int gcd=exgcd(n,m%n,x,y); int temp=x; x=y; y=temp-m/n*y; return gcd; } int inv(int m,int n,int &x,int &y) { int gcd=exgcd(m,n,x,y); if(gcd!=1) { return -1; } else { return (x+n)%n; } } int main() { int x=0,y=0; int x1; bool flag1=false; for(int i=0;i<6;i++) { flag1=false; for(int z=0;z<=59440;z++) { key[0] =lastkey[0]; key[1]= lastkey[1]; key[2]= lastkey[2]; key[3]= lastkey[3]; key[4]= lastkey[4]; x1=z; for(int j=0;j<5;j++) { x1=(key[j]*x1)%p; key[j]=x1; } if(x1==ans[i]) { printf("%d:%x\n",i,z); flag1=true; for(int l=0;l<5;l++) { lastkey[l]=key[l]; } //cout<<key[4]<<endl; break; } } if(flag) { continue; } else { cout<<"error"<<i; } } }
附上爆破的结果:
0:3b52 1:978d 2:17ed 3:86ae 4:51f2 5:0dc4
我想过是不是把两个字符通过位移运算拼到一起去了,但是动调发现程序生成的数组就是一个输入对应一个输出,如下图所示:
此题至此暂时没有新的思路,可能还没get到出题人的点,欢迎各大网友留言讨论。
simple flower
两解题,当时做出来还是很开心的(做出来之后没截图只能用网上的图片了)。
本题花指令非常多,但总结起来无非两条:重复赘余,call+ret实现跳转
附上idapython的去花脚本:
a=0x407000
end=0x40f201
b=0
while a<end:
#处理反复的eax
if(get_wide_byte(a)==0x50):
for i in range(3):
if(get_wide_byte(a+i)==0x58):
patch_byte(a,0x90)
patch_byte(a+i,0x90)
else:
continue
#处理反复的ecx
elif(get_wide_byte(a)==0x51):
for i in range(3):
if(get_wide_byte(a+i)==0x59):
patch_byte(a,0x90)
patch_byte(a+i,0x90)
else:
continue
#处理反复的ebx
elif(get_wide_byte(a)==0x52):
for i in range(3):
if(get_wide_byte(a+i)==0x5A):
patch_byte(a,0x90)
patch_byte(a+i,0x90)
else:
continue
#处理反复的edx
elif(get_wide_byte(a)==0x53):
for i in range(3):
if(get_wide_byte(a+i)==0x5B):
patch_byte(a,0x90)
patch_byte(a+i,0x90)
else:
continue
#处理pushf+call下一个地址
elif(get_wide_byte(a)==0x9c and get_wide_byte(a+1)==0xE8):
patch_byte(a+2,0x0a)
patch_byte(a,0x90)
patch_byte(a+1,0xEB)
for i in range(8):
patch_byte(a+6+i,0x90)
a+=14
continue
#处理反复的xor
elif(get_wide_byte(a)==0x9c and get_wide_byte(a+1)==0x051 ):
for i in range (11):
patch_byte(a+i,0x90)
a+=11
continue
#处理call $+5和下面一部分操作
elif(get_wide_byte(a)==0x3E and get_wide_byte(a+1)==0x087 and get_wide_byte(a+10)==0x24):
for i in range (12):
patch_byte(a+i,0x90)
patch_byte(a-5,0xE9)
patch_dword(a-4,12)
a+=12
continue
#处理第一次过后落单的pushf popf
elif((get_wide_byte(a)==0x9d or get_wide_byte(a)==0x09c) and get_wide_byte(a+1)==0x90):
patch_byte(a,0x90)
a+=1
continue
#处理压入eax并对eax进行一些操作
elif(get_wide_byte(a)==0x50 and get_wide_byte(a+1)==0xB8 and
get_wide_byte(a+2)==0x02 ):
for i in range(21):
patch_byte(a+i,0x90)
a+=21
continue
#处理压入ecx并对ecx进行操作
elif(get_wide_byte(a)==0xEB and get_wide_byte(a+2)==0xC3 ):
for i in range(3):
patch_byte(a+2+i,0x90)
a+=5
continue
elif(get_wide_byte(a)==0x50 and get_wide_byte(a+1)==0xB8 and
get_wide_byte(a+2)==0x02 ):
for i in range(21):
patch_byte(a+i,0x90)
a+=21
continue
#处理压入ecx并对ecx进行操作
elif(get_wide_byte(a)==0x51 and get_wide_byte(a+1)==0xB9 and get_wide_byte(a+10)==0x83):
for i in range (21):
patch_byte(a+i,0x90)
a+=21
continue
elif(get_wide_byte(a)==0xEB and get_wide_byte(a+2)==0xC3 ):
for i in range(3):
patch_byte(a+2+i,0x90)
a+=5
continue
a+=1
a=0x407000
end=0x40f201
b=0
while a<end:
if(get_wide_byte(a)==0x50 and get_wide_byte(a+1)==0xB8 and
get_wide_byte(a+2)==0x02 ):
for i in range(21):
patch_byte(a+i,0x90)
a+=21
continue
#处理压入ecx并对ecx进行操作
elif(get_wide_byte(a)==0x51 and get_wide_byte(a+1)==0xB9 and get_wide_byte(a+10)==0x83):
for i in range (21):
patch_byte(a+i,0x90)
a+=21
continue
a+=1
因为合在一起后第二部分总是没有运行,不知道哪里出了问题,所以分开来执行
去花后从0x407000取消定义再重新生成函数,发现会一直生成到末尾,这样就可以F5了。附上阅读F5后的逆向脚本
#include <bits/stdc++.h>
using namespace std;
int main(){
unsigned int s[100]={0X19,0X37,0XD1,0xD4,
0X5C,0XC0,0X43,0x68,
0X89,0XB4,0X88,0xB3,
0XC0,0X1C,0X8C,0xA0,
0X9A,0X8E,0X19,0x24,
0XEA,0X39,0XA5,0xD2,
0X3D,0XED,0XBA,0x41,
0XA3,0XA5,0XAE,0x57,
0x73,0x9e,0x29,0x8d};
int v22[33]={0};
int v11=0;
int v19=31;
unsigned int v8;
int z;
unsigned int v14,v15;
int v13=30;
int v5=31;
int v7=0;
int v6=0;
int v9=0;
int v10;
do
{
s[0]^=s[5];
s[4]^=s[5]^s[0];
s[3]^=s[8];
s[2]^=s[7];
s[1]^=s[6];
v7=0;
v5=31;
v13=30;
z=0;
v6=0;
v9=0;
memset(v22,0,sizeof(v22));
do
{
v8 = (unsigned __int8)s[v7];
if ( v8 >= 2 )
{
v9=v6;
v10 = v8 >> 1;
do
{
v22[v9] = (int)v8 % 2;
v8 = v10;
++v9;
v10 /= 2;
}
while ( v10 );
}
++v7;
z += 4;
v6=z;
}
while ( v7 < 4 );
if ( v19 % 2 == 1 )
{
for ( int i = 0; i < 32; ++i )
v22[i] = (v22[i] == 0);
}
do
{
v14 = v22[v13 + 1];
if ( v14 == 1 )
{
v15 = v22[v13];
if ( v15 )
{
if ( v15 == 1 )
s[v13 + 5] -= s[v13 + 4];
}
else
{
s[v13 + 5] ^= s[v13 + 4];
}
}
else if ( !v14 && !v22[v13] )
{
s[v13 + 5] += s[v13 + 4];
}
--v13;
--v5;
}
while ( v5 );
v19--;
}while(v19>=0);
for (int i=0;i<36;i++)
{
printf("%c",s[i]);
}
}