
笔者的博客情节
从高中时代开始,笔者就开始以各种各样的姿势折腾自己的博客:一开始用现成的博客网站,然后开始折腾wordpress一类的博客系统,再后来开始自己用各种语言写博客系统……
总之,不是在折腾就是在去折腾的路上(惭愧的是博文没写几篇……)。然而,折腾了这么久,始终总有这样或者那样的不满意:楼主遇见过博客网站倒闭,wordpress的主题和功能不满意,自己写的系统要托管到vps上……
其实,上面说的问题总结一下,无非是如下几个需求:
- 基础设施要稳定可靠
- 博客系统要完全可控
- 写作要方便
恰好参加工作后,入手了一台群晖的NAS。有个笑话说,其实群晖卖NAS硬件只是交个朋友,真正值钱的是DSM系统,不得不说DSM的确非常好用,不仅有丰富的应用,而且支持和生态也都建设的很到位。
其实一开始笔者是用Evernote国际版的,后来Evernote对免费用户越来越不友好逼得笔者只好弃坑,再后来辗转过Onenote、有道云笔记、为知笔记等等,最终都因为这样那样的问题而放弃,直到用到了DSM的NoteStation。
先来看看NoteStation的优势吧(以笔者的需求来评价是否是优势):
- 优秀的多平台支持(尤其是Linux和Android平台)
- 有一个类似Evernote的Chrome插件(这个插件可以用来快速保存网页,简直是转载神器)
- 私有云(基础设施完全可控)
回头看一下上面的优势和对博客的需求会发现,契合度其实非常的高。然而NoteStation有个问题:这玩意是闭源的,不能二次开发。
笔者一度想用NoteStation作为博客后台进行写作,然而也是因为这东西闭源一直没有将想法落地,直到我看到了这篇1文章,笔者强烈建议各位在继续阅读之前大概看一下这篇文章。
在看过上述提到的文章1后,笔者基本了解了NoteStation的文档组织方式,接下来便是逐步落地最初的"用NoteStation搭建一套博客系统"的想法。
编码问题
在落实的过程中,笔者遇到一个关键问题,先来看一下笔记的存储结构:

上面截图中的metatext.json、basic.json等都是存储一些笔记的基本信息用的,比较诡异的是version/text这个目录下的东西,本身这个目录下的文件是存储笔记正文内容用的,但是这个{XX}的形式是个什么鬼?而且整个文件名部分看起来像是某种特殊的编码,最重要的是这个编码应该是私有编码。
如果不能理解这个编码,后续的博客后台代码将无法“优雅”的实现——总不能硬编码到程序里面吧。好不容易弄清了NoteStation的文件组织结构,难道因为这么一个小小的点放弃笔者多年以来的想法吗?当然答案各位看官肯定已经知道了,不然哪来的这篇博文。
编码问题的解决
DSM系统本质上还是一个网站,凡是网站无非就是:WebServer + App的模式,而NoteStation作为DSM这个“网站”的一个App,自然需要有这个App的服务程序。
稍微对DSM做一下研究即可知道,DSM中大部分的App都是以cgi的方式实现的(为什么要用cgi可能是因为闭源需求吧,不然php这种约等于公开),DSM本身是一套运行在*nix系统上的Web服务,NoteStation以cgi应用的方式和用户交互。
因此,可以先看一下so文件(*nix系统上的可执行程序)的情况。在DSM一开始安装的时候,需要指定一个安装盘,换句话说,所有的DSM应用都会被安装在这个盘下面(笔者这里为/volume2),先用如下命令看一下NoteStation的安装目录下,有哪些so文件:
- find ./ -type f -iname '*\.so*'
结果如下:

通过目录,我们基本可以猜到,webapi下的文件可能主要是用于提供和用户的Web页面交互,lib目录下的文件主要提供各种基础功能的支持。
然而这并没有什么卵用——我们需要知道上文中提到的神秘编码是如何解码的。
其实也不是毫无头绪,至少我们知道程序要处理version/text目录下的文件,version/text就是我们的一个突破口。
由于群晖的系统缺少很多基础命令,因此笔者将文件打了个包一起拽了下来,打包命令如上图最后一行:
- find ./ -type f -iname '*\.so*' -exec cp {} /volume1/Documents/Sos \;
然后使用如下命令,提取所有so文件内的可见字符串,并将包含text/的字符串过滤了出来:
- strings -f * | grep -Pi 'text/'
结果如下:

运气比较好,并没有几个文件。而且上面说的webapi和lib两个目录其实还是有点卵用的,基本上我们只需要看libsynodrive.so.6.0这个文件就行了,另外几个文件其实都是和HTTP相关的文本(而且这几个文件都在webapi目录下)。
祭出万能的IDA,shift+f12提取字符串,定位到/text/*处:

有两处引用,经过观察sub_583a0+9C处的引用不是我们需要的,现在只剩下一处了,简直爽歪歪。来到这处调用处,发现是给pattern的一个赋值(下图中的171行):

接着追踪pattern的交叉引用发现上图中的303行是关键位置,因为v107通过std::string::assign赋值给了v129,而306行的SYNODriveDecode函数引用了v129变量,SYNODriveDecode这个函数怎么看都像是能回答我们如何解密神秘编码的函数:
- __int64 __fastcall SYNODriveDecode(const std::string *a1, unsigned __int8 *a2, size_t a3, char a4)
- {
- // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]
- v4 = *(const char **)a1;
- v5 = std::string::_Rep::_S_empty_rep_storage;
- n[0] = a3;
- v6 = *((_QWORD *)v4 - 3);
- v27[0] = (__int64)off_2E7F50 + 24;
- if ( !v6 )
- {
- //省略错误处理代码
- ……
- LABEL_3:
- ……
- goto LABEL_4;
- }
- if ( a4 )
- {
- std::string::assign((std::string *)v27, a1);
- v11 = 0LL;
- LABEL_8:
- bzero(a2, n[0]);
- // SLIBCBase64Decode为DSM封装的一个函数,猜测是包装了base64的解码函数
- if ( (unsigned int)SLIBCBase64Decode(v27[0], *(_QWORD *)(v27[0] - 24), a2, n) )
- {
- v8 = 1;
- }
- else
- {
- //省略错误处理代码
- ……
- v8 = 0;
- }
- }
- else
- {
- v11 = (char *)calloc(v6 + 1, 1uLL);
- if ( !v11 )
- {
- //省略错误处理代码
- ……
- goto LABEL_3;
- }
- snprintf(v11, v6 + 1, "%s", v4);
- v22 = &v25;
- while ( 1 )
- {
- v13 = strchr(v11, '{');
- v14 = v13;
- // 不存在 { 字符时会进入这个if代码
- if ( !v13 )
- {
- v21 = strlen(v11);
- std::string::append((std::string *)v27, v11, v21);
- goto LABEL_8;
- }
- *v13 = 0;
- v15 = strlen(v11);
- std::string::append((std::string *)v27, v11, v15);
- *v14 = '{';
- v16 = strchr(v14, '}');
- // 没有更多的 {xx} 形式的字符需要处理了
- if ( !v16 )
- break;
- *v16 = 0;
- v11 = v16 + 1;
- v17 = strtol(v14 + 1, 0LL, 10);
- *(v11 - 1) = '}';
- std::string::string(v28, 1LL, (unsigned int)v17, &v25);
- // 将 {xx} 中的 xx, 使用strtol以10进制转换
- // 然后将传唤后的整数作为字符append到待处理字符串最后
- std::string::append((std::string *)v27, (const std::string *)v28);
- v18 = v28[0] - 24;
- if ( (void *)(v28[0] - 24) != v5 )
- {
- //省略错误处理代码
- ……
- }
- if ( !v11 )
- goto LABEL_8;
- }
- syslog(3, "%s:%d Failed [%s], err=%m\n", "common/synodrive_common.cpp", 839LL, "NULL == szEnd");
- SYNODriveErrAppendEx("common/synodrive_common.cpp", 839, "NULL == szEnd", (char)&v25);
- v8 = 0;
- }
- LABEL_4:
- free(v11);
- //省略错误处理代码
- ……
- return v8;
- }
好了,逻辑基本清楚了:将{}中的字符串提取出来,然后转换成10进制对应的数字,最后10进制数字的ASCII值作为字符替换到原文中的原位置,最后用私有函数SLIBCBase64Decode解码。
关于SLIBCBase64Decode私有函数,笔者也懒得找在哪实现的了,暂时先闭着眼睛认为就是Base64Decode函数了(有符号就是爽:Synology-LIBC-Base64Decode)。
还是有点抽象,以version/text/Y{110}J{112}ZWY{61}为例:
- ASCII(110) -> n
- ASCII(112) -> p
- ASCII(61) -> =
因此version/text/Y{110}J{112}ZWY{61}转换为:version/text/YnJpZWY=
现在顺眼多了,地球人都知道YnJpZWY=是base64:

原来是简介的意思~
Coding Time
验证没问题后,接下来便是最简单的部分。
由上述分析易得用于解码的代码(Typescript语法):
- const bs_filenameDecode = (filename : string) : string => {
- let decoded_filename = '', current_position = 0;
- while (true) {
- const curly_start_position = filename.indexOf('{', current_position);
- if (curly_start_position != -1) {
- const curly_end_position = filename.indexOf('}', curly_start_position);
- decoded_filename += filename.substring(current_position, curly_start_position);
- decoded_filename += String.fromCharCode(
- parseInt(filename.substring(curly_start_position + 1, curly_end_position))
- );
- if (curly_end_position + 1 === filename.length) break;
- current_position = curly_end_position + 1;
- }
- }
- return decoded_filename;
- }
至于为什么NoteStation的作者会用这么奇怪的编码方式,笔者也猜不出来。不过libsynodrive.so.6.0文件还暴露了一个叫SYNODriveEncode的函数,这个函数用来编码一个“神秘编码”出来,这里就不贴这个函数的IDA HexRay代码了,直接说结果: 函数将传入字符串用base64编码,然后将所有[A-Z0-9]之外的字符,转换成 {对应ascii数值} 的形式。
对应的编码逻辑如下(Typescript语法):
- const bs_filenameEncode = (filename : string) : string => Buffer.from(filename)
- .toString('base64')
- .split('')
- .map(
- c => /[A-Z0-9]/.test(c) ? c : `{${c.charCodeAt(0)}}`
- ).join('');
有了bs_filenameDecode和bs_filenameEncode就可以愉快的读取NoteStation各种笔记的简介、正文相关的数据了。
后记
其实整个博客系统已经完成了,只是介于笔者是懒癌晚期患者,到现在才动笔写点东西出来。
整个博客的工程化过程中,其实涉及到很多安全方面的问题——各位不会认为后台直接跑在群晖的NAS上吧?不会以为整个系统没有隔离吧?不会认为随便什么笔记都能通过博客系统查看吧?这些相关的问题,有机会再专门写东西跟各位看官分享吧,这次先写到这里。
最后来一张博客后台的截图,没什么别的意思,纯属炫耀一下:
