关于010editor的逆向分析
- 暴力破解
- 浅析注册算法
- 通过用户名算出对应的密码位
- 写出注册机
1.暴力破解
首先随便输入点内容,点击检查许可,会提示不可用的名字或密码

在od中搜索字符串,CTRL+F查找Invalid name,会搜到一堆提示的字符串

进入invalid name 的位置,在这里发现了一个跳转
过去到跳转的位置,发现一个跳转

保存修改到可执行文件,再次打开,随便输点什么,就可以通过。只是并没有激活产成功,每次打开都要点一下

2.简单分析算法
通过栈回溯找到上级函数,在反汇编窗口跟随

在这里发现了好几个比较,通过干预的方式跳转到每一个走向,推断出了第一个是检查许可,第二个是退出程序,第三个是打开帮助窗口,对应注册窗口的三个按钮

在上面打一个断点,再次运行,进入第一个call,发现下面有句请输入一个名字,但是跳过去了,这里应该是判断名字是不是空的

再往下有一个请输入密码,应该是判断有没有输密码

这一段不知道干嘛的但是根据程序走向来看这个地方之后就跳转到了关键的那个比较跳转,这里应该就是关键的函数了

回过头来分析一下爆破时遇到的那几个跳转

下面这个地方比较了EDI的值,但是它是从上面跳转下来的,再次跟踪过去

这个地方把EAX的值给了EDI,上面有一个CALL,EAX一般是函数返回值,那么上面的函数应该就是关键的判断,这个位置正好是之前看到的那几个不知道是干嘛的CALL

在这两个call下断点,重新运行程序。这次输入的密码就从1到a依次输入,目的是为了后面方便看位置

点击检查许可,在刚刚的断点断了下来这个地方将eax当参数传进了call,查看eax的值,在堆栈窗口跟随一下,发现这个数据像一个地址,在数据窗口跟随,这里正是我们输入的密码。

进到这个call内部,发现它的内容很少,并没有什么有用的信息,继续运行到下一个call,这个call传进去一个ecx,根据经验,ecx应该是个对象的地址,数据窗口跟随发现了几个地址。

分别跟随这几个地址,分别发现了我们输入的用户名以及密码。那么基本就可以确定是这个call验证了账号和密码了。我们进入到这个call的内部具体的分析一下


根据名字来看下面这两个call应该是做了非空判断

接着,根据传入的参数,我们进行数据跟随。下面两个call分别访问了密码和用户名的位置,再下面有一个字符串操作的call。不过不是我们重点分析的对向

再往下,就到了重要的位置

我们看一下ebp-21的位置是什么。在数据窗口按ctrl+G输入ebp-21,跳转到这个位置,我们发现正好是我们的密码。它把44放进了BL里面。那么应该是按字节来划分的。我们把我们的密码按照k[0]-k[9]来划分,这个位置对应的是k[3].

接下来按照相同的方法找到操作了哪些数字。

这个地方比较了k[3]是否为0x9c,在下面也有AC和FC,这个地方可能是在 比较许可证的类型,期限版或者是永久版,这里以0x9c这个版本为主要分析的版本,这里需要手动把密码的k3位置改为9c或者干预程序流程让他可以接着往下走,我们就可以接着往下分析。

推算过程不是很难,但是需要一步一步地细心的观察。下面有两个call-

第一个call的参数在上面的push,进去后发现它是对参数进行了一些操作。


第二个call和上面的功能类似

下面有几个比较来判断跳转,根据我们推导出来的公式,结合判断条件来写一个代码验证.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | int main() {
/ / 创建随机数
srand(time(NULL));
byte k[ 10 ] = { 0 };
/ / 我们要随机出来一些数来运算,从而找出符合判断条件的数值
while (true)
{
/ / k0和k6是第一个call的结果
byte k0 = rand() % 0xFF ;
byte k6 = rand() % 0xFF ;
byte al = (k0 ^ k6 ^ 0x18 + 0x3D ) ^ 0xA7 ;
if (al> 0 )
{
k[ 0 ] = k0;
k[ 6 ] = k6;
break ;
}
}
/ /
while (true)
{
/ / k1,k7,k2,k5是第二个call的运算
byte k1 = rand() % 0xFF ;
byte k7 = rand() % 0xFF ;
byte k2 = rand() % 0xFF ;
byte k5 = rand() % 0xFF ;
DWORD ESI = ( 0x100 * (k1 ^ k7 & 0xFF ) + k2 ^ k5 & 0xFF ) & 0xFFFF ;
DWORD EAX = (((ESI ^ 0x7892 ) + 0x4d30 ) ^ 0x3421 ) & 0xFFFF ;
if (EAX % 0XB = = 0 &&EAX / 0XB < = 0X3EB )
{
k[ 1 ] = k1;
k[ 7 ] = k7;
k[ 5 ] = k5;
k[ 2 ] = k2;
break ;
}
}
/ / k3必须是 9c
k[ 3 ] = 0x9c ;
printf( "%02X%02X-%02X%02X-%02X%02X-%02X%02X" ,k[ 0 ],k[ 1 ], k[ 2 ], k[ 3 ], k[ 4 ], k[ 5 ], k[ 6 ], k[ 7 ], k[ 8 ], k[ 9 ]);
system( "pause" );
}
|
运行我们写的程序,得到一个字符串E416-A69C-0081-14B4。输入进密码框,运行到比较的位置,就会顺利经过

我们成功的进入到了下一个位置,我们接着分析,在第一个call,将一个局部变量的地址当参数,然后调了一个call,这里只是将用户名的字符串转为acill的字符串

接着将字符串以及两个参数传进一个 call,结果返回的还是用户名。接着下一个call将用户名传进去,并把返回值给了edx

下面将dl也就是edx最低位的字节和ebp-20的这个数据比较,我们在数据窗口发现ebp-20这个地方放的是我们密码的k[4]-k[7]的数据,接着将edx的数据给ecx,将ecx向右移动8位,也就是一个字节。取cl,就是原来edx'的第三个字节,以此类推把kK[4]-k[7]的值和edx相比较。也就是上面函数的返回值。

那么函数的功能就是传入一个用户名,返回四个字节的数据,并和密码的kK[4]-k[7]进行比较。如果不一致就会验证失败,函数最后将0x2D返回

3.用户名对应密码的转换
我们看看这个关键函数如何将用户名转换成密码的,进去这个call一探究竟,网上关于010editor的分析也有很多,但这里都是直接去IDA把汇编转换成C语言代码,我也看了下,ida翻译出来的代码确实看不明白,这里我们直接自己分析一下这个算法的流程。
进去这个call

首先这个位置定义了局部变量,一共16个字节大小,然后把用户名地址给了edx

然后又从edx给到esi,通过比较al是不是零,让esi自增,循环完用esi-edi得到的就是循环的次数也就是字符串的长度,

下面有两个地方是比较关键的地方。第一个是一个库函数,C语言的库函数,将小写字母转化为大写字母。第二个是一个比较跳转。判断传进来的参数然后跳转

再往下就是各种运算,里面的运算涉及一个数组,因为太大,我们还是从od复制出快

最后的位置是向上跳转,很明显这个是一个循环,根据上面我们判断过的,那么这个循环是为了将每一位用户名进行运行算,循环次数就是用户名的长度。

那么,基本的逻辑结构我们已经清楚了。下面就是将这部分代码写进注册机里了,值得注意的一点是汇编中的eax*4实际上在我们用C语言数组的时候,就没必要乘以4了,因为汇编中是以地址加字节数来取值,4个字节才对应一个数,所以会乘以4
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 | / / 数组过长,节省空间就不放进来了。
DWORD arr[] = {......};
/ / 通过用户名得到对应的密码位
DWORD getResult(const char * name, int n1, int n2, DWORD val);
int main() {
/ / 创建随机数
srand(time(NULL));
byte k[ 10 ] = { 0 };
/ / 设置一个用户名
char userName[] = { "van1" };
/ / 生成用户名关联密码
DWORD result = getResult(userName, 1 , 0 , 0x3e8 );
/ / 设置k4 - k7的值
k[ 4 ] = result << 24 >> 24 ;
k[ 5 ] = result << 16 >> 24 ;
k[ 6 ] = result << 8 >> 24 ;
k[ 7 ] = result >> 24 ;
/ / 我们要随机出来一些数来运算,从而找出符合判断条件的数值
while (true)
{
/ / k0和k6是第一个call的结果
byte k0 = rand() % 0xFF ;
byte k6 = k[ 6 ];
byte al = (k0 ^ k6 ^ 0x18 + 0x3D ) ^ 0xA7 ;
if (al> 10 )
{
k[ 0 ] = k0;
break ;
}
}
/ /
while (true)
{
/ / k1,k7,k2,k5是第二个call的运算
byte k1 = rand() % 0xFF ;
byte k7 = k[ 7 ];
byte k2 = rand() % 0xFF ;
byte k5 = k[ 5 ];
DWORD ESI = ( 0x100 * (k1 ^ k7 & 0xFF ) + k2 ^ k5 & 0xFF ) & 0xFFFF ;
DWORD EAX = (((ESI ^ 0x7892 ) + 0x4d30 ) ^ 0x3421 ) & 0xFFFF ;
if (EAX % 0XB = = 0 &&EAX / 0XB = = 0X3E8 )
{
k[ 1 ] = k1;
k[ 2 ] = k2;
break ;
}
}
/ / k3必须是 9c
k[ 3 ] = 0x9c ;
printf( "%02X%02X-%02X%02X-%02X%02X-%02X%02X-%02X%02X" ,k[ 0 ],k[ 1 ], k[ 2 ], k[ 3 ], k[ 4 ], k[ 5 ], k[ 6 ], k[ 7 ], k[ 8 ], k[ 9 ]);
system( "pause" );
}
/ / 从用户名计算出密码对应位
DWORD getResult(const char * name, int n1, int n2, DWORD val) {
/ / 开始位置栈顶上移了 16 个字节
DWORD loc1, loc2, loc3, loc4;
loc1 = loc2 = loc3 = loc4 = 0 ;
DWORD edx;
DWORD ecx;
DWORD eax;
loc1 = 0 ;
/ / 求长度
int len = strlen(name);
DWORD ebx = val;
loc4 = loc3 = 0 ;
ecx = n2;
ebx = val;
ebx = ebx << 4 ;
ebx - = val;
ecx = ecx << 4 ;
ecx + = n2;
for ( int i = 0 ; i < len ; i + + ) {
edx = toupper(name[i]);
ecx = arr[edx] + loc1;
if (n1 ! = 0 ) {
eax = (edx + 0xd ) & 0xff ;
ecx = ecx ^ arr[eax ];
eax = edx + 0x2f ;
eax = eax & 0xff ;
ecx = ecx * arr[eax ];
eax = loc2 & 0xff ;
ecx = ecx + arr[eax ];
eax = ebx & 0xff ;
ecx = ecx + arr[eax ];
eax = loc3;
eax = eax & 0xff ;
ecx = ecx + arr[eax ];
eax = ecx;
loc1 = eax;
}
else
{
eax = edx + 0x3f ;
eax = eax & 0xff ;
ecx = ecx ^ arr[eax];
eax = edx + 0x17 ;
eax = eax & 0xff ;
ecx = eax * arr[eax];
eax = loc2 & 0xff ;
ecx = ecx + arr[eax];
eax = ebx & 0xff ;
ecx = ecx + arr[eax];
eax = loc4;
eax = eax & 0xff ;
ecx = ecx + arr[eax];
eax = ecx;
}
loc3 + = 0x13 ;
loc2 + = 9 ;
ebx + = 0xd ;
loc4 + = 7 ;
}
return eax;
}
|
运行程序获得密码,输入我们生成的密码。再次运行程序

我们的密码可以正确的通过前面的验证,但是会弹出网络验证

4.去除网络验证
重新运行程序,输入验证码。在原来的验证密码的函数之后还有一个类似的函数,先步过看一下返回值,发现它把返回值修改为了113,正确的返回值应该是DB

我们进到函数里面一探究竟。发现这个地方有个比较,相同就会跳转,不相同就会返回113.我们先让他强制跳转过去。

然后就会一路运行到返回正确的返回值的地方,这里吧DB给eax并且返回

出了这个函数,继续往下走,发现还有一个和之前一样的比较,如果一样就跳到后面最后的验证,不一样回去执行下面的call,在这里会程序会卡住好几秒,应该是在链接服务器获取信息。我们直接修改掉这个跳转,让它无条件跳转


弹出许可证接受界面,成功!!!
总结
觉得去除网络验证也就是相当于一个爆破的过程。为什么我还要写注册机呢?写完注册机到最后还是要爆破掉。但是,写注册机与爆破网络验证的目的并不是为了破解掉这个软件,而是去分析这个软件运行的一个过程,理解它的思路。学习分析一个程序的思路和常用手段。并不是单纯的为了破解而去破解的。我们要的是一个学习的过程。
思路还是前辈们的思路,感谢各位为安全行业的付出。这里只是记录分享一下学习的过程。分析的过程中还是有一些不太好去理解的东西。因为时间原因,在通过用户名来求证密码的后四位的时候也是没有自己去分析。对汇编的掌握还是有很多陌生的地方。也很有可能因为不知道这个指令的作用而错过某些关键点。
路还很长,我们一起走下去。
特别致谢
15PB薛老师的思路
【看雪培训】目录重大更新!《安卓高级研修班》2022年春季班开始招生!
最后于 2021-12-5 12:49
被wx_Van_Zovy编辑
,原因: 更新了用户名对应为密码的算法解析