某国产“自主”操作系统专属 Electron 应用“移植”
2020-12-30_16-53.png
最近在 Linux 相关群组内看到某个国产软件给某个国产系统提供了专版应用,这个专版应用在别的系统上都不可以正常运行,但是有老哥发现这个 xxxx
程序读取了 lsb-release, libyyosdevicea.so 和 os-release,在替换这三个文件成 yy 系统的文件之后,就可以直接登陆了...所以我就进行了分析和学习。
自主规避模式,将某操作系统的名字换成了 yy,而某国产软件的名字换成了 xxxx。请不要无端联想。
分析
首先先解压 deb 包,看到的是一个 Electron 程序的正常架构。
~/d/r/u/v2 > tree.├── control├── control.tar.gz├── data.tar.xz├── debian-binary├── md5sums├── opt│ └── apps│ └── com.xxxx│ ├── entries│ │ ├── applications│ │ │ └── com.xxxx.desktop│ │ ├── doc│ │ │ └── xxxx│ │ │ └── copyright│ │ ├── icons│ │ │ └── hicolor│ │ │ ├── 128x128│ │ │ │ └── apps│ │ │ │ └── xxxx.png│ │ │ ├── 16x16│ │ │ │ └── apps│ │ │ │ └── xxxx.png│ │ │ ├── 256x256│ │ │ │ └── apps│ │ │ │ └── xxxx.png│ │ │ ├── 48x48│ │ │ │ └── apps│ │ │ │ └── xxxx.png│ │ │ └── 64x64│ │ │ └── apps│ │ │ └── xxxx.png│ │ ├── lintian│ │ │ └── overrides│ │ │ └── xxxx│ │ └── pixmaps│ │ └── xxxx.png│ ├── files│ │ ├── blink_image_resources_200_percent.pak│ │ ├── content_resources_200_percent.pak│ │ ├── content_shell.pak│ │ ├── icudtl.dat│ │ ├── libffmpeg.so│ │ ├── libnode.so│ │ ├── LICENSES.chromium.html│ │ ├── locales│ │ │ ├── zh-CN.pak│ │ │ └── zh-TW.pak│ │ ├── natives_blob.bin│ │ ├── pdf_viewer_resources.pak│ │ ├── resources│ │ │ ├── app.asar│ │ │ ├── electron.asar│ │ │ ├── sae.dat│ │ │ └── wcs.node│ │ ├── snapshot_blob.bin│ │ ├── ui_resources_200_percent.pak│ │ ├── version│ │ ├── views_resources_200_percent.pak│ │ └── xxxx│ └── info├── postinst├── postrm├── sign└── usr ├── lib │ └── license │ └── libyyosdevicea.so └── share └── doc └── com.xxxx ├── changelog.Debian.gz └── copyright32 directories, 106 files
这里我们直入正题,看下 opt/apps/com.xxxx/files/
的 xxxx 文件,发现这个文件有点大,IDA 分析不动...所以就先照着别的老哥说的,看下 opt/apps/com.xxxx/files/resources
下的 app.asar。这个文件直接用 asar e app.asar $解压到的目录
就解压开了。比较关键的是下面的函数,应该是通过 node.js 的 module 进行生成 token,然后将这个 token 塞入 http 请求的头部,然后服务器进行特殊校验就可以判断是否允许通过了。
function getToken() { let pathname = path.dirname(__dirname); let wcs = require(path.join(pathname, './wcs.node')); let kp = path.join(pathname, 'sae.dat'); let buf = wcs.get_cc_data(1, kp); return buf.toString('ascii');}function bindToken(session) { session.webRequest.onBeforeSendHeaders( { urls: urlFilters }, (details, callback) => { if (details.url.indexOf('/cgi-bin/mmwebxxxx-bin/webxxxxnewloginpage') > -1) { details.requestHeaders['extspam'] = getToken(); details.requestHeaders['client-version'] = app.getVersion(); } } );}
那么现在就去分析下 wcs.node 了。看了下 wcs.node 的 strings
结果,发现没有什么关键的字符串,并且在 lld
的结果中没有发现 libyyosdevicea.so,猜测用了 dlopen
进行动态加载,所以查了下 dlopen
的交叉引用
Direction Type Address TextDown p sub_7B180+31 call _dlopenDown p sub_800D0+C5 call _dlopen
然后再看了下 sub_800D0 的引用,发现字符串似乎被加密了
.text:0000000000080189 lea rdi, weird_str.text:0000000000080190 mov esi, 2 ; mode.text:0000000000080195 call _dlopen
查了下 weird_str 的交叉引用,看来就是 xor 了下字符串
.text:00000000000845EC mov [rsp+var_8], 0.text:00000000000845F5 lea rax, weird_str.text:00000000000845FC nop dword ptr [rax+00h].text:0000000000084600.text:0000000000084600 loc_84600: ; CODE XREF: sub_844D0+146↓j.text:0000000000084600 mov rcx, [rsp+var_8].text:0000000000084605 xor byte ptr [rcx+rax], 17h.text:0000000000084609 add rcx, 1.text:000000000008460D mov [rsp+var_8], rcx.text:0000000000084612 cmp rcx, 21h.text:0000000000084616 jb loc_84600
写个 IDAPython 脚本解密下
import idautilsimport idcdef get_str(addr, str_len): s = '' for i in range(str_len): temp = idaapi.get_byte(addr + i) if temp == 0: break s += chr(temp) return sdef xor_again(func): op, xor_num, str_len = None, None, None for curr_addr in idautils.FuncItems(func): mnem = idc.print_insn_mnem(curr_addr) if mnem == 'lea': op = idc.get_operand_value(curr_addr, 1) if mnem == 'xor': xor_num = idc.get_operand_value(curr_addr, 1) if mnem == 'cmp': str_len = idc.get_operand_value(curr_addr, 1) enc_s = get_str(op, str_len) dec_s = '' for i in enc_s: dec_s += chr(ord(i) ^ xor_num) print("%x %x %x %s" % (op, xor_num, str_len, dec_s))xor_again(0x844D0)
输出如下
49bf60 1 2c Failed to load symbol "get_hddsninfo" : %s.49bf20 11 13 Failed to load %s.49bfb0 19 2c Failed to load symbol "yy_get_mb_sn" : %s.49c100 14 d yy_is_active49c020 1f 10 yy_get_hwserial49c140 4 13 basic_string::erase49bef0 17 21 /usr/lib/license/libyydevicea.so49c160 17 36 %s: __pos (which is %zu) > this->size() (which is %zu)49bf8d 1e 7 unknown49bff0 17 26 Failed to load symbol "get_mac" : %s.49c0b0 5 14 yy_get_licensetoken49bf40 1b 11 yy_get_hddsninfo49bf95 14 d yy_get_mb_sn49bfdd 1f b yy_get_mac49c040 6 2f Failed to load symbol "yy_get_hwserial" : %s.49c070 11 d yy_get_osver49c0d0 17 2f Failed to load symbol "get_licensetoken" : %s.49bec0 1d 10 /etc/lsb-release49c110 7 28 Failed to load symbol "is_active" : %s.49c080 15 28 Failed to load symbol "get_osver" : %s.49bedd 14 e DISTRIB_ID=yy49bed1 2 b DISTRIB_ID=
移植
那目的很明确了,将汇编中指向 0x49bef0(/usr/lib/license/libyydevicea.so) 和 0x49bec0(/etc/lsb-release) 的地方,指向我们自定义的字符串。
借助 Keypatch 就好。
我们先在 .eh_frame 上加上自定义字符串。
.eh_frame:000000000022BC78 yy_new_so db '/opt/apps/com.xxxx/misc/libyydevicea.so',0.eh_frame:000000000022BC78 ; DATA XREF: sub_800D0:loc_80189↑o.eh_frame:000000000022BCA6 db 0.eh_frame:000000000022BCA7 db 0.eh_frame:000000000022BCA8 lsb_new db '/opt/apps/com.xxxx/misc/lsb-release',0