ISCTF 2022 REVERSE

ISCTF 2022 REVERSE 部分题解

去年11月份左右为期一周的比赛,比较适合新手练习提高技能。比赛结束后有两道0解题,官方目前也没放出相应的wp。近期无意间翻到了这两道赛题中的一道就又拿来看了一眼,有一些新的思路和想法。

青春re手不会梦到密码学学姐

在这里插入图片描述

官方在赛后放出了源代码,相应降低了逆向的难度,源代码位置:链接: https://pan.baidu.com/s/1SV71L2SYrksLpVJJcZJNVg?pwd=yuki

两道HINT分别提示我们:

  • 最后一步均为小写字母
  • 不要管细枝末节,将主要内容逆出来分析算法即可

下面将根据源代码给出本道题的逆向思路:

image-20230411155652787

主函数逻辑如上,输入一个格式为 ”admin{**************************}" 且长度为42的字符串,通过加密函数 VillageShopper 得到一个新的数组和 ourArray内容完全一致既表示我们输入的字符串正确,下面研究加密函数 VillageShopper

image-20230411160722385

有一个找素数的函数 GetPrime(),拿得到的素数去初始化key[]数组,调用的rand()函数应该在别处初始化过随机数种子。最后返回的final函数则是加密之后的结果。输入的每一个字符经过五轮相乘然后mod p 取余得到,key[]实时更新保存阶段值。我们先看看GetPrime()的实现。

image-20230411161829463

这个地方先把伪随机数产生的数的范围限制在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,ap11mod(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(bc)(modp)(1)
​ 因为p为质数,有(p,c)=1,令x满足下列条件:
( c ∗ x ) ≡ 1 ( m o d p ) ( 2 ) (c*x)\equiv 1(mod p)(2) (cx)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) (bcx)(ax)(modp)(3)
​ 将(2)式变形:
( b ∗ c ∗ x ) ≡ b ( m o d p ) ( 4 ) (b*c*x)\equiv b(mod p)(4) (bcx)b(modp)(4)
​ 结合(3),(4)得出如下结论:
( a ∗ x ) ≡ b ( m o d p ) (a*x)\equiv b(mod p) (ax)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
    

    我想过是不是把两个字符通过位移运算拼到一起去了,但是动调发现程序生成的数组就是一个输入对应一个输出,如下图所示:

    image-20230412195315339

此题至此暂时没有新的思路,可能还没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]);
	}
}

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值