由爱到痛

有道云笔记是个好东西,在认识它之前,我一直使用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来实现上述的开关。

行动

明确本次行动的目标:

  1. 修复纯文本粘贴就变成微软雅黑字体的BUG
  2. 增加功能,加一个开关,开启后粘贴的内容永远是纯文本,不管是从哪里复制来的

思路有了,解决问题的办法也想出来了,只差行动了,我们不能做思想上的巨人,行动上矮子,既然是男人,说干就干!准备好趁手的工具,直接开车!

DLL注入方式使用远程线程注入,这种方式比较经典、简单。

API HOOK技术使用IAT HOOK,这种Hook方式多线程下稳定可靠。API HOOK库我选择的是《Windows核心编程》的随书示例代码中的CAPIHook。也可以使用强大的WinAPIOverride或微软的Detours等。当然手写Inline Hook也是可以的,代码超简短,由于有道云笔记访问剪贴板时不存在多线程并发访问情况,Inline Hook也是没有问题的。

新建一个Win32动态库项目取名YNotePatch,关键代码如下:

#include <windows.h>
#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;elseg_switch = true;}}
}CAPIHook hooker_RegisterClipboardFormatW("User32.dll", "RegisterClipboardFormatW", reinterpret_cast<PROC>(My_RegisterClipboardFormatW));CAPIHook hooker_IsClipboardFormatAvailable("User32.dll", "IsClipboardFormatAvailable", reinterpret_cast<PROC>(My_IsClipboardFormatAvailable));

新建一个Win32应用程序项目取名YNoteStarter,写一个EXE作为启动器,用于启动有道云笔记主程序后注入DLL:

#include "YNoteStarter.h"
#include <windows.h>
#include <TlHelp32.h>
#include <tchar.h>
#include <string>
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<LPTHREAD_START_ROUTINE>(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转载请注明出处

用游戏外挂的方式修复有道云笔记的BUG相关推荐

  1. 高并发-【抢红包案例】之三:使用乐观锁方式修复红包超发的bug

    文章目录 导读 乐观锁 CAS 原理 ABA问题 库表改造 代码改造 RedPacketDao新增接口方法及Mapper映射文件 UserRedPacketServic接口及实现类的改造 Contro ...

  2. 高并发-【抢红包案例】之二:使用悲观锁方式修复红包超发的bug

    文章目录 概述 超发问题分析 使用数据库锁的解决方案 使用悲观锁(排它锁 for update) 使用乐观锁(依靠表的设计和代码) 总结 悲观锁(抽象的描述,不真实存在这个锁) 共享锁(S锁) 排他锁 ...

  3. 有道云笔记markdown全部导出打包下载[带视频演示]

    有道云笔记markdown全部导出打包下载 放弃有道云的理由 没有所见即所得,功能不全,没有快捷键,编写起来特别变扭. 只有搜索没有替换,之前换图床的时候,我一个个拷贝到typora,替换完再拷回来, ...

  4. 有道云笔记分享_《有道云笔记》分享三 ——笔记应该怎么做

    灵感.会议.工作日程.项目进展.图片--随时随地.采用各种方式编辑有道云笔记,让它成为你的高效数字秘书,帮你更好地完成工作,开心生活! 工作中,很多人习惯使用纸质笔记本. 你有没有为了找一个数据翻遍整 ...

  5. 游戏外挂怎么来的?十年经验的老程序员道出了这些不为人知的秘密

    使用vc开发的一款简单的游戏作弊器.游戏加速,VC++源码及测试程序,也就是游戏外挂的意思,总之是游戏作弊器,内含源代码.在Debug目录内有两个文件,game.exe是游戏,GameHack.exe ...

  6. 金山网盾监测:游戏玩家下载西西游戏外挂会中大量***

    金山网盾监测:游戏玩家下载西西游戏外挂会中大量*** 据金山毒霸安全实验室监测,6月17日,金山网盾云端数据突然监测到西西游戏网的当日外挂下载均报危险.西西游戏网是深受外挂玩家欢迎的游戏外挂下载网站, ...

  7. 腾讯手游如何提早揭露游戏外挂风险?

    目前腾讯SR手游安全测试限期开放免费专家预约!点击链接:手游安全测试立即预约! 作者:sheldon,腾讯高级安全工程师  商业转载请联系腾讯WeTest获得授权,非商业转载请注明出处. 文中动图无法 ...

  8. 游戏外挂反外挂技术简介

    1.游戏外挂分类: ⑴按实现方式 ㈠脱机式: 完全脱离游戏客户端程序,可以与游戏服务器自由通讯的外挂程序,开发难度最大, 普通的100多开,对游戏的危害最大 ,严重破坏游戏市场.影响玩家正常游戏.缩短 ...

  9. 网络封包基础,执着游戏外挂教程

    要想在修改游戏中做到百战百胜,是需要相当丰富的计算机知识的.有很多计算机高手就是从玩游戏,修改游戏中,逐步 对计算机产生浓厚的兴趣,逐步成长起来的.不要在羡慕别人能够做到的,因为别人能够做的你也能够! ...

  10. 游戏外挂衍生黑色产业链,代理月入 10 万元

    点击上方"CSDN",选择"置顶公众号" 关键时刻,第一时间送达! "你家游戏上线几天后出现外挂?"这是游戏圈时有的调侃.游戏一火,必出外挂 ...

最新文章

  1. 四种软件架构演进史,会一种就很牛逼了!
  2. linux运行geoserver源码,Linux 下Geoserver 的部署
  3. Python安装selenium启动浏览器
  4. [数据结构]合并有序数组
  5. Java 练习:编写 Java 程序,输入年份和月份,使用 switch 结构计算对应月份的天数。月份为 1、3、5、7、8、10、12 时,天数为 31 天。月份为 4、6、9、11 时,天数为 3
  6. linux python3_在Linux上安装Python 3
  7. 模型优化在风控中的运用(全)
  8. iOS TableView reloadData结束
  9. 《从问题到程序:用Python学编程和计算》——2.4 字符串
  10. MongoDB学习笔记(四)--索引 性能优化
  11. 2018-2019-2 网络对抗技术 20165322 Exp9 Web安全基础
  12. Cadence Orcad Capture在元件库中修改默认封装图文视频教程
  13. 等保之——等级保护2.0要求及所需设备清单
  14. java设计模式三个模式结合_Java设计模式——责任链(结合Tomcat中Filter机制)
  15. java工程license机制_使用truelicense实现用于JAVA工程license机制(包括license生成和验证)...
  16. 实用解析dmp文件内容
  17. 计算机网络技术原理文献,计算机类毕业论文参考文献汇总
  18. 剑指offer.把数字翻译成字符串
  19. 更好的表现,NTP8835替代AD83586B方案(一)
  20. Python3之旅之计算机基础知识

热门文章

  1. springcloud记录篇10-thymeleaf模板引擎
  2. 从0到1搭建大数据平台之数据采集篇
  3. 使用POI实现报表打印功能
  4. SMOTE算法原理及Python代码实现
  5. ImageAI训练自定义数据总结
  6. php零售,ThinkPHP开源新零售小程序_萤火商城系统
  7. 计算机维修管理平台软件,美萍电脑行业管理软件(电脑业务管理系统、电脑维修管理系统、组装业务、电脑装机管理软件)--管理软件,美萍是专家!...
  8. 松下A6驱动面板操作参数设置与保存
  9. openwrt 需要高级浏览器_斐讯K2P刷openwrt设置mentohust
  10. 手写linux系统,在Linux操作系统中使用手写板