由爱到痛
有道云笔记是个好东西,在认识它之前,我一直使用Windows记事本来保存网上摘抄的文档资料和学习心得体会。某天朋友推荐了有道云笔记,我安装后就不可收拾的爱上了它。那种感觉,就好比一夜之间手扶拖拉机换成了奥迪Q7,从此驶上了码字界的康庄大道。
可就在我对它的爱如火如荼的进行中时,一件痛心疾首的事情发生了。
宋体,是我钟爱的字体,而有道云笔记钟爱的字体则是微软雅黑。就是那么一个兴趣爱好的不同,使我们之间产生了矛盾,并不断被激化,最终影响到了工作和生活,以至于之后一度要和它分手。
问题是这样的,如上图所示,当我正襟危坐的打开有道云笔记开始写点东西的时候,我先将字体设置为宋体,然后开始打字。当我打下了“现在是宋体”这几个字后,接下来我需要从网上摘抄一句话,于是便从Chrome中的网页上复制出了“我走过最长的路就是你的套路”这段话,然后到有道云笔记中粘贴。我想要的效果是,粘贴后,让这句话和当前的字体格式一致,于是便使用纯文本粘贴,没想到,无论是先点右键,再点纯文本粘贴,还是Ctrl+Shift+V,粘贴后,这段文字一定会变成微软雅黑!!这完全是一个BUG,并不是我的用法有问题,在Word中根本就不会这样。虽然微软雅黑是微软的小儿子,但是微软绝不会让自己的大儿子Word干出这种溺爱的事情。
至于直接粘贴,效果如同第二行,带颜色带下划线,和网页中的格式字体一样,更不是我想要的结果。我就是单纯的想把网页上拷贝下来的文字,粘贴为宋体,就有那么难吗?技术文章和笔记的写作,往往需要频繁引用和拷贝网上的各种文档、代码片段,现在有道云笔记的这种情况,让我完全无法写下去。要么就妥协,和宋体说拜拜,通篇文字使用微软雅黑来写,要么还是妥协,每次粘贴后,再拉选这段文字,改为宋体。不得不说,这样使用体验,让人很心累。
我们谈谈吧
都说沟通是解决问题的最好方式,于是我便尝试和有道云笔记沟通一下这个问题。于是我在有道云笔记反馈页面上反馈了该BUG。
没过两天,于2016年9月26日,有道云笔记客服就给我发来了邮件,说这是一个已知的问题,将在后续版本中进行修复。我心里顿时有了一丝愉悦,觉得网易的这波办事效率,还是可以的。于是我便在等待的过程中,继续用麻烦的办法,将复制来的文字一句一句改成宋体。
但接下来结果,让我从期待变成了失望,最终演变成了愤怒。
2016年10月24日,时隔近一个月,总算等来了有道云笔记的更新,而且这次是大更新,版本号直接从4.12变成了5.0。我满心欢喜的下载更新了新版本,然而测试结果却给了我当头一棒,这个微软雅黑的BUG依然存在。
我不灰心,我是一只打不死的小强,我继续反馈,不过与上次反馈不同,这次反馈后并没有收到邮件回复,我不放心,除了在网页上反馈,我还给其之前回复我的邮箱发送了一封邮件。
2016年12月13日,在苦苦的等待与煎熬中,终于又迎来了有道云笔记的一次更新,版本号从5.0变成了5.5。依然是满怀期待的下载更新,然而又吃了当头一棒,微软雅黑的BUG依然存在。
在接来下的日子里,我不停在问题反馈页面、邮箱、在线客服三种渠道上反复反馈该BUG,希望能引起重视,因为这真的很影响使用。
结局是悲催的,尽管后来又有一些更新,但这个BUG,直到2017年4月22日的今天,依然没有修复。
不放弃不抛弃
在这期间多次想过和它分手,但是试用了其它同类产品,如为知笔记、印象笔记,都有让人不如意的地方。依然继续用有道云笔记,每次复制粘贴弄的想发火的时候,都对自己说,咬咬牙,再忍一忍,说不定明天会更好呢。而且不知不觉在有道上积累了大量的文章,想搬迁也不容易了。
很久以前我一直觉得,不要试图去改变一个人,要么改变你自己,要么离开。
直到我有了生命中的第一个女朋友,她是一个漂亮、可爱,充满阳光的女孩子,和她在一起每天都对生活充满希望,看着她走在我前面蹦蹦哒哒开心的样子,一切烦恼都烟消云散。
尽管我们的兴趣爱好有很大不同,性格上也有一些差异,尽管我们身上都有彼此讨厌的一些缺点。但我一直记着她说的那句话:
没有天生合适的两个人,需要的是彼此包容理解与改变。
是的,我们既要为彼此做出改变,也要帮助对方塑造一个更好的自己,这样不是很好么。
现在的我不会轻易说离开。
停止抱怨,冷静分析
抱怨是解决不了问题的,既然要做出改变,就要静下心来分析问题根源所在,并寻找解决方案。
在之前的测试中发现,无论粘贴的来源带不带格式,只要粘贴为纯文本,一定会变成微软雅黑。说明粘贴为纯文本的功能代码上出现了BUG。一个简单的思路是使用OD跟进去调试,找到改字体的代码,在粘贴为纯文本时,跳过改字体相关代码的调用。如何在OD中找到粘贴为纯文本功能的代码,首先想到的是既然要粘贴,有道云笔记肯定会去读剪贴板,而Windows中读剪贴板的API是GetClipboardData,只需在OD中对该API下断点很容易就可以找到粘贴为纯文本的实现代码。不过反汇编代码看起来实在太头疼,本着能偷懒就偷懒的思想,还是应该优先寻求非逆向的实现方案。
思考一下有没有什么变通的方法实现我要的效果,实际上我想要的效果就是,无论哪里来的内容,统统给我粘贴为纯文本,不要乱改我设置好的字体,我设置的是什么字体格式,粘贴后的文本字体格式就保持和当前上下文一致。
既然有道云笔记的粘贴为纯文本功能有BUG,那么直接使用粘贴功能,能不能实现我要的效果呢?
当然是可以的,而且更方便,直接按Ctrl+V就行了,不用按Ctrl+Shift+V,但是有个前提,就是粘贴来源本来就是纯文本。
可是我的粘贴来源都是直接从网页上复制的,怎么可能不带格式呢?基本都不是纯文本吧。
当然可以,只是要进行一个额外操作,先把网页上复制的内容粘贴到Windows记事本里,然后再复制一遍,再粘贴到有道云笔记里。这样文本在Windows记事本里过了一遍,格式就丢掉了。
好想法,那么只要编写一个小程序,监听剪贴板,一旦发现我从网页上复制了带格式的新内容,就对其进行处理,去掉格式,这样我在有道云笔记中Ctrl+V的时候,就是纯文本了。
这个思路可以是实现我要的效果,但是会影响到其它软件,比如你想带格式粘贴到Word中时怎么办?而且这样一来你这台电脑上,再也无法复制粘贴带格式的文本了,严重影响其它软件的使用。剪贴板不是你一个人的,电脑上其它软件也要用,不能乱改剪贴板的内容。
是的,不能影响全局,剪贴板是大家的。那么我有没有办法只让有道云笔记这个软件读剪贴板的时候,永远读到的都是不带格式的纯文本的内容,这样Ctrl+V就是纯文本了。而其它如Word的软件是正常的,剪贴板里是什么就读到什么。
当然可以,使用API HOOK就可以实现,Hook住有道云笔记读剪贴板的API,改掉内容就行了。
这么搞好像游戏外挂一样,注入DLL、API HOOK、改内存之类的操作,让我想到了变速齿轮,它就是Hook了获取时间相关的API,给目标程序提供了错误的时间,让目标程序以为世界都变快了。
是的,善意的谎言让它的世界更美好。
思路已定,那简单了,直接祭出大杀器API Monitor,简单粗暴,快速有效。直接分析有道云笔记在粘贴为纯文本时调用了哪些WindowsAPI,设置过滤器只关注和剪贴板相关的API,分析如下:
当粘贴带格式的文本到有道云笔记时:
当粘贴不带格式的纯文本到有道云笔记时:
一经对比,很快就能找出不同之处。有道云笔记注册了名为"HTML Format"的剪贴板格式,实际上这是一种使用HTML表示富文本的通用格式,从浏览器中拷贝出来的文本正是这种格式。
对比两次粘贴,当粘贴带格式的文本时,有道云笔记询问操作系统关于剪贴板的内容:
RegisterClipbardFormatW("HTML Format")
我要注册"HTML Format"这种格式
49381
注册好了,ID是49381,拿去吧
IsClipboardFormatAvailable(49381)
现在剪贴板里面的东西是“HTML Format”这种格式吗?
TRUE
是的
GetClipboardData(49381)
我要获取剪贴板里“HTML Format”这种格式的内容
0x0d42bd18
好的,获取了,存在这个地址处了
当粘贴不带格式的纯文本时,有道云笔记是这样和操作系统对话的:
RegisterClipbardFormatW("HTML Format")
我要注册"HTML Format"这种格式
49381
注册好了,ID是49381,拿去吧
IsClipboardFormatAvailable(49381)
现在剪贴板里面的东西是“HTML Format”这种格式吗?
FALSE
不是
IsClipboardFormatAvailable(CF_TEXT)
那好吧,那现在剪贴板里面的东西是CF_TEXT(纯文本)这种格式吗?
TRUE
是的
GetClipboardData(CF_UNICODETEXT)
那好吧,我要获取剪贴板里的纯文本内容,以CF_UNICODETEXT(Unicode文本)形式给我
0x0d3d01d8
好的,获取了,存在这个地址处了
区别在于:
当剪贴板中是带格式的文本时,IsClipboardFormatAvailable(49381)返回了TRUE
当剪贴板中是不带格式的纯文本时,IsClipboardFormatAvailable(49381)返回了FALSE
那好办!我们只需要写一个DLL,注入到有道云笔记进程中,Hook IsClipboardFormatAvailable这个API,当有道云笔记询问是不是“HTML Format”这种格式时,我们就用于告诉它,不是!!这样一来,它永远都只会去获取纯文本,从而,我们Ctrl+V粘贴到有道云笔记中的文本,永远都是纯文本!
是的,但最好让用户可以控制,设置一个开关,当开启时,会改变有道云笔记,让它读剪贴板读到的永远是纯文本,当关闭开关时,一切恢复正常,带格式的就是带格式,粘贴后依然带格式。让用户自主选择更棒,因为像我这样的用户,基本上永远都只会粘贴为纯文本,网页上拷贝过来的格式,几乎都要去掉的,否则怎么融入到我文章上下文中,但是Ctrl+Shift+V用起来很不顺手(何况目前还有微软雅黑的BUG),只用Ctrl+V多方便。
是的,我们可以在注入的DLL的DllMain中启动一个线程,使用RegisterHotKey注册一个热键,比如Ctrl+Q,然后启动消息循环来接收WM_HOTKEY消息,启用或关闭API Hook来实现上述的开关。
行动
明确本次行动的目标:
- 修复纯文本粘贴就变成微软雅黑字体的BUG
- 增加功能,加一个开关,开启后粘贴的内容永远是纯文本,不管是从哪里复制来的
思路有了,解决问题的办法也想出来了,只差行动了,我们不能做思想上的巨人,行动上矮子,既然是男人,说干就干!准备好趁手的工具,直接开车!
DLL注入方式使用远程线程注入,这种方式比较经典、简单。
API HOOK技术使用IAT HOOK,这种Hook方式多线程下稳定可靠。API HOOK库我选择的是《Windows核心编程》的随书示例代码中的CAPIHook。也可以使用强大的WinAPIOverride或微软的Detours等。当然手写Inline Hook也是可以的,代码超简短,由于有道云笔记访问剪贴板时不存在多线程并发访问情况,Inline Hook也是没有问题的。
新建一个Win32动态库项目取名YNotePatch,关键代码如下:
#include
#include "APIHook.h"
static UINT g_format = 0;
static bool g_switch = true;
UINT __stdcall My_RegisterClipboardFormatW(LPCWSTR lpszFormat)
{
UINT ret = RegisterClipboardFormat(lpszFormat);
if (wcscmp(lpszFormat, L"HTML Format") == 0)
g_format = ret;
return ret;
}
BOOL __stdcall My_IsClipboardFormatAvailable(UINT format)
{
BOOL ret = IsClipboardFormatAvailable(format);
if (g_switch && format == g_format)
ret = FALSE;
return ret;
}
void WorkThread(void *param)
{
const UINT Q_KEY = 0x51;
if (!RegisterHotKey(NULL, GlobalAddAtom(L"MyHotKey"), MOD_CONTROL | MOD_NOREPEAT, Q_KEY))
return;
MSG msg = { 0 };
while (GetMessage(&msg, NULL, 0, 0) != 0)
{
if (msg.message == WM_HOTKEY)
{
if (g_switch)
g_switch = false;
else
g_switch = true;
}
}
}
CAPIHook hooker_RegisterClipboardFormatW("User32.dll", "RegisterClipboardFormatW", reinterpret_cast(My_RegisterClipboardFormatW));
CAPIHook hooker_IsClipboardFormatAvailable("User32.dll", "IsClipboardFormatAvailable", reinterpret_cast(My_IsClipboardFormatAvailable));
新建一个Win32应用程序项目取名YNoteStarter,写一个EXE作为启动器,用于启动有道云笔记主程序后注入DLL:
#include "YNoteStarter.h"
#include
#include
#include
#include
using std::wstring;
DWORD StartProcess(const wstring &app_name, const wstring &cmd)
{
STARTUPINFO start_info = { sizeof(start_info) };
PROCESS_INFORMATION process_info = { 0 };
if (!CreateProcess(app_name.c_str(), (LPWSTR)cmd.c_str(), NULL, NULL, FALSE, NULL, NULL, NULL, &start_info, &process_info))
return 0;
WaitForInputIdle(process_info.hProcess, INFINITE);
CloseHandle(process_info.hThread);
CloseHandle(process_info.hProcess);
return process_info.dwProcessId;
}
bool InjectModule(DWORD process_id, const wstring &module_name)
{
//获取要注入的模块绝对路径
wchar_t self_path[MAX_PATH + 1] = { 0 };
GetModuleFileName(NULL, self_path, MAX_PATH);
wcsrchr(self_path, L'\\');
wstring inject_module_path = self_path;
size_t last_backslash = inject_module_path.rfind(L'\\');
if (last_backslash == wstring::npos)
return false;
inject_module_path = inject_module_path.substr(0, last_backslash + 1);
inject_module_path += module_name;
if (_waccess(inject_module_path.c_str(), 0) != 0)
return false;
HANDLE process = OpenProcess(PROCESS_ALL_ACCESS, FALSE, process_id);
if (process == NULL)
return false;
//在目标进程中分配内存并写入待注入模块的路径
int mem_size = (inject_module_path.length() + 1) * sizeof(wchar_t);
void *module_name_buffer = VirtualAllocEx(process, NULL, mem_size, MEM_COMMIT, PAGE_READWRITE);
if (module_name_buffer == NULL)
{
CloseHandle(process);
return false;
}
if (!WriteProcessMemory(process, module_name_buffer, inject_module_path.c_str(), mem_size, NULL))
{
VirtualFreeEx(process, module_name_buffer, mem_size, MEM_RELEASE);
CloseHandle(process);
return false;
}
//创建远程线程
HMODULE kernel_module = GetModuleHandle(L"kernel32.dll");
LPTHREAD_START_ROUTINE start_function_addr = reinterpret_cast(GetProcAddress(kernel_module, "LoadLibraryW"));
HANDLE remote_thread = CreateRemoteThread(process, NULL, 0, start_function_addr, module_name_buffer, 0, NULL);
if (remote_thread == NULL)
{
VirtualFreeEx(process, module_name_buffer, mem_size, MEM_RELEASE);
CloseHandle(process);
return false;
}
WaitForSingleObject(remote_thread, INFINITE);
CloseHandle(remote_thread);
VirtualFreeEx(process, module_name_buffer, mem_size, MEM_RELEASE);
CloseHandle(process);
return true;
}
int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow)
{
//奇葩的有道,不带show参数就会重启自身进程
DWORD process_id = StartProcess(L"YoudaoNote.exe", L" show");
if (process_id == 0)
{
MessageBox(NULL, L"无法启动YoudaoNote.exe", L"提示", MB_OK);
return 1;
}
if (!InjectModule(process_id, L"YNotePatch.dll"))
{
MessageBox(NULL, L"无法注入YNotePatch.dll", L"提示", MB_OK);
return 2;
}
return 0;
}
访问 Github https://github.com/charlessimonyi/YNotePatch 查看src和bin
总结
是的,在没有程序源码的情况下要给一个已经编译好的Native程序修复BUG,增加、修改功能往往就是这么做的。把我们的代码编译成DLL注入进去执行,这种方式称为打内存补丁。也可以在目标进程中分配内存,直接用WriteProcessMemory把机器码写进去让它执行,也可以把整个DLL复制到这块内存中,不过需要处理导入表和重定位,比较麻烦。当然也可以打文件补丁,直接修改它的PE文件,不过只改几行指令还好,如果要大量注入代码,也是比较麻烦的,而且万一目标EXE有加壳有压缩或者有完整性校验,就走不通了。注入DLL其实是最简单的,注入后我们就可以在它的进程空间内为所欲为,动它的窗口,拦截和修改它的窗口消息,改内存,改变量的值,改目标代码的跳转流程,替换目标代码,配合VirtualProtect,没有什么是不能动的。当然最大难点还是在于该改什么,什么能改什么不能改,改什么才能实现想要的效果,需要花时间慢慢分析。
新生活
至此,总算可以舒服的使用有道云笔记了,使用YNoteStarter启动有道云笔记,任何文本内容,不管从哪里复制来的,Ctrl+V后都是纯文本,实在是爽哉。使用过程中按Ctrl+Q关闭补丁,恢复本色,带格式的文本粘贴后就是带格式的。
好景不长
可是好景不长,没过几天有道云笔记的富文本编辑器就被我抛弃了,果断拥抱Markdown。
本文由CharlesSimonyi发表于CSDN博客:http://blog.csdn.net/CharlesSimonyi/article/details/70344604转载请注明出处