首页
论坛
课程
招聘
[原创]某知笔记服务端docker镜像授权分析
2021-6-30 17:32 3185

[原创]某知笔记服务端docker镜像授权分析

2021-6-30 17:32
3185

启动docker镜像

根据其官网 https://www.wiz.cn/zh-cn/docker 说明,使用如下命令即可启动

1
2
3
4
5
docker pull wiznote/wizserver:latest
docker run --name wiz --restart=always -it -d \
   -v `pwd`/wizdata:/wiz/storage \
   -v /etc/localtime:/etc/localtime \
   -p 80:80 -p 9269:9269/udp  wiznote/wizserver

初探授权

  • 启动好之后,默认授权限制只能有5个用户。

    freelicense

  • 进入docker容器,查看基本信息。

    执行 docker exec -it wiz bash 进入容器,可以看到

    • 后端主要使用node实现并用pm2管理,nginx反代,数据库mysql
    • node版本v8.11.2,v8版本6.2.414.54

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      # ps -aef
      UID        PID  CMD
      root         1  bash /wiz/app/entrypoint.sh
      root        33  /usr/bin/redis-server 127.0.0.1:6379
      mysql       53  /usr/sbin/mysqld
      root        59  nginx: master process /usr/sbin/nginx -c /etc/nginx/nginx.conf
      nginx       60  nginx: worker process
      nginx       61  nginx: worker process
      root       119  PM2 v4.5.0: God Daemon (/root/.pm2)
      root       129  node /root/.pm2/modules/pm2-logrotate/node_modules/pm2-logrotate/app.js
      root       137  node /wiz/app/wizserver/app.js
      root       157  node /wiz/app/wizserver/app.js
      root       173  node /wiz/app/wizserver/app.js
      root       189  node /wiz/app/wizserver/app.js
      root       205  node /wiz/app/wizserver/app.js
      root       221  node /wiz/app/wizserver/app.js
      root       240  node /wiz/app/wizserver/app.js
      root       246  crond -n
      root       256  bash
      root       270  ps -aef
      # node -v
      v8.11.2
      # node -p process.versions.v8
      6.2.414.54
      # pm2 list
      ┌─────┬──────────────────┬
      id  │ name             │
      ├─────┼──────────────────┼
      1   │ as               │
      6   │ cloud            │
      4   │ index            │
      2   │ note             │
      7   │ office           │
      5   │ search           │
      3   │ ws               │
      └─────┴──────────────────┴
  • 分析代码发现后缀为jsc的文件

    1
    2
    3
    4
    5
    6
    7
    8
    # ll /wiz/app/wizserver/common/
    total 536
    -rw-r--r-- 1 root root  8792 Dec 23  2020 alicloud_tools.jsc
    -rw-r--r-- 1 root root  9984 Dec 23  2020 client_tools.jsc
    -rw-r--r-- 1 root root  7024 Dec 23  2020 cookie_tools.jsc
    -rw-r--r-- 1 root root  2352 Dec 23  2020 date_tools.jsc
    -rw-r--r-- 1 root root 27528 Dec 23  2020 db_tools.jsc
    ......

    进一步分析发现,这是通过bytenodehttps://github.com/bytenode/bytenode工具编译成了v8的字节码。

    在各种搜索之后发现,目前还没有相关的反汇编、反编译工具。

    唯一相关项目https://github.com/PositiveTechnologies/ghidra_nodejs是GHIDRA的插件,但node版本和我们不匹配

反汇编v8字节码,改造d8

通过v8源码分析发现,v8本身是有反汇编功能,node可通过--print-bytecode参数开启。

 

但只能反汇编源码,无法反汇编jsc文件。为了反汇编字节码只能通过修改v8来实现。

 

d8 https://v8.dev/docs/d8 是v8的开发工具,为了简单起见决定对d8进行修改。

反汇编jsc思路

  • jsc实际是由v8::internal::CodeSerializer::Serialize方法生成
  • 反汇编需要调用v8::internal::BytecodeArray::Disassemble方法生成

反序列v8::internal::CodeSerializer::Deserialize原型

1
2
MUST_USE_RESULT static MaybeHandle<SharedFunctionInfo> Deserialize(
    Isolate* isolate, ScriptData* cached_data, Handle<String> source);

其中参数cached_data,可通过ScriptData的构造函数ScriptData(const byte* data, int length);构造

 

其中返回值SharedFunctionInfo对象有bytecode_array方法,可以获得BytecodeArray来进行反汇编

1
2
3
4
BytecodeArray* SharedFunctionInfo::bytecode_array() const {
  DCHECK(HasBytecodeArray());
  return BytecodeArray::cast(function_data());
}

于是思路自然而然就有了

  • 读取jsc,构造ScriptData对象
  • 反序列化,获取SharedFunctionInfo对象
  • 反汇编,通过bytecode_array获取BytecodeArray,并调用Disassemble反汇编

搭建v8编译环境

1
2
3
4
5
6
7
8
9
10
11
12
git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
export PATH=`pwd`/depot_tools:"$PATH"
export HTTPS_PROXY=http://172.31.0.1:18080  # 设置代理
gclient sync                                # 更新工具链
fetch v8                                    # 获取v8代码
cd v8
git checkout 6.2.414.46                     # 切换至需要的分支
gclient sync                                # 根据分支再次更新工具链
tools/dev/v8gen.py x64.release -- \
  v8_enable_disassembler=true \
  v8_enable_object_print=true               # 配置编译选项
ninja -C out.gn/x64.release d8              # 编译d8

v8_enable_disassemblerv8_enable_object_print一定要开启,否则反汇编时不显示常量内容

修改d8代码(d8.cc)

在Shell类增加LoadJSC方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
static void Disassemble(v8::internal::BytecodeArray* bytecode) {
  internal::OFStream os(stdout);
  bytecode->Disassemble(os);
  auto consts = bytecode->constant_pool();
  for (int i = 0; i < consts->length(); i++) {
    auto obj = consts->get(i);
    if (obj->IsSharedFunctionInfo()) {
      auto shared = v8::internal::SharedFunctionInfo::cast(obj);
      os << "Function name " << shared->name()->ToCString().get() << "\n";
      Disassemble(shared->bytecode_array());
    }
  }
}
void Shell::LoadJSC(const v8::FunctionCallbackInfo<v8::Value>& args) {
  auto isolate = reinterpret_cast<i::Isolate*>(args.GetIsolate());
  for (int i = 0; i < args.Length(); i++) {
    String::Utf8Value filename(args.GetIsolate(), args[i]);
    if (*filename == NULL) {
      Throw(args.GetIsolate(), "Error loading file");
      return;
    }
    int length = 0;
    auto filedata = reinterpret_cast<uint8_t*>(ReadChars(*filename, &length));
    if (filedata == NULL) {
      Throw(args.GetIsolate(), "Error reading file");
      return;
    }
    auto scriptdata = new i::ScriptData(filedata, length);
    auto source = isolate->factory()
                      ->NewStringFromUtf8(i::CStrVector("source"))
                      .ToHandleChecked();
    auto fun = i::CodeSerializer::Deserialize(isolate, scriptdata, source)
                   .ToHandleChecked();
    Disassemble(fun->bytecode_array());
  }
}

注册为全局函数,在Shell::CreateGlobalTemplate中添加代码

1
2
3
4
global_template->Set(
    String::NewFromUtf8(isolate, "loadjsc", NewStringType::kNormal)
        .ToLocalChecked(),
    FunctionTemplate::New(isolate, LoadJSC));

由于目标v8版本为6.2.414.54,但v8的git仓库中并没有这个版本

 

所以用了最接近的版本6.2.414.46,但反序列时一些校验无法通过

  • SerializedCodeData::SanityCheck
  • Deserializer::Initialize

需要修改以上两个方法,代码过于丑陋就不贴了。

 

修改后使用ninja -C out.gn/x64.release d8命令重新编译即可。

使用loadjsc进行反汇编

使用命令v8/out.gn/x64.release/d8 -e "loadjsc('xxx.jsc')" 即可反汇编jsc文件

 

例如源码test.js

1
2
3
4
function xxx(){
    console.log("asdasd")
}
xxx()

其通过wiz的docker里bytenode编译为test.jsc

 

命令 /wiz/app/wizserver/node_modules/.bin/bytenode -c test.js

 

再通过d8反汇编,结果如下:

 

命令 ./d8 -e "loadjsc('test.jsc')"

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
Parameter count 1
Frame size 8
    0 E> 0x17c42dd2c522 @    0 : 91                StackCheck
    0 S> 0x17c42dd2c523 @    1 : 6e 00 00 00       CreateClosure [0], [0], #0
         0x17c42dd2c527 @    5 : 1e fb             Star r0
  115 S> 0x17c42dd2c529 @    7 : 95                Return
Constant pool (size = 1)
0x17c42dd2c531: [FixedArray] in OldSpace
 - map = 0x1b27f0f82309 <Map(HOLEY_ELEMENTS)>
 - length: 1
           0: 0x17c42dd2c549 <SharedFunctionInfo>
Handler Table (size = 16)
Function name
Parameter count 6
Frame size 8
         0x17c42dd2c742 @    0 : 6e 00 00 02       CreateClosure [0], [0], #2
         0x17c42dd2c746 @    4 : 1e fb             Star r0
   10 E> 0x17c42dd2c748 @    6 : 91                StackCheck
  106 S> 0x17c42dd2c749 @    7 : 4f fb 01          CallUndefinedReceiver0 r0, [1]
         0x17c42dd2c74c @   10 : 04                LdaUndefined
  113 S> 0x17c42dd2c74d @   11 : 95                Return
Constant pool (size = 1)
0x17c42dd2c751: [FixedArray] in OldSpace
 - map = 0x1b27f0f82309 <Map(HOLEY_ELEMENTS)>
 - length: 1
           0: 0x17c42dd2c769 <SharedFunctionInfo xxx>
Handler Table (size = 16)
Function name xxx
Parameter count 1
Frame size 24
   74 E> 0x17c42dd2c862 @    0 : 91                StackCheck
   82 S> 0x17c42dd2c863 @    1 : 0a 00 02          LdaGlobal [0], [2]
         0x17c42dd2c866 @    4 : 1e fa             Star r1
   90 E> 0x17c42dd2c868 @    6 : 20 fa 01 04       LdaNamedProperty r1, [1], [4]
         0x17c42dd2c86c @   10 : 1e fb             Star r0
         0x17c42dd2c86e @   12 : 09 02             LdaConstant [2]
         0x17c42dd2c870 @   14 : 1e f9             Star r2
   90 E> 0x17c42dd2c872 @   16 : 4c fb fa f9 00    CallProperty1 r0, r1, r2, [0]
         0x17c42dd2c877 @   21 : 04                LdaUndefined
  104 S> 0x17c42dd2c878 @   22 : 95                Return
Constant pool (size = 3)
0x17c42dd2c881: [FixedArray] in OldSpace
 - map = 0x1b27f0f82309 <Map(HOLEY_ELEMENTS)>
 - length: 3
           0: 0x2e3b60d34a19 <String[7]: console>
           1: 0x2e3b60d08e41 <String[3]: log>
           2: 0x17c42dd2c8e9 <String[6]: asdasd>
Handler Table (size = 16)

分析授权逻辑

前台获取授权信息的api接口

httpapi

 

返回数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
    "returnCode": 200,
    "returnMessage": "OK",
    "externCode": "",
    "result": {
        "start": 1571192332036,
        "end": 4733432332036,
        "count": 5,
        "oem": "wiz",
        "type": "license_free",
        "version": 1,
        "key": "4bc0cc40-efbb-11e9-bef0-0faf68675f7c",
        "ext": {}
    }
}

定位接口对应的文件

根据上述url,http://127.0.0.1/as/admin/licence?clientType=web&clientVersion=4.0&lang=zh-cn

 

结合后端的目录结构,首先对admin_router.jsc文件进行反汇编分析

1
2
3
4
5
6
7
8
9
10
11
12
# pwd
/wiz/app/wizserver/as/admin
# ll
total 128
-rw-r--r-- 1 root root  7408 Dec 23  2020 admin_file_utils.jsc
-rw-r--r-- 1 root root 80720 Dec 23  2020 admin_router.jsc
-rw-r--r-- 1 root root  1680 Dec 23  2020 admin_static_files.jsc
-rw-r--r-- 1 root root  3384 Dec 23  2020 ldap_settings.jsc
drwxr-xr-x 4 root root  4096 Dec 23  2020 meta_files
-rw-r--r-- 1 root root  3304 Dec 23  2020 middleware.jsc
-rw-r--r-- 1 root root 15240 Dec 23  2020 oem_utils.jsc
-rw-r--r-- 1 root root  6304 Dec 23  2020 wizbox_search.jsc

admin_router.jsc

consts

 

consts2

 

consts3

 

search

 

粗略分析,发现admin_router调用了addLicencegetLicence两个方法

 

而同时包含这两个方法的文件是wiz_name_utils.jsc

wiz_name_utils.jsc

consts4

 

pubkey

 

发现wiz_name_utils.jsc应该是调用了node-rsa库,通过RSA算法来计算相关授权

node-rsa

为了验证猜测,通过修改/wiz/app/wizserver/node_modules/node-rsa/src/NodeRSA.js的构造函数,来打印一下公钥

 

NodeRSA

1
2
pm2 restart as  #pm2重启as服务,并访问网页授权页面
tail -f /root/.pm2/logs/as-out.log #查看as日志

log

 

为了进一步验证,我们修改解密函数,打印返回值

 

decrypt

 

再次重启as服务,并访问网页授权页面

1
2
2021-06-29 18:15 +08:00: {"start":1571192332036,"end":4733432332036,"count":5,"oem":"wiz","type":"license_free","version":1,"key":"4bc0cc40-efbb-11e9-bef0-0faf68675f7c","ext":{}}
2021-06-29 18:15 +08:00: e58678339769ba7a9139202655ab345f

解密后的数据与网页接口返回的数据一致,证明猜测位置正确

解除授权封印

通过上面的分析,思路就很清楚了。我们只要在特定的时候修改NodeRSA.prototype.decryptPublic的返回值可以控制授权

1
2
3
4
5
6
7
8
9
10
11
12
13
NodeRSA.prototype.decryptPublic = function (buffer, encoding) {
    var data = this.$$decryptKey(true, buffer, encoding);
    try{
        var v = JSON.parse(data);
        if(v.count == 5){
            v.count = 99999;
            v.type = 'license_vip';
            data = Buffer.from(JSON.stringify(v));
        }
    }catch(e){
    }
    return data;
};

最终结果:

 

crack


第五届安全开发者峰会(SDC 2021)议题征集正式开启!

收藏
点赞4
打赏
分享
最新回复 (5)
雪    币: 15573
活跃值: 活跃值 (18157)
能力值: (RANK:75 )
在线值:
发帖
回帖
粉丝
Editor 活跃值 2021-7-15 09:57
2
0
最终结果后面的内容丢失了哦
雪    币: 802
活跃值: 活跃值 (282)
能力值: ( LV6,RANK:80 )
在线值:
发帖
回帖
粉丝
HOWMP 活跃值 1 2021-7-15 17:39
3
0
Editor 最终结果后面的内容丢失了哦
最终结果就是许可破解了,只有个图啊
雪    币: 279
活跃值: 活跃值 (44)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
Captains 活跃值 2021-7-22 20:39
4
0
这个不错,按照教程修改成功。感谢LZ
雪    币: 1907
活跃值: 活跃值 (882)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
caolinkai 活跃值 2021-7-23 09:17
5
0
这个也能卖199元/用户/年 ???还
雪    币: 200
活跃值: 活跃值 (25)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
萦纡 活跃值 2021-7-23 09:54
6
0
别把某知弄倒闭了,我充了6年VIP呢
游客
登录 | 注册 方可回帖
返回