前言
Focusky是一款非常优秀的演示制作工具,类似于Prezi,但功能更强大,对中文的支持更好。所以已经成了我做演示的第一选择,但有一个问题一直困扰我:公司使用的内部网络是不能连接到互联网的,而Focusky的导出等功能必须要登录才能使用,这就给我的讲义的分享造成了障碍。培训部门一直要求我把讲义分享出来,我每次发个PDF稿过去,结果都被批评为不愿分享。于是就想找个办法解决一下这个问题。
提示:以下内容仅供技术学习参考,请支持优质国产正版软件。
一、初步思路
最开始想到的思路是抓包分析,然后在内网上搭一个服务模拟响应,从而解决内网登陆问题,但是想想觉得这个方案难以实现,内网管理太严格,不可能随便让你架个服务的,还要修改DNS之类,在本机搭也很麻烦。(其实还是自己不太会 _)
于是决定还是逆向破解一下好了,把需要登录验证的地方绕开就好了呀。于是按这个思路开动。
二、逆向尝试
1.初探
先祭出x64dbg,准备对Focusky.exe 和 fs.exe进行跟踪,跟了好一会才发现不太对劲,搞了半天fs.exe不是正常的可执行文件。仔细观察了一番,发现了Adobe AIR的文件夹,原来这是用Adobe AIR技术开发的ActionScript3应用,就是俗称Flash的东东。
查看了fs.exe的文件头,发现了CWS的字样,这不就是swf文件么。看样子要用到SWF逆向了。
2.再探
SWF逆向对我这个小白来说也是新东西,当然是先搜啊搜啊的呀,下了不少所谓的各种工具,最后发现还是JPEXS Free Flash Decompiler 最管用。这个软件很良心,完全开源,功能强劲。
把fs.exe后缀改成swf,用FFDec打开,果然是swf的喔。但是跟想象中不一样的,没有发现什么资源和代码,只有一个DefineBinaryData和一些少量的脚本。
后来在反编译的脚本里看到:
private function init() : void
{
context = new LoaderContext();
context.allowCodeImport = true;
loader = new Loader();
loader.contentLoaderInfo.addEventListener("complete",onComplete);
loader.contentLoaderInfo.addEventListener("ioError",onError);
loader.loadBytes(new FocuskyAirContentData(),context);
}
果然又嵌套了一层。
3.三探
于是把DefineBinaryData导出成一个swf文件再打开,这下一切都出现了。脚本和资源都一目了然了。
这样很快就可以找到导出EXE的PublishWindow的脚本位置,可以找到几个判断的地方,比如:
对应的P-Code是:
这时只要点击edit p-code按钮就可以对这个p-code修改了,改成iffalse ofs0059就可以实现不用登陆也可以了。
我以为只要保存这个改过的SWF文件,然后导入fs.exe(实际也是swf文件)就应该可以了。事实证明我太天真了,做完这些,再启动Focusky就启动不了啊。囧
不知为什么会这样,难道也有什么自校验之类的?还是FFDec修改保存的SWF文件有错误?总之该怎么办呢?
4.初捷
后来就想啊,这些文件不是都要加载到内存来执行的么,改文件不行那改内存行不行呢?试试呗
又祭出了x64dbg,加载后在内存中搜索对应的P-Code的hexdata
找到了一个唯一的地址,把修改后的P-code代码替换,这里只改一个字节就行了。
然后再运行试一下,奏效!不再提示需要登陆了!
当然,还不是很完美,因为会弹出要升级的窗口。不过这个思路已经通了,只要再找到类似的判断点改掉就行了。
很快内存中需要修改的地方就都就位了,接下来就是要考虑怎么打这个内存补丁了,毕竟不能每次都祭出x64dbg么,还是需要简单点才好啊。
5.试手
打内存补丁我只会一种方法,那就是用Dll劫持。试了一下version.dll,果然可以利用。于是用上Aheadlib,打开VS,开始写代码呀。
与固定内存地址的打补丁稍有区别的是,由于每次运行内存中这个SWF文件的加载地址都是变化的,所以需要先搜索指定的特征码,找到后再改。特征码当然就是P-Code对应的hexdata了,查找算法本来想试一试自己实现一下sunday的算法,不过后来发现了一个更简单的方法。
这个代码段所在的内存区域的大小是固定的,虽然地址每次都变,但大小一直不变。并且需要修改的地方相对于这个区域的偏移是固定的。这样就只需要搜索这个大小的内存区域,属性为PRV、RW的就可以直接打补丁了。
以下是部分代码:
DWORD WINAPI PatchThread()
{
DWORD dwProcessId;
dwProcessId = GetCurrentProcessId();
HANDLE hProcess;
hProcess = OpenProcess(PROCESS_ALL_ACCESS,false,dwProcessId);
ULONGLONG dwAddrStart = 0x0;
ULONGLONG dwAddrEnd = 0x7FFFFFFFFFFF;
ULONGLONG dwAddrCur = dwAddrStart;
ULONGLONG dwOffset = 0x3409F4;
MEMORY_BASIC_INFORMATION mbi;
memset(&mbi, 0, sizeof(MEMORY_BASIC_INFORMATION));
while(true){
dwAddrCur = dwAddrStart;
while(dwAddrCur<dwAddrEnd){
if (VirtualQueryEx(hProcess, (LPCVOID)dwAddrCur, &mbi, sizeof(mbi)) > 0)
{
if (MEM_COMMIT == mbi.State && MEM_PRIVATE == mbi.Type &&
PAGE_READWRITE == mbi.Protect &&
mbi.RegionSize == 0x17B0000//the region size that include the code to patch
)
{
//find it, do patch
ULONGLONG AddrPatch;
AddrPatch = (ULONGLONG)mbi.BaseAddress+dwOffset;
if(CheckTheMem(){
DoPatch();
Debug("Patch is ok! \n");
return 0;
}
return 0;
}else{
dwAddrCur += mbi.RegionSize ;
}
}else{
dwAddrCur=dwAddrStart;
Debug("VirtualQueryEx error:%d",GetLastError());
}
}
Debug("All memory is checked. \n") ;
Sleep(200);
}
return 0;
}
// 入口函数
BOOL WINAPI DllMain(HMODULE hModule, DWORD dwReason, PVOID pvReserved)
{
if (dwReason == DLL_PROCESS_ATTACH)
{
DisableThreadLibraryCalls(hModule);
CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)PatchThread,NULL,0,NULL);
}
else if (dwReason == DLL_PROCESS_DETACH)
{
}
return TRUE;
}
很快这个用于打补丁的version.dll就写好了,放到Focusky的目录,试一下。正常启动,试着导出exe,不会提示需要登陆了,终于可以离线也可导出EXE了。成功!
6.再战
本来到这里我的初始目标已经实现了,解决了Focusky的离线使用问题。但是,呵呵,人都是有点好奇心的么。我就想啊,我能不能干脆让它登录好了,就是离线也能识别成登录状态,这样其他需要登录的功能不是也能使用了不是么。
于是仔细琢磨了一下,发现好像是可以实现的啊,只要把login的脚本改动一下,把需要联网获取的用户状态变成本地获取不就可以了么。
于是就开始动手实践了,当然过程比较漫长啦。毕竟我对ActionScript一点都不熟,再加上没办法直接写AS代码,而是需要写P-Code的代码,就是哪些Push,setlocal 的东西,所以还是花费了一定的时间。最后的用户登录代码大概改成类似这个样子:
public function login(u:String, p:String) : void
{
_logined = true;
var file:File = File.applicationDirectory.resolvePath("user.xml");
stream = new FileStream();
stream.open(file,"read");
data= new XML(stream.readUTFBytes(stream.bytesAvailable));
readData(data);
stream.close();
Global.focusky.gSignalManager.signaled("SIGIN_IN_SUCCESS",this);
}
当然这是用AS3代码示意的,实际是一堆类似下面的东西:
getlocal_0
pushscope
newactivation
dup
setlocal_3
pushscope
getlocal_0
getlocal_3
swap
setslot 3
findproperty Qname(ProtectedNamespace("com.wonderidea.focusky.air.user:User"),"_logined")
swap
user.xml是一个XML格式的文本,这个文本里面包含了用户的基本信息,格式当然是抓的我自己的登录用户信息。示例如下:
<ID>4582165</ID>
<Email>master@focusky.com</Email>
<userName>master</userName>
…………
这些修改还是一样通过version.dll来打内存补丁。最后的结果也是不错的,可以保持和正常联网登录一样的效果。当然那些需要联网使用的资源是用不了的。
总结
整个解决离线登录问题的过程还是很有意思的,第一次接触了ActionScript,感觉这还真是个好东东。只是已经很小众了,有点可惜。
这种打补丁的方式有一个不稳定的弊端,那就是有可能你打补丁的修改和原程序的读取同时发生,这时就会出错了,程序表现得很怪异。所以我通常需要把Focusky的自动登录关掉,然后把打补丁的时间延后到程序稳定启动后。在我的机器上是延后了500ms左右。这样基本上算是比较稳定。
最后,本文只为解决自己使用的痛点。请大家支持国产优质软件。
写作不易,如果觉得不错的,请关注评论一下。