逆向工程实验——lab7(c++语言逆向、随机数实验、go语言逆向)

1.阅读

怎样攻破 RSA-1024的算法保护 ?(翻译部分)
https://www.pediy.com/kssd/pediy07/pediy7-705.htm

2. Crackme2.exe

Rules:

  1. Do not Patch
  2. Sniff a serial for your name
  3. write a keygen

首先运行程序,随便输入用户名和注册码,发现输出Error
在这里插入图片描述

用IDA打开,找到关键代码:
在这里插入图片描述

代码的主要逻辑就是这:
在这里插入图片描述

所以这个程序的主要逻辑就是将我们输入的用户名进行一些计算,然后将结果与我们输入的序列号进行比较,相等则输出“Correct :: Good Work”。
我们将程序加载进OD中去,然后搜索字符串,找到对应的位置:
在这里插入图片描述

然后进入到关键代码处,往上找会发现有输入用户名和注册码的函数:
在这里插入图片描述

发现这里有获取长度的函数,并且在获取长度之后进行了一系列的运算操作,继续调试,输入用户名A123456和序列号123:
在这里插入图片描述
在这里插入图片描述

最终运算过后的数据会利用fild存入到堆栈中,然后再利用fstp弹出并保存到内存地址中去,这里存入是以单精度的形式存入,而弹出保存到地址中去的时候是以双精度的方式保存,所以我这里就跟了一下两个内存里面的数据,发现最终保存的时候低四个字节是原来的数据低11位左移21位以后得到的数据,并且最终的输出结果是将这四个字节作为带符号的十进制输出并且与”-x019871”拼接而成。
在这里插入图片描述

原来内存中的数据:
在这里插入图片描述

fstp弹出后到内存中的数据0x66000000:
在这里插入图片描述

最终的注册码是0x66000000的带符号十进制(1711276032)和”-x019871”拼接而成的字符串:
在这里插入图片描述

之后进入strcmp函数中进行比较:
在这里插入图片描述

果然是按照我们之前推测的逻辑,程序将我们的用户名A123456进行运算,算出ASCII“1711276032-x019871”再和我们输入的序列号“123”进行比较。
将这个注册码输入之后,发现注册成功,所以这个就是A123456正确的注册码:
在这里插入图片描述

然后根据上面的代码逻辑写出注册机的代码:

#include <stdio.h>
#include <string.h>
int main()
{
	char Name[20];
	int len;
	printf("请输入你的用户名:");
	scanf("%s", Name);
	len = strlen(Name);
	long long eax;
	eax = len * 0x875CD;
	eax = eax * 0x51EB851F;
	long long edx = eax >> 32;
	int ecx = edx >> 5;
	ecx = ecx * (-0x370);
	int end = ecx & 0x7ff;
	end = end << 21;
	printf("你的序列号为:%d-x019871\n", end);
	return 0;
}

在这里插入图片描述

重新运行Crackme2.exe:
在这里插入图片描述

3.Pseudo Random Number Generation Lab

http://www.cis.syr.edu/~wedu/seed/Labs_16.04/Crypto/Crypto_Random_Number/

Task1:

在使用当前时间初始化随机种子的情况下:
在这里插入图片描述

所产生的随机值随当前时间的变化而变化
在不使用当前时间初始化随机种子的情况下:
在这里插入图片描述

所产生的随机值不变化,始终为默认产生的随机值。

Task 2:Guessing the Key

因为必须在Linux下开始实验,且使用了aes-128-cbc进行加密的,所以我们先安装openssl,使用openssl的AES加密-CBC模式来加解密:
本题描述的背景是,在得到了一份文件,知道这份文件是用上述代码产生的秘钥通过aes 128 cbc模式加密的,并且知道文件加密的时间在2018-04-17 23:08:49左右,知道一部分明文和加密后的秘文以及cbc模式中需要的IV,需要我们求出加密的秘钥。
Plaintext: 255044462d312e350a25d0d4c5d80a34
Ciphertext: d06bf9d0dab8e8ef880660d2af65aa82
IV: 09080706050403020100A2B2C2D2E2F2
首先我们将Linux系统时间分别放到2018-04-17 21:00:00和2018-04-17 23:08:49,在这两个时间节点产生Time函数对应的值:
在这里插入图片描述
在这里插入图片描述

从1970-01-01 00:00:00到所改过时间一共经历了1524013228秒和1524020933秒。得到两个值后用for循环:
在这里插入图片描述

遍历这两个值中所有可能值对应的rand值(先用srand产生种子)使用这些rand值分别对已知明文块进行加密,结果和秘文比对,找出正确的秘钥,主要的main函数部分代码:

int main() {
    // case 1
    int i,m;
    long long j;
    uint8_t key[16];
    const uint8_t IV[16]={0x09,0x08,0x07,0x06,0x05,0x04,0x03,0x02,0x01,0x00,0xA2,0xB2,0xC2,0xD2,0xE2,0xF2};
    const uint8_t expectct[16]={0xd0,0x6b,0xf9,0xd0,0xda,0xb8,0xe8,0xef,0x88,0x06,0x60,0xd2,0xaf,0x65,0xaa,0x82};
    uint8_t pt[16]={0x25,0x50,0x44,0x46,0x2d,0x31,0x2e,0x35,0x0a,0x25,0xd0,0xd4,0xc5,0xd8,0x0a,0x34};
    uint8_t ct[16] = {0};     // 外部申请输出数据内存,用于加密后的数据
    uint8_t plain[16] = {0};  // 外部申请输出数据内存,用于解密后的数据
    for(i=0;i<16;i++)
    {
        pt[i]=pt[i]^IV[i];
    }
    for (j=1524013228;j<1524020933;j++)
        {
            m=0;
            srand (j);
    for (i = 0; i< 16; i++)
        {
            key[i] = rand()%256;
            printf("%.2x", (unsigned char)key[i]);
        }
        printf("\n");
    aesEncrypt(key, 16, pt, ct, 16); // 加密
    for(i=0;i<16;i++)
    {
        if(ct[i]==expectct[i])
            m++;
    }
    if(m==16)
    {
        printHex(key, 16, "key:");
        break;
    }
}
}

得到了秘钥key:95 FA 20 30 E7 3E D3 F8 DA 76 1B 4E B8 05 DF D7

Task 3: Measure the Entropy of Kernel

运行命令生成随机值:
在这里插入图片描述
在这里插入图片描述

在分别进行点击鼠标,移动鼠标,打开软件,新建文件夹等操作后,发现移动鼠标和打开软件时值变化幅度较大,且鼠标移动越快值增加越快(值一直处于增长中)

Task 4: Get Pseudo Random Numbers from /dev/random

按照pdf上的要求,首先我们运行$ cat /dev/random | hexdump命令发现首先产生一些16进制数,然后不定时的再产生一些:
在这里插入图片描述

接着我们使用Ctrl -C命令强制停止,再运行$ watch -n .1 cat /proc/sys/kernel/random/entropy_avail 命令得到结果:
在这里插入图片描述

观察得到当鼠标不动时随机数缓慢的增加,当鼠标移动时,随机数会先增加然后又马上减小,在120~190之间,随机数在这两个值之间迅速的波动。

Task 5: Get Random Numbers from /dev/urandom

首先我们运行$ cat /dev/urandom | hexdump 这个代码,发现和$ cat /dev/random | hexdump 一样是出现大量的16进制数,但是$ cat /dev/urandom | hexdump 会不间断的快速的产生,而不像$ cat /dev/random | hexdump产生一定量的16进制数后就缓慢的,间断的产生。
在这里插入图片描述

修改代码实现产生256位随机数秘钥:
修改的代码如下:

#include<stdio.h>
#include<stdlib.h>
#define LEN 32 // 256 bits
void main()
{
unsigned char *key = (unsigned char *) malloc(sizeof(unsigned char)*LEN);
FILE* random = fopen("/dev/urandom", "r");
fread(key, sizeof(unsigned char)*LEN, 1, random);
fclose(random);
int i;
for(i=0;i<32;i++)
{
printf("%.2x",(unsigned char)key[i]);
}
printf("\n");
}

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

4. (选做)gl.exe

(go语言逆向)
先查看exe信息:
在这里插入图片描述

在WIN64系统上运行的原生64位应用程序,其PE格式称为PE32+,x86-64是x86架构的64位拓展,向后兼容于16位及32位的x86架构。
运行gl.exe:
在这里插入图片描述

发现是一个注册类型软件,读取了我们的CPU型号,要求我们输入许可证,输入错误就显示Failed
再尝试用IDA打开gl.exe:
在这里插入图片描述

发现它将符号信息已经去除掉了。go语言的样本可以分为如下的三个等级以及对应的等级的说明:
1、简单:未去除符号以及未混淆的的软件。
2、一般:去除符号的软件。
3、困难:混淆的软件。
接下来我们用George Zaytsev编写的脚本IDAGolangHelper进行还原符号,先下载IDAGolangHelper脚本,然后用IDA选择文件->脚本文件找到go_entry.py,打开就会出现IDAGolangHelper脚本的UI界面:
在这里插入图片描述

第一个和第二个按钮分析了go语言的版本问题,IDAGolangHelper脚本通过2种方式来确定当前程序的go语言版本:一是通过分析go中特有的结构体类型,由于不同版本之间有结构体会产生变化,作者提出了这种思路来确定版本信息;二是通过特征字符串的来进行查找。这里我们选择1.10版本来还原符号:
在这里插入图片描述

上面这个是用最新版的IDAGolangHelper脚本还原后的函数,下面这个是用旧版的GoUtils2.0还原后的函数:
在这里插入图片描述

打开字符串视图,搜索字符串License、Failed发现都没有结果,这是因为Go语言会将所有字符串连接在一起,通过起始指针和字符串长度来表示整个串,和C语言用’\0’表示字符串结束是不一样的,而IDA无论从之后再说的传参和返回值,还是字符串检索功能都针对的是C语言,对于不按套路的Go语言局限很多,字符串检索失败就是一点。
因此只能使用菜单栏中的Search-Text或者Sequence of bytes来搜索:Text会根据交叉引用来查找,在稍等片刻以后应该就能找到;而Sequence of bytes则是根据字节流。
我们把“License”转换成16进制ASCII码:“4c 69 63 65 6e 73 65”进行搜索,秒出:
在这里插入图片描述
在这里插入图片描述

不过双击过去会跳到整个字符串变量的开头,如果还需要具体值的话还得首先undefine这个串:
在这里插入图片描述

然后重新双击找到0x4EF5C2,或者快捷键G跳转到4EF5C2:
在这里插入图片描述

就找到了“License”字符串,再Ctrl+A生成字符串即可查看交叉引用:
在这里插入图片描述

Ctrl+X查看交叉引用,竟然提示没有交叉引用。
只能换成“Failed”,转换成“46 61 69 6c 65 64”:
在这里插入图片描述
在这里插入图片描述

Ctrl+A生成字符串:
在这里插入图片描述

Ctrl+X查看交叉引用,找到代码所在位置:
在这里插入图片描述

已经可以根据这部分的汇编代码看懂大致的判断跳转逻辑了,这段汇编是main_main_func2函数里的,也可以查看反编译之后的代码:
在这里插入图片描述

可以看到代码的关键判断就是runtime_chanrecv1(&v21)计算得到的v21,如果v21为真就退出while循环输出Success,否则输出Failed继续while循环等待下一次输入。
这一段的源代码:

  while ( 1 )
  {
    do
    {
      *(_QWORD *)&v22 = qword_4C6E60;
      *((_QWORD *)&v22 + 1) = &unk_5AB200;
      fmt_Scanf((__int64)&unk_5AB200, (__int64)&word_4EE96A[2], v11, v12);
    }
    while ( v20 );
    runtime_chansend1(&unk_5AB200);
    v21 = 0;
    runtime_chanrecv1(&v21);
    if ( v21 )
      break;
    fmt_Printf(v14, v13, v15, v16);
    time_Sleep(v17);
  }

其中runtime_chansend1和runtime_chanrecv1是go语言中的写操作和读操作,函数原型是:

func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool)

因为是64位的exe文件,所以用x64dbg打开,找到代码对应位置:
在这里插入图片描述

在几个重要的函数上下断点,然后输入字符串“123456”运行:
在这里插入图片描述

字符串“123456”保存在ds:[5AB200]=c:50470上:
在这里插入图片描述
在这里插入图片描述

每次保存的ds:[5AB200]的具体地址是不同的:
在这里插入图片描述
在这里插入图片描述

关键的判断跳转:在恒定地址byte ptr [rax]=[000000C000097F87]=0
在这里插入图片描述

通过把修改ZF修改成0,可以实现跳转输出Success:

在这里插入图片描述

执行完runtime_chansend1函数之后,000000C000097F87的值没有变化说明这里没有改变:
在这里插入图片描述

之后下一行mov byte ptr ss:[rsp+0x47], 0x0把000000C000097F87上一个byte的值赋为0:
在这里插入图片描述

但是我们始终无法找到对输入的字符串进行判断的关键算法:
在这里插入图片描述

关键可能在于chansend1和chanrecv1的写和读操作。关于go语言逆向的知识还是太少,之后又新的思路会再更新。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值