[计算机安全] 爆破欧帝黑板硬盘还原软件writeup
0.豪迈(?)的前言
征服还原软件(俗称还原卡,源自早期的硬件解决方案,本文二者混用)一直是千万学生的梦想,也是笔者从2020年夏天就开始攻克的难题。从远古的增霸卡到机房dell自带还原,到欧帝黑板,从D盘不还原到全面沦陷,这一问题始终困扰着我们,限制着班级电脑的全部潜力。进入欧帝黑板时代,其软件能在Windows启动后设置且粗制滥造的特点使这一现状有所转机。在2022年末笔者该问题解决后,根据计算机安全传统,这里写一篇简短的writeup。(大嘘)(实际上只有欧帝适用)
1.关于软件本身
这玩意PE信息里填的公司名是"Company",安装位置是C:\Program Files (x86)\Company\PROT
,不认真找很容易跳过这个一点也不像还原软件的目录,实属难蚌。
另外PE信息中的版本完全不对,上述exe应是10.0.3
。另有10.0.5
与10.0.35
版,处理方法相同,此处以10.0.3
为例。
安装方法是由Setup-Odin-10.0.x.exe
安装包解包到PROT文件夹,然后利用Install.exe
启动主界面,设定密码(8位以内,大小写字母+数字),执行对硬盘的安装。对电脑环境的检测貌似是在安装包时期进行的,似乎会检测架构与BIOS模式,貌似只支持EFI。然而笔者最初只搞到了PROT
文件夹,跳过了该步骤,在两台UEFI机器上安装(真机安装,作死),喜获两台蓝屏(世界名画)花了一天修好,好在损失不大。
除了Install.exe
以外PROT
还包含一些其他的工具和库,比如核心安装工具DprotSetup.exe
和MFC,VC库等。然而除了Install.exe
别的文件都没什么卵用。
笔者曾经sb地认为由于Install.dat
包含AdminPwd
字串就必然以某种方式储存了密码(可能是哈希),折腾了半天,后来发现在UTF-16下这玩意就是个本地化文件(没救了)
笔者在22年10-11月进行了大量的行为分析,可以观察到的行为包括:
- 安装dprot
驱动,修改原来的硬盘驱动
- 在系统分区创建\ProtSpac\~ProtSpac.dat
,大小数十MB,可能是某种缓存
- 修改BCD信息(导致笔者蓝屏的元凶)
- 通过某种方法(可能是BCD)将开机启动序列引导到Pre-OS界面,夺取某些系统权限
- 修改注册表硬盘相关信息
- 在注册表写入软件注册信息
- 复制System32.dll
到一个临时目录(???)
2.思路(Zzzyt智障之旅)
最早的发现来自2020年夏:通过CheatEngine修改剩余尝试次数来实现无限试密码。一般的学生级的爆破方式到这里基本上就结束了,下面不是bruteforce就是偷看密码 问密码 社会工程学。笔者当年技术很渣写了一些模拟按键脚本遍历密码,但速度显然是不够的。
到2022年笔者再次回顾这个问题,断定了模拟按键是行不通的,因为根据之前维修人员改密码时瞄到的信息,该密码至少6位且相当复杂。
2022年12月7日笔者(意外)拆除了人生的第一张还原卡。大教室电脑有BIOS锁,而小教室没有,可以进入WinPE系统。笔者试图用PE系统绕过还原软件劫持Windows的过程,在PE下卸载还原卡的驱动,然而不慎搞废了系统,陷入了和之前一样的蓝屏状态。然而老学长的智慧启发了笔者:D盘下有一个GHOST文件保存了未安装还原卡的状态,估计是给维修用的,学长通过还原这个GHOST干掉了还原卡。于是笔者如法炮制,消除了蓝屏并获得一血(笑)。
然而需要BIOS不加锁,且不能获取明码。于是其他的旁路攻击开始了,包括将还原卡用已知密码装在虚拟机上然后在内存和硬盘中全局搜索明码和明码的常见哈希,后来到了连UEFI变量都抽出来扫描的程度,然而一无所获。
于是只有一条路了:二进制分析时代开始。
3.二进制地狱
应该庆幸Install.exe
没有加壳,不然笔者可以和这个项目彻底再见了()
经过漫长的阅读反编译代码与瞎猜,走了很多弯路以后,包括试图寻找strcmp
函数,从MFC库切入等,笔者发现捷径依然是2020年的CheatEngine路线。
用CheatEngine扫内存可以轻易地找到剩余次数计数器retryTimesCounter
,位于0043ce70
处。搜索对该地址的引用即可找出剩余次数逻辑retryCountLogic
函数(笔者命名),位于0041f630
处,其if-else结构是一目了然,可以用伪代码表示如下:
pwdStr = normalizeAndHash(some_input);
if (pwdStr == '\0') {
# 密码错误
if (0 < RetryTimesCounter) {
RetryTimesCounter = RetryTimesCounter + -1;
}
if (RetryTimesCounter == 0) {
# 显示次数用完
}
else {
# 显示剩余次数
}
}
else {
RetryTimesCounter = 5;
# 一些没看懂的函数指针 推测为密码正确操作
}
return;
}
normalizeAndHash
则是另一个重要的函数,我们下文会用到。
事实证明这个猜测是正确的。通过CheatEngine在第一个if的TEST
指令处(0041f682
)加断点,并手动修改ZF
寄存器来改变执行顺序,我们可以直接进入正确分支,从而通过密码检验。
另一种方法是将0041f684
处的JZ
指令替换成NOP
,也可以改变控制流。这种方法方便一些。
这时候第一阶段目标就达成了:通过密码检验,然而我们依然有获取明码的第二阶段。
笔者将目标迅速锁定在normalizeAndHash
函数上,其内部必然进行了某种密码比较。好在调用结构不是很复杂,笔者画了点时间全部阅读了一遍。
normalizeAndHash
(00423cb0
)反编译如下:
void normalizeAndHash(LPCWSTR param_1)
{
int EAXmaybe;
uint EDXmaybe;
uint pwdStringLen;
undefined8 pwdString;
byte hashMaybe [4];
uint local_8;
local_8 = mysteriousXorThingy ^ (uint)&stack0xfffffffc;
hashMaybe[0] = 0;
pwdString = 0;
EAXmaybe = WideCharToMultiByte(0,0,param_1,-1,(LPSTR)0x0,0,(LPCSTR)0x0,(LPBOOL)0x0);
WideCharToMultiByte(0,0,param_1,-1,(LPSTR)&pwdString,EAXmaybe,(LPCSTR)0x0,(LPBOOL)0x0);
pwdStringLen = 0;
EAXmaybe = checkLengthLeq8(&pwdString,(int *)&pwdStringLen);
EDXmaybe = pwdStringLen;
if (EAXmaybe < 0) {
EDXmaybe = 0;
}
hashThingy((byte *)&pwdString,EDXmaybe,hashMaybe);
Ordinal_1042();
mysteriousAssert(local_8 ^ (uint)&stack0xfffffffc);
return;
}
checkLengthLeq8
(00418420
)如下:
int __thiscall checkLengthLeq8(void *this,int *param_1)
{
int iVar1;
int iVar2;
iVar1 = 9;
iVar2 = 0;
do {
/* WARNING: Load size is inaccurate */
if (*this == '\0') {
if (iVar1 != 0) goto LAB_00418443;
break;
}
this = (void *)((int)this + 1);
iVar1 = iVar1 + -1;
} while (iVar1 != 0);
iVar2 = -0x7ff8ffa9;
LAB_00418443:
if (param_1 != (int *)0x0) {
if (-1 < iVar2) {
*param_1 = 9 - iVar1;
return iVar2;
}
*param_1 = 0;
}
return iVar2;
}
这个函数我真的很难理解,它的唯一作用就是判断一个ascii字符串的长度是否小于等于8,我真的不理解,这么喜欢造轮子的吗
hashThingy
(0041aba0
)则是问题的关键:
void __fastcall hashThingy(byte *string,uint len,byte *hashOut)
{
byte *pt2;
byte j;
uint k;
int i;
byte sum;
undefined4 *pt1;
byte jmod8;
if ((ushort)len < 8) {
pt1 = (undefined4 *)(string + (len & 0xffff));
for (k = (8 - len & 0xffff) >> 2; k != 0; k = k - 1) {
*pt1 = 0xcccccccc;
pt1 = pt1 + 1;
}
for (k = 8 - len & 3; k != 0; k = k - 1) {
*(undefined *)pt1 = 0xcc;
pt1 = (undefined4 *)((int)pt1 + 1);
}
}
sum = 0;
i = 8;
pt2 = string;
do {
sum = sum + *pt2;
pt2 = pt2 + 1;
i = i + -1;
} while (i != 0);
*hashOut = sum;
i = 8;
j = 1;
do {
jmod8 = j & 7;
j = j + 1;
*string = (*string << jmod8 | *string >> 8 - jmod8) ^ sum;
i = i + -1;
string = string + 1;
} while (i != 0);
return;
}
不难发现,normalizeAndHash
函数的功能就是检验输入密码的合法性并计算输入密码的哈希。函数到这里似乎就结束了,那么密码比较去哪里了?
经过长时间的排查,笔者最终发现,比较部分没有被成功地反编译,必须直接阅读反汇编输出。
00423d39 e8 62 6e CALL hashThingy
ff ff
00423d3e 8b 4d e8 MOV ECX,dword ptr [EBP + local_1c]
00423d41 8d 55 f0 LEA EDXmaybe=>pwdString,[EBP + -0x10]
00423d44 8b 02 MOV EAXmaybe,dword ptr [EDXmaybe]=>pwdString
00423d46 83 c4 08 ADD ESP,0x8
00423d49 3b 41 46 CMP EAXmaybe,dword ptr [ECX + 0x46]
00423d4c 75 0a JNZ LAB_00423d58
00423d4e 8b 42 04 MOV EAXmaybe,dword ptr [EDX + pwdString+0x4]
00423d51 3b 41 4a CMP EAXmaybe,dword ptr [ECX + 0x4a]
00423d54 75 02 JNZ LAB_00423d58
00423d56 b3 01 MOV BL,0x1
LAB_00423d58 XREF[2]: 00423d4c(j), 00423d54(j)
00423d58 8d 4d 08 LEA ECX=>param_1,[EBP + 0x8]
关键部分在于两个CMP
指令,将EAX
开始的8个字节和ECX+0x46
开始时的8个字节各分为高低位,用DWORD CMP
分别比较。看到这里,已经能猜测出EAX处的8个字节为输入密码的哈希,而ECX+0x46
则是正确密码的哈希。这段汇编写成伪代码如下:
bool password_correct=false;
unsigned int* EAX,ECX;
if(*EAX==*(ECX+0x46)){
EAX++;
if(*EAX==*(ECX+0x4a)){
password_corect=true
}
}
而这个第6行的flag实际上对应BL
寄存器,在00423d56
处将BL寄存器设为1
。实验证明,手动将BL
设为1
也能触发密码正确。
那么只剩hashThingy
了。
4.欧帝,你真的会写哈希吗?
解读hashThingy
是一个极为痛苦的过程,但最终解读完毕时,我都怀疑自己读的是不是正经哈希()
不多讲了,上C++
unsigned char rol(unsigned char x,unsigned char y){
return (unsigned char)(x<<y)|(unsigned char)(x>>(8-y));
}
void hsh(char *s,int len){
for(int i=7;i>=len;i--){
s[i]='\xcc';
}
unsigned char sum=0;
for(int i=0;i<8;i++){
sum+=s[i];
}
unsigned char j=1;
for(int i=0;i<8;i++){
unsigned char jm=j&7;
j++;
s[i]=rol(s[i],jm)^sum;
}
printf("sum= %02X hash= ",sum);
for(int i=0;i<8;i++){
printf("%02X ",(unsigned char)s[i]);
}
putchar('\n');
}
其中rol
是8位按位左旋函数。用人话说就是:
- 不足8个字符的,用
0xcc
补至8个字 - 按
unsigned char (byte)
处理,计算各字符和。 - 将第
i
个字符左旋i%8
位 - 将每一个字符与
sum
取异或
相信各位已经看出问题了:ROL
和XOR
运算都是可逆的。我们只需遍历sum
所有256种可能的值,逆向进行第4,3步,最后检查和是否为猜测的sum
以及字符是否全部为大小写字母数字或0xcc
即可破解明文。
这玩意的密码哈希函数极为拉跨,可以在1ms内爆破,建议写这玩意的老哥学点基本的密码学常识()
我连hashcat都端出来了,结果你一个ROL一个XOR就结束了,我大受震撼---笔者的QQ空间
很轻松地就能写出逆向哈希:
unsigned char ror(unsigned char x,unsigned char y){
return (unsigned char)(x>>y)|(unsigned char)(x<<(8-y));
}
inline bool isvalid(unsigned char c){
return ('0'<=c&&c<='9')||('A'<=c&&c<='Z')||('a'<=c&&c<='z')||c==0xcc;
}
void reverse_hsh(unsigned char* hash){
for(int i=0;i<=255;i++){
unsigned char sum=(unsigned char)i;
unsigned char s[10]={0};
memcpy(s,hash,8);
bool flag=true;
for(int j=0;j<8;j++){
s[j]^=sum;
int jm=(j+1)&7;
s[j]=ror(s[j],jm);
flag=isvalid(s[j]);
if(!flag)break;
}
if(!flag)continue;
unsigned char sum2=0;
for(int j=0;j<8;j++){
sum2+=s[j];
}
if(sum2==sum){
printf("recover sum= %02X pwd= ",sum);
for(int k=0;s[k]!=0xcc&&k<8;k++){
putchar(s[k]);
}
putchar('\n');
}
}
}
接下来就简单了,用CheatEngine在00423d49 CMP
(或00423d46 ADD
,这是最初使用的位置)加断点,读取ECX+46
到ECX+4d
的8个字节,逆向哈希,获得明码。
2022年12月19日,笔者潜入教室,将上述技术付诸实践,终于获得了千百人梦寐以求的密码:
故事就到此结束了。
下面是破解程序全文:
#include<stdio.h>
#include<string>
unsigned char rol(unsigned char x,unsigned char y){
return (unsigned char)(x<<y)|(unsigned char)(x>>(8-y));
}
void hsh(char *s,int len){
for(int i=7;i>=len;i--){
s[i]='\xcc';
}
unsigned char sum=0;
for(int i=0;i<8;i++){
sum+=s[i];
}
unsigned char j=1;
for(int i=0;i<8;i++){
unsigned char jm=j&7;
j++;
s[i]=rol(s[i],jm)^sum;
}
printf("sum= %02X hash= ",sum);
for(int i=0;i<8;i++){
printf("%02X ",(unsigned char)s[i]);
}
putchar('\n');
}
unsigned char ror(unsigned char x,unsigned char y){
return (unsigned char)(x>>y)|(unsigned char)(x<<(8-y));
}
inline bool isvalid(unsigned char c){
return ('0'<=c&&c<='9')||('A'<=c&&c<='Z')||('a'<=c&&c<='z')||c==0xcc;
}
void reverse_hsh(unsigned char* hash){
for(int i=0;i<=255;i++){
unsigned char sum=(unsigned char)i;
unsigned char s[10]={0};
memcpy(s,hash,8);
bool flag=true;
for(int j=0;j<8;j++){
s[j]^=sum;
int jm=(j+1)&7;
s[j]=ror(s[j],jm);
flag=isvalid(s[j]);
if(!flag)break;
}
if(!flag)continue;
unsigned char sum2=0;
for(int j=0;j<8;j++){
sum2+=s[j];
}
if(sum2==sum){
printf("recover sum= %02X pwd= ",sum);
for(int k=0;s[k]!=0xcc&&k<8;k++){
putchar(s[k]);
}
putchar('\n');
}
}
}
std::string hsh2(std::string str){
char s[10]={0};
for(int i=0;i<str.size();i++){
s[i]=str[i];
}
hsh(s,str.size());
return std::string(s);
}
void reverse_hsh2(std::string str){
unsigned char s[10]={0};
for(int i=0;i<str.size();i++){
s[i]=str[i];
}
reverse_hsh(s);
}
int main(){
unsigned char buf[18];
while(true){
for(int i=0;i<8;i++){
scanf("%x",&buf[i]);
}
reverse_hsh(buf);
}
return 0;
}
版权声明:
作者:Zzzyt
链接:https://blog.hellholestudios.top/archives/904
来源:Hell Hole Studios Blog
文章版权归作者所有,未经允许请勿转载。
共有 0 条评论