目标软件是GrindEQ word-to-latex, 一个可以在word和latex, mathml之间转换的工具. 为何搞这个就说来话长了.
拖延症老半仙几个月前买了一个标准文档, ISO停售了, IEC官方买的谁知道也是扫描版.
为了将它转为pdf, ABBYY识别, 手工精校, 输出为pdf但是有几个公式无法正确识别, 用图片方式则是无法满足(强迫症)纯文字能复制的标准, 输出成docx后手动复原公式发现word里的公式小里小气的, 最后想到了latex的公式, tex宣传是能精确控制每一个元素的排版语言, 它生成的同样公式已经和扫描版相当接近了.
但是从docx转latex时候, 试了一些在线工具和离线工具pandoc等, 并不能识别到等宽文字, 而原扫描文档正文里面大量混合了关键字排版, 关键字都是等宽粗体.
一个个加太不累人了.
然后看到有人推荐这个word-to-latex, 看界面挺好看的啊, 估计做的也比那些丑东西完善吧, 兴冲冲下载安装, 打开我精校的docx, 一转换word本身直接崩溃了. 然后照着官方试验怎么设置避免崩溃呢, 这软件的10次试用也到期了.
网上搜了下只有些老版本的破解, 爆破, 我所不能接受也! 干脆看看能不能逆出算法吧.
经过查看过期时候弹出的字符串和MessageBox的调用栈, 在GrindEQw2l.cnv定位到了一个提示过期函数:
这个插件是集成在office里面, 该函数在选择输出tex时候调用的, 第一次调用时候msgbox参数固定传入的负值因此不会弹出过期提示, 第二次是非负就弹出过期提示了.
弹出过期提示后会设置注册表, 然后下次就不弹, 干扰分析.
这个gchkloop计数器是在另一个函数func_chkmd5里面循环检测的, 为0则不断检查用户授权的md5, 计次试用期间它的值会在func_placeopt中*2+1, 从0,1,3,7一直递增, 不过到1就不再循环了, 估计也是干扰人的分析, 用算术运算取代逻辑赋1.
10次计次未用完时候, 为何不会弹出过期提示呢? 要么是没有调用这个函数, 要么是调用时候参数是负值. 在虚拟机全新安装了一次, 跟踪发现只有一次负值调用, 第二次不再调用. 那么第二次的调用在哪里发生的呢?
就在刚提到的func_chkmd5:
这个ghashmap里面正常会填充0x100个md5的hash, 他用了类似hmac的算法确保注册用户的授权信息都落在这个map里面, 当我们伪造了一个用户放入注册表后, 它拿出来hash一下就会导致map新增一项纪录(下标操作符在key不存在时候会执行插入), 从而size变成0x101, 实现了正确和错误的注册用户调用的是不同的函数, 因为有abs给参数整形, 调用到func_showExpired时候参数一定是非负, 从而弹出对话框.
那么问题就来了, 这个ghashmap里面预执的那些key, 我们有没有办法碰撞出来呢? 这个key是一个23字节的hashsrc算出来的, 看看它是如何生成的吧:
可见这是个明文加盐操作, 盐在末尾, 来自于官方授权系统发放的注册码, 这个盐和明文的角色在官方看来可能相反, 末尾是15字节明文(他们知道), 然后根据用户注册信息的md5首字节加8字节可变盐在前面, 生成256种组合, 在代码中插入到ghashmap.
要想写注册机而不是爆破, 我们就得通过操作用户授权信息和注册信息末尾的15字节明文, 生成一个在表里存在的hash. 前半部分的8个YN我们不用管, 因为官方设计的就是不管用户名和日期都是啥, 生成的前缀都在256个范围内. 得把后面的15字节的跑出来, 算了下要好多好多显卡年(647667346537838518), 似乎此路不通啊. 那么还有一个办法就是去买一次官方软件, 解出这个后缀, 一查价格$99, 教育优惠是$49, 看了下这里面三个插件cnv里面内置的hashmap表都不同, 要买得买三套, 正在我到处找学生兄弟代购教育授权的时候, 我突然想起了之前的迷思, 计次试用未过期时候, 这个ghashmap是怎么避免插入的? 立刻运行一波, 生成的头部是NYYYNYNY, 附加后缀是tthegaofhecould, 结果是7a125e22cdbfeeff44ec824c3870ecbd, 这字串恰好是在表里的. 而过期后, 生成的头部是NYYYNNNN, 结果dd47febc1be4f5d974ebde499f5dcf44不在表内, 就跑进了报错函数.
在注册表有注册信息的时候, 这个头部的pre值来自注册信息的产品id+版本号+标志位+各种时间+编号+用户名, 经验上好的散列函数每个bit的概率都接近正态分布, 我们很容易"碰撞"出首字节是AE的md5结果. 写了个程序验证, 我选择控制注册信息中一个四字节的"启动次数", 循环2^32次, 跑了下花费7分20秒, 其中命中了16774599次AE. 是不是很接近16777216? 平均一下就是每次碰撞耗时, 26.23us.
然后序列号怎么存储到注册表和怎么显示? 代码在插件的GrindEQw2l.dll里面, 是类似于base64但更换了映射表, 315字节注册信息对应的是420位序列号.
brbEbbb4kXd3ZGcexpCzaSSbbUUbHk12xp6bbbGbsrajb3UbxbbTb1bbbbbbbbbbbbbbbbbbbbabbrbbbbbbbbbbbbbbbbbbbbb
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbuzYgodxObbbabbbbbbbbbURYbbbbGZw!k95iJOHObbbbbbbbbbbbbbbbbbbbbbbbbrbbbb
bbbbbbbqQXpb1bbbbbbbz0KkbbbbaMdqG2bbbbbbbapb1bbTsbBbbbbbbbS1PxdqG2bbbbbqQXpb1bb4obska!bqkbkb*MdqG2b
bzbzqQXpb1bbaQbbbalb36bUbbbbbbbbb2bQqQXpb1bbUbU6CHsbGbb1bbbbbbbbbbbbbbbbbbbbbGbbbbbbbbbUbbbbbbbbbbb
bbbbx4HRe0E!A4xWspY!A3Mh
该段序号是我构造的在launches递增到5时候(就凑巧产生了碰撞)的注册信息.
用户注册信息的二进制格式不再赘述, 只有"关键"的五个时间字段进行了交织编码, 可能是为了让字段的变化尽可能分散避免碰撞吧.
regtime 19,17,15,13
time1 7,5,3,1
time0 6,4,2,0
time8 18,16,10,8
launches 14,12,11,9
regtime 不能小于2020年3月, 注册标志是限期订阅的话, time0从当地时间一天前算过期. time8是time0的镜像.
我又收集了一下其他版本的15字节后缀和base64表, 回头优化下注册机发上来, 有时候生活就像一部老式游戏, 只是为了清掉一个红点就要往返于各个大陆最后开始荒野求生~~~
老版本的算法也是类似, 网上破解都是爆破, 可能是大佬们觉得md5原像攻击不现实吧!
void func_showExpired(
int
msgbox)
{
int
chkloop
=
gchkloop;
if
(msgbox >
=
0
) {
MessageBoxW(gMsgHwnd, gMsgCaption.c_str(), gEvalExpired.c_str(), MB_ICONWARNING);
std::wstring cat(L
"tex"
);
cat.insert(
0
,
"a"
);
/
/
atex
cat.insert(
0
,
"c"
);
/
/
catex
cat.resize(
3
);
/
/
cat
if
(gchkloop) {
gchkloop
-
=
chkloop;
}
}
}
void func_showExpired(
int
msgbox)
{
int
chkloop
=
gchkloop;
if
(msgbox >
=
0
) {
MessageBoxW(gMsgHwnd, gMsgCaption.c_str(), gEvalExpired.c_str(), MB_ICONWARNING);
std::wstring cat(L
"tex"
);
cat.insert(
0
,
"a"
);
/
/
atex
cat.insert(
0
,
"c"
);
/
/
catex
cat.resize(
3
);
/
/
cat
if
(gchkloop) {
gchkloop
-
=
chkloop;
}
}
}
std::string mymd5
=
md5hex(hashsrc,
23
);
int
div16
=
pre
/
16
;
ghashmap[mymd5]
=
div16;
/
/
这里选择一个func, 不能选报错func?
/
/
100
个是
111EDCDC
void (__stdcall
*
)(
int
)func10
=
g_expfuncs[ghashmap.size()];
func10(
abs
(ghashmap[mymd5]));
/
/
abs
(div16)
std::string mymd5
=
md5hex(hashsrc,
23
);
int
div16
=
pre
/
16
;
ghashmap[mymd5]
=
div16;
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
最后于 2023-8-22 18:54
被曾半仙编辑
,原因: