逆向工程实验——lab6(linux、windows64位逆向)

1. Yet another crackme (or rather keygenme).

Executables: Linux x64 Windows x64
It is just to be run with name
and serial number as command line arguments. Valid ones are:
2Z7A7-EK270-TMHR4-BHC71-CEB52-HELL0-HELL0-EONP9
2Z7A7-6I7R9-MZGO9-FDQJ3-JN0Q6-HELL0-HELL0-72KJ9

The first serial number has E feature turned off, the second has E
feature turned on. There are two tasks: Easy: by patching, turn on/off
all 5 features. Medium: generate a serial number with arbitrary
features turned on/off. Good luck!

(它只是以名称和序列号作为命令行参数来运行。有效的有:

2Z7A7-EK270-TMHR4-BHC71-CEB52-HELL0-HELL0-EONP9
2Z7A7-6I7R9-MZGO9-FDQJ3-JN0Q6-HELL0-HELL0-72KJ9
第一个序列号关闭了E功能,第二个序列号打开了E功能。
有两个任务:
简单:通过补丁,打开/关闭所有5个功能。
介质:生成任意功能开启/关闭的序列号。
好运!)
hint:
https://down.52pojie.cn/Tools/Debuggers/x64dbg_2018-04-05.zip
https://stackoverflow.com/questions/5475790/how-to-disassemble-the-main-function-of-a-stripped-application

(1)简单:通过补丁,打开/关闭所有5个功能。

首先我们输入2Z7A7-6I7R9-MZGO9-FDQJ3-JN0Q6-HELL0-HELL0-72KJ9查看一下输出的结果
在这里插入图片描述

然后再观察一下2Z7A7-EK270-TMHR4-BHC71-CEB52-HELL0-HELL0-EONP9输出结果
在这里插入图片描述

发现最后一位会相反,而其他的都会全部相同,如果想要某一个全开,某一个全关的话,肯定不能改跳转,因为改完跳转之后两个结果的ABCD 功能始终会保持一样,所以我们发现在跳转前有一个and 常数 的操作,并且输入两个码会发现与常数进行AND的数据都不一样,所以我们可以通过控制and后的常数来进行控制跳转两个码对应的跳转。
我们发现2Z7A7-6I7R9-MZGO9-FDQJ3-JN0Q6-HELL0-HELL0-72KJ9与常数进行与运算的数据分别为F8、43、EF、5A、CF
而2Z7A7-EK270-TMHR4-BHC71-CEB52-HELL0-HELL0-EONP9是7A、EF、DB、1E、F4
所以我们将第一个常数改为和F8与运算之后为0但是和7A与运算后不为0的数据,可以改为0x40,其他四位同理进行修改
运行到下图所示的关键地方,对数据进行修改
在这里插入图片描述

将0x40改成0x2,0x1改成0x20,0x2改成0x10,0x8改成0x4,如下图所示:
在这里插入图片描述

之后进行保存,然后进行运行,结果如下图所示:
在这里插入图片描述

(2)中等:生成任意功能开启/关闭的序列号

第一步:
在这里插入图片描述

是从第一个HELLO开始到第二个HELLO结束做了比较,并且很容易可以发现这里如果输入别的数据将会报错说格式错误。
所以初步确定格式应该是xxxxxxxxxxxxxxxxxxxxxxxxxxxxx-HELL0-HELL0xxxxxx
第二步:
在IDA里找到如下所示的代码,发现它的功能是检测每隔5位是否为“-”符号,即从第六位开始:
在这里插入图片描述

所以格式可以进一步确定为
xxxxx-xxxxx-xxxxx-xxxxx-xxxxx-HELL0-HELL0-xxxxx
然后会发现每五位会进行一次处理,处理的大致步骤是:
首先判断我们输入的是数字还是大写字母还是其他,如果输入其他的话就是非法的字符,会报错;
如果输入的是数字的话就减去0x30;
如果输入的是大写字母的话就减去0x37,然后所获得的值分别存入arr[4],arr[3],arr[2],arr[1],arr[0]中;
然后再进行Result=arr[4]+36*(arr[3]+36*(arr[2]+36*(arr[1]+36*arr[0]))))的操作,最后将Result存入到内存中去。
存到内存中的这一串数据在之后校验的过程中有重要作用.
假设这段数据的首地址为S
第三步:
我们在IDA中找到如图所示代码
在这里插入图片描述

这个判断是S[0]<<24+S[1]<<16+S[2]<<8+S[3]得到的数据和0XBEBAADDE进行比较,这里需要两个数据相等.所以前五位必须是2Z7A7
在这里插入图片描述

发现这里有一个很多判断条件的if语句,但是真正有用的判断只有v4<1,v3<1,v5<0x7e0,v5>0x834,v4>0xc,v3>0x1f和sub_1400010B0函数,这里的条件应该全部判断为假,即1<S[6]<0xc,
0x7E0<(S[4]<<8)+S[5]<0x834,
0x1<S[7]<0x1f
且函数sub_1400010B0运算后的数据得等于S[16-23]十六进制组成的数据
经过分析v4是S[6],v3是S[7],v5是S[4]<<8+S[5].
如果这里验证通过的话,就会执行开启和关闭功能的代码:
在这里插入图片描述

用S[8]和0x40进行与运算,判断是否开启A功能,同理S[9],S[10],S[11],S[12]均会进行这样的操作.
所以综合分析后:
11111-22222-33333-44444-55555-HELL0-HELL0-66666
1处必须填入2Z7A7
2处填入的数据必须满足得到的S[3]=0xBE0x7E0<(S[4]<<8)+S[5]<0x834,
3处填入的数据满足1<S[6]<0xc,0x1<S[7]<0x1f
4,5处填入的数据就是根据需求对功能进行选择
在6处填入的数据就需要满足整串数据的校验码符合等于S[16-23]十六进制组成的数据

2. CTF(看雪.京东 2018CTF 第十二题 破解之道)

x6412.exe

先打开程序:
在这里插入图片描述

是一个输入字符串再判断对错的程序。
打开程序进行动态调试,一步一步走。首先会遇到一个对输入数据的长度进行判断的地方如下图所示:

第一步:

在这里插入图片描述

这里会对输入的数据的长度与0x1E进行比较,如果不相等的话就会跳到错误的地方。
所以得到第一个条件就是输入的长度必须是30;

第二步:

然后继续向下执行,找到再次出现我们输入的字符串的地方发现如下汇编语言:
在这里插入图片描述
在这里插入图片描述

这段代码的意思是从我们输入的所有数据中每一个符号都要进行运算。
运算过程是先与0xCBF29CE484222325进行异或,然后再乘以0x100000001B3。
然后运算的结果会和0xAF63B44C8601A894进行比较,如果相等的话就会给r8+1,
最终r8的数据应该要大于等于3。
那么我们写一个程序来看一下哪个字符的运算后会和给的结果相同,最终运算结果是’9’,
所以我们输入的字符串中应该至少有3个’9’;
在这里插入图片描述

第三步:

运行到如下图所示的地方,又出现了我们输入的字符串,所以这里应该也是一处关键性的代码,通过分析我们发现这里取了输入的前9个字符进行和上图一样的运算,而且都对结果进行了检验,由于每一个字符的选择是从0-255,所以这里选择爆破的方法获得前九位字符.
在这里插入图片描述

爆破代码及运行结果如下图所示:

#include<stdio.h>
int main()
{
	__int64 a[9]={0xAF64064C860233EA,
		0xAF64154C86024D67,
		0xAF63FE4C86022652,
		0xAF64094C86023903,
		0xAF63FB4C86022139,
		0xAF63AF4C8601A015,
0xAF63AD4C86019CAF,
0xAF63AC4C86019AFC,
0xAF63B54C8601AA47};
	__int64 b=0x100000001B3;
	__int64 d=0;
	int flag=0;
	int i=0;
	__int64 c=0xCBF29CE484222325;
	__int64 final=0x4F8075587499C0FF;
	__int64 rax=0xA189FA2D2B8F61B6;
	for(i=0;i<9;i++)
	{
		for(d=0;d<255;d++)
		{
			__int64 temp=d^c;
			temp=temp*b;
			if(temp==a[i])
			{
				printf("%c",d);
				break;
			}
		}
}
	return 0;
}

运行结果:
在这里插入图片描述

所以我们可以确定输入的前九个字符就是KXCTF2018
到目前为止我们可以确定输入的字符串的格式大概是KXCTF2018999XXXXXXXXXXXXXXXXXXXX

第四步:

又找到如图的关键代码处,此处的功能是将所有输入的字符进行计算,然后得出的结果必须和0x4F8075587499C0FF相同,由于这里共有30个字符串我们仅仅已知12个,还有18个字符未知,爆破是不可取的,所以这里应该可以留下来做一个检验的功能,这里先修改跳转,不进行分析:
在这里插入图片描述

第五步:

在这里插入图片描述

如图所示,这个地方应该是关键代码处,由于上面的跳转会跳过这一部分,所以我们修改跳转之后让程序进入这一部分运行。
在这里插入图片描述

当运行过BDD8这个函数之后发现返回的字符串是在第一个’9’之后的字符串,
之后又取了截取后的字符串的前五位作为参数,同时又传进了”.DLL”作为参数,执行了E46C函数,我们继续运行发现执行了E46C函数之后返回值里有7819c.DLL这个就是我们截取的5个字符和.DLL进行拼接后的结果
在这里插入图片描述

所以这个E46C的作用应该是拼接。
在这里插入图片描述

这里将第三个’9’赋值为0,然后取出了第二个’9’到没赋值前第三个’9’之间的字符串.
所以这一段的功能应该是将第一个’9’之后的五位取出来与.DLL进行拼接,生成一个动态链接库的名字,然后将第二个’9’和第三个’9’之间的字符取出来,暂时不清楚有什么用,但是大概分析出格式为:
KXCTF20189XXXXX9xxxxxxxxxxxxx9
其中的XXXXX应该是一个DLL的名字.
猜测可能是NTDLL
通过网上查找发现这里的计算是一种hash算法。

第六步:

在这里插入图片描述

分析这段代码大概功能是从当前加载的DLL库中进行查找所有函数,然后进行HASH值的计算,找到HASH值等于0x53b2070f的函数名,我们直接执行到他找到之后的代码,在寄存器里会发现这段代码找到的了LoadLibraryEXA函数.
继续向下执行
在这里插入图片描述

这段的作用就是加载我们输入的DLL。
然后返回DLL地址00007FFA349C0000;
然后继续向下执行发现了和上面查找函数一样的代码,只不过是查找的hash值不同而已,采取同样的方法,直接看他找到了什么函数
在这里插入图片描述

发现他找到了GetProcessAffinitMask函数,但是这个函数好像没什么用。
在这里插入图片描述

我们发现在用CALL R8的时候其实就是在调用GetProcAddress函数,并且使用了我们输入的第二个’9’到第三个’9’之间的数据作为一个函数名。
并且在调用该函数的时候使用的第一个参数是我们输入的DLL的地址.
在这里插入图片描述

所以这一段我们分析出来他应该是从我们输入的DLL中去找到我们输入的函数的地址,如果找到的话会返回函数地址,如果没有找到就会返回NULL
在这里插入图片描述

这里必须不跳转,所以GetProcAddress要调用成功.然后这里就会返回一个负数在接下来的一个判断中起作用,然后跳转到成功的地方
在这里插入图片描述
所以综上所述我们输入的flag应该是
KXCTF20189[XXX]9{XXXX}9
条件1:长度为30位
条件2:必须有至少3个’9’
条件3:前九位为 KXCTF2018
条件4:整个字符串的hash值应该是0x4F8075587499C0FF
条件5:[]之间的数据应该是一个长度为5的DLL名称
条件6:{}之间应该是一个长度为13的函数名称
还有可能有4个’9’或者5个’9’等等但是这样的话就输入了很多无用的数据,所以这里应该是只有3个’9’在字符串中的。
所以综上可以写出代码来爆破得出最终的flag;
def Hash(string_xx):
temp = 0xcbf29ce484222325
a = 0x100000001b3
for i in string_xx:
temp = temp ^ ord(i)
temp = (temp * a) & 0xffffffffffffffff
return temp

def Find():
file = open(“C:\Users\aer\Desktop\06x64\api.txt”)
while 1:
str = file.readlines(100000)
if not str:
break
for line in str:
Function = line.strip()
str_in = “KXCTF20189NTDLL9” + Function + “9”
Code = Hash(str_in)
if Code == 0x4f8075587499c0ff:
print (str_in)
break
file.close()
Find()
运行结果:
在这里插入图片描述

得到正确的flag:KXCTF20189NTDLL9DbgUiContinue9
将flag输入exe:x6412 KXCTF20189NTDLL9DbgUiContinue9
在这里插入图片描述

正确

3. 某文件被加密了

最后修改时间是2019/04/11 22:10:34。 猜测大概2019/04/11
22点11分之前,某一刻用户运行了genprik,生成了密钥加密文件。
加密算法已知,通过分析密文,只能得出密钥第1个字节是0x25第2个字节是0x61。 求完整的16个字节的密钥。

刚开始用IDA打开,IDA会提示exe的入口点在其他的位置,应该是有壳,先对它进行查壳脱壳:
在这里插入图片描述

再用IDA打开:
在这里插入图片描述

int __cdecl main(int argc, const char **argv, const char **envp)
{
  __time32_t v3; // ST40_4@1
  DWORD Seed; // ST38_4@1
  int v5; // eax@2
  signed int v7; // [sp+24h] [bp-34h]@1

  v3 = time32(0i64);        //0i64表示int64_t类型的0,time3()表示返回自1970年1月1日午夜起经过的秒数,或者在出现错误时返回-1
  Seed = GetCurrentProcessId() ^ v3;   //获取当前进程的唯一标识
  srand(Seed);
  v7 = 0;
  do
  {
    v5 = rand();
    printf(Format, (v5 >> 7) & 0xFF);
    ++v7;
  }
  while ( v7 < 16 );
  return 0;
}

这就是代码的关键部分,获取当前系统时间和进程号,来进行运算生产16字节的密钥,所以我们将时间调整到2019/04/11 22:10:34之前进行爆破,因为我们是进行爆破,所以时间不用太准确,只要在这之前都行,当然这里的2019/04/11 22:10:34可能是老师给的hint:设置密钥的正确事件。我将当前时间设为了2019/04/11 22:10:00开始,先将它转换成从1970年1月1日以来持续时间的秒数:
源代码:time.c

#include<stdio.h>
#include<time.h>
/**
struct tm {
    int tm_sec;// 秒 - 取值区间为[0,59] /
    int tm_min; // 分 - 取值区间为[0,59] /
    int tm_hour; / 时 - 取值区间为[0,23] /
    int tm_mday; / 一个月中的日期 - 取值区间为[1,31] /
    int tm_mon; / 月份 (从一月开始,0代表一月) - 取值区间为[0,11] /
    int tm_year; / 年份,其值等于实际年份减去1900 /
    int tm_wday; / 星期 - 取值区间为[0,6],其中0代表星期天,1代表星期一,以此类推 /
    int tm_yday; / 从每年的1月1日开始的天数 - 取值区间为[0,365],其中0代表1月1日,1代表1月2日,以此类推 /
    int tm_isdst; / 夏令时标识符,实行夏令时的时候,tm_isdst为正。不实行夏令时的时候,tm_isdst为0;不了解情况时,tm_isdst()为负. /
};
*/
int main(void)
{
    //获取时间差
    struct tm tnNormal;
    ///开始的时间:2019/04/11 22:10:00
    tnNormal.tm_year = 2019-1900;
    tnNormal.tm_mon = 4-1;
    tnNormal.tm_mday = 11;
    tnNormal.tm_hour = 22;
    tnNormal.tm_min = 10;
    tnNormal.tm_sec = 0;
    tnNormal.tm_isdst = 0;

    time_t tmNormal;
    tmNormal = mktime(&tnNormal);
    ///开始的时间:2019/04/11 22:10:00转化为的秒数:1554991800
    printf("开始的时间:2019/04/11 22:10:00转化为的秒数:%d\n",tmNormal);
    return 0;
}

运行结果:
在这里插入图片描述

将2019/04/11 22:10:00转化为的秒数是:1554991800。接下来从1554991800开始递加,当前进程ID从0到100000循环,爆破直到得出的密钥第1个字节是0x25第2个字节是0x61:
下面是源代码:key.c

#include<stdlib.h>
#include <stdio.h>
/**
0~100000和0~500的第一个解
0x25 0x61 0x6c 0xd5 0x1d 0xd3 0x4b 0xcb 0xe7 0x34 0x97 0x93 0xa4 0x92 0x53 0x1f
当前时间:1554991800,当前进程:32375
*/
int main()
{
    ///开始的时间:2019/04/11 22:09:00
    __time32_t v3 = 1554991800; // ST40_4@1
    long Seed; // ST38_4@1
    int v5; // eax@2
    signed int v7; // [sp+24h] [bp-34h]@1
    int i;
    int flag = 0;
    int id;
    __time32_t now;
    ///0x25和0x61
    __int8 v4,v6;
    ///时间递加
    for(i = 0; i< 500; i++){
        now = v3+i;
        ///获取当前进程的唯一标识GetCurrentProcessId()
        for(id = 0 ; id < 100000 ; id++){
            Seed = id ^ now;
            ///设置时间种子
            srand(Seed);
            v7 = 0;
            __int8 temp = 0;
            do
            {
                ///根据时间种子生成随机数
                v5 = rand();
                temp = (v5 >> 7) & 0xFF;
                ///第一个数是0x25
                if( v7 ==0 && 0x25 == temp){
                    v4 = 0x25;
                ///第一个数不是0x25
                }else if(v7 == 0 && temp !=0x25){
                    v4 = 0;
                    break ;
                }

                ///第二个数是0x61
                if(v7 == 1 && 0x61 == temp){
                    v6 = 0x61;
                    printf("0x25 ");
                ///第二个数不是0x61
                }else if(v7 == 1 && 0x61 != temp){
                    v4 = 0;
                    v6 = 0;
                    break;
                }

                ///如果找到结果
                if(v4 == 0x25 && v6 ==0x61){
                    printf("0x%x ",temp & 0xFF);
                }
                ++v7;
            }while( v7 < 16 );
            if(v7 == 16){
                ///输出正确答案
                printf("\n当前时间:%d,当前进程:%d\n",now,id);
                flag = 1;
                break;
            }
        }
        if(flag == 1){
            printf("flag\n");
            break;
        }
    }
    return 0;
}

运行结果:
在这里插入图片描述

最后破解的16字节密钥为:0x25 0x61 0x6c 0xd5 0x1d 0xd3 0x4b 0xcb 0xe7 0x34 0x97 0x93 0xa4 0x92 0x53 0x1f。
到这破解的密钥已经得到的,但是我们发现当前时间:1554991800,当前进程:32375,因为随机种子是由当前时间和当前进程异或得到的,所以随着当前时间和当前进程递增,会出现多个时间和进程的异或结果相同,然后它们都最终得到这同一个密钥:0x25 0x61 0x6c 0xd5 0x1d 0xd3 0x4b 0xcb 0xe7 0x34 0x97 0x93 0xa4 0x92 0x53 0x1f。
我们来输出一下增加的时间0500以内和进程ID在0100000以内的所有解:
源代码:

#include<stdlib.h>
#include <stdio.h>

int main()
{
    ///开始的时间:2019/04/11 22:10:00
    __time32_t v3 = 1554991800; // ST40_4@1
    long Seed; // ST38_4@1
    int v5; // eax@2
    signed int v7; // [sp+24h] [bp-34h]@1
    int i;
    int flag = 0;
    int id;
    __time32_t now;
    ///0x25和0x61
    __int8 v4,v6;
    ///时间递加
    for(i = 0; i< 500; i++){
        now = v3+i;
        ///获取当前进程的唯一标识GetCurrentProcessId()
        for(id = 0 ; id < 100000 ; id++){
            Seed = id ^ now;
            ///设置时间种子
            srand(Seed);
            v7 = 0;
            __int8 temp = 0;
            do
            {
                ///根据时间种子生成随机数
                v5 = rand();
                temp = (v5 >> 7) & 0xff;
                ///第一个数是0x25
                if( v7 ==0 && 0x25 == temp){
                    v4 = 0x25;
                ///第一个数不是0x25
                }else if(v7 == 0 && temp !=0x25){
                    v4 = 0;
                    break ;
                }

                ///第二个数是0x61
                if(v7 == 1 && 0x61 == temp){
                    v6 = 0x61;
                    printf("0x25 ");
                ///第二个数不是0x61
                }else if(v7 == 1 && 0x61 != temp){
                    v4 = 0;
                    v6 = 0;
                    break;
                }

                ///如果找到结果
                if(v4 == 0x25 && v6 ==0x61){
                    printf("0x%x ",temp & 0xff);
                }
                ++v7;
            }while( v7 < 16 );
            if(v7 == 16){
                ///输出正确答案
                printf("\n当前时间:%d,当前进程:%d,异或的结果随机种子:%d\n",now,id,now^id);
                v4 = 0;
                v6 = 0;
            }
        }
    }
    return 0;
}

运行结果:
在这里插入图片描述

这是因为源文件算法中异或这个限制条件不够苛刻,又没有其他的条件来限制多解,才会出现这样的情况,题目的主要用意也是让我们明白以当前系统时间作为随机种子并不是安全的。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值