写在前面:
大半年忙东忙西,很少有时间弄技术上的东西了,前几天和一个学长闲聊,他说之前做了一个小东西,就是分析010editor的注册算法,然后就找了一个"最新"的来试试。弄了几天基本算完成了,后面一直偷懒没有写文档,这会儿慢慢补充好吧。
.text:00550267 cmp edi, 0DBh ; 同时,要来到这里进行判断,有三条路线
.text:0055026D jnz short loc_5502A7 ; 只有这条路线是正确的,此时edi必须要等于0xDB
.text:0055026F push 0FFFFFFFFh
.text:00550271 push offset aPasswordAccept ; "Password accepted. Thank you for purcha"...
.text:00550276 call ebp ; QString::fromAscii_helper(char const *,int) ; QString::fromAscii_helper(char const *,int)
.text:00550278 mov [esp+40h+var_18], eax
根据IDA的交叉引用,我们可以看到,达到这一步,必须满足:
loc_550267: ; 同时,要来到这里进行判断,有三条路线
cmp edi, 0DBh
jnz short loc_5502A7 ; 只有这条路线是正确的,此时edi必须要等于0xDB
我们看到,理论上从在三种可能性,我们逐一来看:
总原则:
Edi必须等于0xDB
push 3C70h
push 5
call sub_403963 ; 有三种返回情况:0xE7,0x4E,0x2D
mov ecx, dword_7D6E24 ; 关键算法点之一
push 3C70h
push 5
mov ebx, eax
call sub_40897C ; 这个函数会影响到edi的值(返回值赋值给edi)
mov ecx, dword_7D6E24 ; 尽管后面有三条路线,但是不管怎么说edi都必须为0xDB
xor edx, edx ; '关键算法点之一'如果返回的是0x2D,那么edi就为0xDB
mov edi, eax ; ‘关键算法点之一’如果返回的是0xE7,那么edi为0x177
mov eax, [ecx+34h]
test eax, eax
setz dl
mov [esp+38h+var_10], edx
cmp ebx, 0E7h
jz loc_550267 ; 如果这一条路想走通,必须满足:ebx为0xE7,edi为0xDB
我们注意到,前面调用了两个函数:sub_403963和sub_40897C。并且,sub_403963的返回值决定了ebx的值,而sub_40897C的返回值决定了edi的值。
如果jz跳转成立,则跳转到了图2处,则此时满足ebx为0x2d,edi为0xDB既可实现注册。
下面,分析sub_616050函数
.text:00616081 jz loc_6163DF ; 类中的标志位比较
.text:00616087 mov ecx, [esi+8]
.text:0061608A cmp [ecx+8], ebp
.text:0061608D jz loc_6163DF ; 类中的标志位比较
.text:00616093 push edi
.text:00616094 lea edx, [esp+30h+var_18] ; edx的值就是存放处理之后的密码串的地址
.text:00616098 push edx
.text:00616099 mov ecx, esi
.text:0061609B call sub_40889B ; 这个函数里面对输入的密码进行了操作
.text:006160A0 mov edi, offset off_7D58B8
.text:006160A5
.text:006160A5 loc_6160A5: ; CODE XREF: sub_616050+71j
.text:006160A5 mov eax, [edi]
.text:006160A7 push eax
.text:006160A8 mov ecx, ebx
.text:006160AA call ds:??8QString@@QBE_NPBD@Z ; QString::operator==(char const *)
.text:006160B0 test al, al
.text:006160B2 jnz loc_616279
.text:006160B8 add edi, 4
.text:006160BB cmp edi, offset unk_7D58BC
.text:006160C1 jl short loc_6160A5
首先,判断了两个标志位之后,会进入sub_40889B函数对输入的密码串进行操作,每位字符转化成相应的hex值(目前只考虑0-f的情况,不区分大小写)。
现在,我们假设到这里的时候,我们输入的密码串转换成了一个byte数组,称其为数组a。
比如,输入的用户名:loongzyd,密码:0fb6bcace7870c1fe696
数组a:
注意到这个函数有三种返回值:0xE7,0x4E,0x2D,而依据总原则:”edi必须等于0xDB”,加上后面的逆向倒推分析可知:该函数的返回值必须为0x2D。
那现在往下分析,怎么样才能让程序流程执行下去使得函数返回0x2D.
.text:006160C3 mov al, [esp+30h+var_15] ; al = a[3]
.text:006160C7 mov bl, byte ptr [esp+30h+var_14+1] ; bl = a[5]
.text:006160CB cmp al, 9Ch ; 判断a[3]是否为0x9c
.text:006160CD jnz short loc_616149 ; 判断7,8位是不是F,C
如果a[3]为0x9c,则有一条分支往下走;
.text:00616149 cmp al, 0FCh ; 判断7,8位是不是F,C
.text:0061614B jnz short loc_616165
如果a[3]为0xfc,则有一条分支往下走;
.text:00616165 cmp al, 0ACh
.text:00616167 jnz loc_616279 ; 判断7,8位是不是A,C
如果a[3]为0xac,则有一条分支往下走;
结论:a[3]必须为0x9c,0xfc,,0xac这三个值之一,否则就会失败。
当a[3]为0x9c时:
.text:006160CF mov dl, byte ptr [esp+30h+var_14+3] ; 第8位 a[7]
.text:006160D3 xor dl, [esp+30h+var_17] ; dl = a[7] ^ a[1] (第二位)
.text:006160D7 mov cl, byte ptr [esp+30h+var_14+2] ; 第7位 a[6]
.text:006160DB xor cl, [esp+30h+var_18] ; cl = a[6] ^ a[0] (第一位)
.text:006160DF movzx ax, dl ; ax = a[7] ^ a[1]
.text:006160E3 mov byte ptr [esp+30h+var_1C], cl ; [esp+14] = a[6] ^ a[0]
.text:006160E7 mov ecx, 100h
.text:006160EC imul ax, cx ; ax = (a[7] ^ a[1]) << 8
.text:006160F0 mov dl, bl ; dl = a[5] (第6位)
.text:006160F2 xor dl, [esp+30h+var_16] ; dl = a[5] ^ a[2] (第三位)
.text:006160F6 movzx cx, dl ; cx = a[5] ^ a[2]
.text:006160FA mov edx, [esp+30h+var_1C]
.text:006160FE add ax, cx ; ax = (a[7] ^ a[1]) << 8 + a[5] ^a[2]
.text:00616101 push edx ; edx的值来源于[esp+14],等于a[6] ^ a[0]
.text:00616102 movzx edi, ax ; edi = (a[7] ^ a[1]) << 8 + a[5] ^a[2]
.text:00616105 call sub_406D6B ; eax = ((arg0 ^ 0x18) + 0x3D) ^ 0xA7
.text:0061610A movzx eax, al ; eax = (([esp+14] ^ 0x18) + 0x3D) ^ 0xA7
.text:0061610D push edi
.text:0061610E mov [esi+1Ch], eax ; [esi+1c] = (([esp+14] ^ 0x18) + 0x3D) ^ 0xA7
.text:00616111 call sub_4077D4 ; eax = (((arg0 ^ 0x7892) + 0x4d30) ^ 0x3421) / 0xB
.text:00616116 mov ecx, [esi+1Ch] ; 上一步中,除以0xB必须要整出,否则eax = 0,失败
.text:00616119 movzx eax, ax
.text:0061611C add esp, 8
.text:0061611F mov [esi+20h], eax ; 假设得到的eax恰好为0x3E8
.text:00616122 test ecx, ecx ; 坚决不能转 [esi+0x1C]的值不能为0
.text:00616124 jz loc_616279
要进过上述的计算,为了不是其跳转到eax=0xE7分支上面造成失败,必须满足:
1.((a[6] ^ a[0] ^ 0x18) + 0x3D ) ^ 0xA7 != 0 (令商为dev1)
2.arg0 = (a[7] ^ a[1]) << 8 + a[5] ^ a[2]
((arg0 ^ 0x7892 + 0x4d30 ) ^ 0x3421) &&0x0ffff必须整除0xB,并且商不能大于0x3E8
(令商为dev2)
edi的值影响后面的计算流程
.text:006162BA loc_6162BA: ; CODE XREF: sub_616050+24Cj
.text:006162BA mov cl, [esp+30h+var_15] ; 取a[3],判断标志位
.text:006162BE cmp cl, 9Ch
.text:006162C1 jnz short loc_6162D2 ; 判断a[3]是否为0x9c
.text:006162C3 mov eax, [esp+30h+arg_0] ; 参数固定为5
.text:006162C7 or edx, 0FFFFFFFFh
.text:006162CA cmp eax, [esi+1Ch] ; (pass[6] ^ pass[0] ^ 0x18 + 0x3D ) ^ 0xA7的值必须要大于等于5
.text:006162CD jmp loc_616352
条件3:
((a[6] ^ a[0] ^ 0x18) + 0x3D ) ^ 0xA7 >= 5
.text:00616220 mov eax, [ecx+0Ch] ; 此时eax的值,为用户名字符串的地址
.text:00616223 mov edx, [esi+20h] ; 上面除以0xB后的商
.text:00616226 xor ecx, ecx
.text:00616228 cmp [esp+30h+var_15], 0FCh ; 根据a[3]是否为0xFC,来决ecx为0,还是1
.text:0061622D push edx
.text:0061622E setnz cl ; a[3]等于0xfc,则ecx=0,否则ecx=1
.text:00616231 push edi
.text:00616232 push ecx
.text:00616233 push eax ; 根据用户名,进行一系列的运算
.text:00616234 call sub_40263F ; 计算出来的'hash'值,决定了a[4],a[5],a[6],a[7]的值
sub_40263F是一个比较关键的函数,它可以根据上面的几个运算结果及用户名,算出一个’hash’,而这个’hash’可以决定a[4],a[5],a[6],a[7]的值
a[4] = hash & 0xff; a[5] = (hash >> 8) &0xff
a[6] = (hash >> 16) &0xff; a[7] = (hash >> 24) &0xff
将hash获取之后,再根据条件1,2,3我们就可以确定所有的密码字符(情况不唯一),具体参看源代码。
当a[3]为0xac时:
这个和前面的条件2是相同的:
arg0 = (a[7] ^ a[1]) << 8 + a[5] ^ a[2]
((arg0 ^ 0x7892 + 0x4d30 ) ^ 0x3421) &&0x0ffff必须整除0xB,并且商不能大于0x3E8
.text:006161BB movzx ecx, [esp+30h+var_F] ; ecx = A[9]
.text:006161C0 movzx eax, byte ptr [esp+30h+var_14] ; eax = a[4]
.text:006161C5 movzx edx, bl ; edx = a[5]
.text:006161C8 xor ecx, edx ; ecx = A[9] ^ a[5]
.text:006161CA movzx edx, [esp+30h+var_10] ; edx = A[8]
.text:006161CF xor eax, edx ; eax = a[4] ^ A[8]
.text:006161D1 movzx edx, [esp+30h+var_18] ; edx = a[0]
.text:006161D6 shl ecx, 8
.text:006161D9 add ecx, eax ; ecx = (A[9] ^ a[5]) << 8 + a[4] ^ A[8]
.text:006161DB movzx eax, byte ptr [esp+30h+var_14+2] ; eax = a[6]
.text:006161E0 shl ecx, 8 ; ecx = ((A[9] ^ a[5]) << 8 + a[4] ^ A[8]) << 8
.text:006161E3 xor eax, edx ; eax = a[6] ^ a[0]
.text:006161E5 add ecx, eax ; ecx = ((A[9] ^ a[5]) << 8 + a[4] ^ A[8]) << 8 + a[6] ^ a[0]
.text:006161E7 push (offset loc_5B8C26+1)
.text:006161EC push ecx
.text:006161ED call sub_403882
.text:006161F2 mov ebp, eax ; 不为0,且要大于等于0x3c70
.text:0061634B or edx, 0FFFFFFFFh ; sub_403882的返回值必须大于0x3c70
.text:0061634E cmp [esp+30h+arg_4], ebp ; 参数固定为0x3c70
sub_403882是个关键的函数,参数的值ecx涉及了多个密码数组元素,它必须满足返回值不为0,同时要大于等于0x3c70
.text:00616220 mov eax, [ecx+0Ch] ; 此时eax的值,为用户名字符串的地址
.text:00616223 mov edx, [esi+20h] ; 上面除以0xB后的商
.text:00616226 xor ecx, ecx
.text:00616228 cmp [esp+30h+var_15], 0FCh ; 根据a[3]是否为0xFC,来决ecx为0,还是1
.text:0061622D push edx
.text:0061622E setnz cl ; a[3]等于0xfc,则ecx=0,否则ecx=1
.text:00616231 push edi
.text:00616232 push ecx
.text:00616233 push eax ; 根据用户名,进行一系列的运算
.text:00616234 call sub_40263F ; 计算出来的'hash'值,决定了a[4],a[5],a[6],a[7]的值
同样,也是根据用户名计算出一个hash,值得注意的是,当a[3]为0xac的时候,sub_40263f的一个参数是之前sub_403882函数的返回值。
大致的逻辑就可以确定了,详细见源代码。
当a[3]为0xfc时,这种情况不能满足条件。
下面关注一下获取hash的函数sub_614560,总体来看是根据用户名字符串依次进行循环操作,因为设计很多的移位,特别是imul指令,不太好用C语言表示,故直接用内联汇编来编写:
hash获取函数
int get_hash(char *name, int arg4, int arg8, int argc)
{
int name_length;
int argc_tmp;
int arg8_tmp;
int eax_tmp;
int ecx_tmp;
int edx_tmp;
int index_i;
int index_j;
int index;
int hash;
name_length = strlen(name);
argc_tmp = argc << 4;
argc_tmp -= argc; //edi
arg8_tmp = arg8 << 4;
arg8_tmp += arg8; //esi
index_i = 0;
index_j = 0;
hash = 0;
if (arg4 != 0)
{
for(index = 0; index < name_length; index++)
{
if (name[index] >= 'a' && name[index] <= 'z')
{
eax_tmp = name[index] - 0x20;
}
else
{
eax_tmp = name[index];
}
_asm
{
mov eax, eax_tmp
mov ecx, hash_table[eax*4]
lea edx, [eax+0xd]
and edx, 0xff
add ecx, hash
xor ecx, hash_table[edx*4]
add eax, 0x2f
and eax, 0xff
imul ecx, hash_table[eax*4]
mov edx, arg8_tmp
and edx, 0xff
add ecx, hash_table[edx*4]
mov edx, index_j
mov eax, argc_tmp
and eax, 0xff
add ecx, hash_table[eax*4]
and edx, 0xff
add ecx, hash_table[edx*4]
mov hash, ecx
}
index_j += 0x13;
argc_tmp += 0xd;
arg8_tmp += 9;
}
}
else
{
for(index = 0; index < name_length; index++)
{
if (name[index] >= 'a' && name[index] <= 'z')
{
eax_tmp = name[index] - 0x20;
}
else
{
eax_tmp = name[index];
}
_asm
{
mov eax, eax_tmp
mov ecx, hash_table[eax*4]
lea edx, [eax+0x3f]
add edx, hash
add eax, 0x17
and ecx, 0xff
xor ecx, hash_table[ecx*4]
and eax, 0xff
imul ecx, hash_table[eax*4]
mov eax, arg8_tmp
and eax, 0xff
add edx, hash_table[eax*4]
mov eax, index_i
mov ecx, argc_tmp
and ecx, 0xff
add edx, hash_table[ecx*4]
and eax, 0xff
add edx, hash_table[eax*4]
mov hash, edx
}
index_i += 0x7;
arg8_tmp += 0x9;
argc_tmp += 0xd;
}
}
return hash;
}
其中,要涉及一个数组表,这里采用IDA中的IDC脚本语言来获取(具体参加代码),运行之后在C盘根目录中会出现一个1.txt文件,里面就是需要的数组。
IDC脚本:
auto address;
auto num;
auto str;
auto index;
auto file_handle;
address = 0x7D53E8;
index = 0;
file_handle = fopen("c:\\1.txt", "w");
writestr(file_handle, "hash_table[] = {");
while(1)
{
num = Dword(address);
if (num != 0)
{
str = ltoa(num, 16);
str = "0x" + str + ", ";
Message("%s\n", str);
writestr(file_handle, str);
address = address + 4;
index = index + 1;
if (index % 4 == 0)
{
writestr(file_handle, "\n");
}
}
else
{
break;
}
}
writestr(file_handle, "};");
fclose(file_handle);
Message("over\n", num);
比较纠结的还有一个,sub_614880,逆向推理之后进行了偷懒,简化成:
(不是考虑了所有的情况)
for (i = 0x40370; i < 0xffffff; i += 0x11)
{
_asm
{
mov eax, 0x0f0f0f0f1
mov ecx, i
mul ecx
shr edx, 4
mov t, edx
}
if (t < 0x3c70)
{
continue;
}
if (t * 0x11 == i)
{
符合条件
}
}
通过上面的分析,算法部分就完成了,注册机里面的密码,绝大部分都是符合算法本身的;
不过因为软件有联网的检测,注册一段时间之后不能重新注册,应该是和MAC地址绑定了,在虚拟机注册之后回滚了,然后几天之后发现同样的用户名和密码就不能用了。
有两处是需要注意的:
.text:00617B53 cmp dword ptr [esi+2Ch], 0 ; 如果[esi+0x2C]不为0,则返回的是0x113,应该就失败了
.text:005500D8 cmp dword ptr [ecx+2Ch], 0 ; 这里ecx应该是某个对象的指针 要搞清楚[ecx+0x2C]的含义
这两处中ecx和esi的值是一样的,所代表的含义也是一样的。在第一次进行注册之前,[this+0x2c]为0,用户名和密码符合算法要求后即可正确注册,虚拟机回滚几天之后,再次注册的时候,[this+0x2c]的值变成了1,意味着即使有相同的用户名和密码也不能注册了。估计是通过网络验证限制了一台机器的注册次数,当我们验证的时候需要注意到这点,[this+0x2c]的值和我们输入的用户名和密码是无关的,手动修改即可。
简易注册机:
010editor.txt
注册机算法里面,简略了很多数学的操作,从严谨上来说,忽略了很多情况,只是能找到符合算法的用户名和密码。从使用角度上来说,爆破即可。
赶着末日重生第一天,有些措辞或者介绍的不详细,慢慢修复,敬请谅解
[课程]Linux pwn 探索篇!