新手解密:对知乎网页登录加密算法的一次简单分析

对于某乎登录加密算法的分析

最近看到了知乎登录加密的算法, 然后就心血来潮, 分析一下这个加密算法, 看到其他爬虫的方案都是直接执行js文件, 在这里记录一下分析历程.

提取加密代码

这一步我没有自己做, 网上直接找别人提取好的代码, 这篇文章的重点不在提取代码上.

对于这种代码, 首先自动格式化一下, 然后手工优化一下代码结构, 让代码变得稍微好读一些.

看代码, 这个__g._encrypt代码应该是动态添加进去的, 决定还是直接先下断点瞅一眼看看流程.

发现了一个坑

G.prototype.J = function(e, t, n) {
  for (t = t || 0, n = n || [], this.o = t, 'string' == typeof e
    ? (this.F(n), this.v(e))
    : (this.b = e, this.g = n), this.G = true, this.x = Date.now(); this.G;) {
    var r = this.b[this.o++]

    if ('number' != typeof r) {
      break
    }

    var o = Date.now()
    // 这里会比较代码运行时间, 两个时间超过500ms, 程序就直接return
    if (500 < o - this.x) {
      return
    }

    this.x = o

    try {
      this.M(r)
    } catch (e) {
      if (this.R = e, !this.Q) {
        throw 'execption at ' + this.o + ': ' + e
      }
      this.o = this.Q
    }
  }
}

在这里我们直接把这三行代码注释掉, 要不调试的时候就直接return了.

然后我们执行一下加密函数, 在返回之前下断点, 我们可以看到下面的结果

 

我们发现这个_encrypt是一个native code, 利用搜索大法, 我们可以找到

b.prototype.M = function(e) {
  var t = this
  var n = [0]
  e.k.forEach(function(e) {
    n.push(e)
  })

  var r = function(r) {
    var o = new G()
    o.k = n
    o.k[0] = r
    o.J(e.b, t.A, e.g, e.w)
    return o.c[3]
  }
  r.toString = function() {
    return '() { [native code] }'
  }

  e.c[3] = r
}

猜想这个函数(r)可能最终的那个_encrypt, 我们先把那个toString函数给注释掉.

 

这样主体的加密函数我们就找到了.

下面我们来尝试恢复出这个函数怎么添加进去的.

我们首先来看这个函数, 这个函数初始化了一些参数, 首先看到到这些代码天真的我以为是base64, 然鹅发现并不是, 只能继续分析一下代码了.

(new G).
J('4AeTAJwAqACcAaQAAAAYAJAAnAKoAJwDgAWTACwAnAKoACACGAESOTRHkQAkAbAEIAMYAJwFoAASAzREJAQYBBIBNEVkBnCiGAC0BjRAJAAYBBICNEVkBnDGGAC0BzRAJACwCJAAnAmoAJwKoACcC4ABnAyMBRAAMwZgBnESsA0aADRAkQAkABgCnA6gABoCnA+hQDRHGAKcEKAAMQdgBnFasBEaADRAkQAkABgCnBKgABoCnBOhQDRHZAZxkrAUGgA0QJEAJAAYApwVoABgBnG6sBYaADRAkQAkABgCnBegAGAGceKwGBoANECRACQAnAmoAJwZoABgBnIOsBoaADRAkQAkABgCnBugABoCnByhQDRHZAZyRrAdGgA0QJEAJAAQACAFsB4gBhgAnAWgABIBNEEkBxgHEgA0RmQGdJoQCBoFFAE5gCgFFAQ5hDSCJAgYB5AAGACcH4AFGAEaCDRSEP8xDzMQIAkQCBoFFAE5gCgFFAQ5hDSCkQAkCBgBGgg0UhD/MQ+QACAIGAkaBxQBOYGSABoAnB+EBRoIN1AUCDmRNJMkCRAIGgUUATmAKAUUBDmENIKRACQIGAEaCDRSEP8xD5AAIAgYCRoHFAI5gZIAGgCcH4QFGgg3UBQQOZE0kyQJGAMaCRQ/OY+SABoGnCCEBTTAJAMYAxoJFAY5khI/Nk+RABoGnCCEBTTAJAMYAxoJFAw5khI/Nk+RABoGnCCEBTTAJAMYAxoJFBI5khI/Nk+RABoGnCCEBTTAJAMYBxIDNEEkB3JsHgNQAA==',
  0, [
    'BRgg',
    'BSITFQkTERw=',
    'LQYfEhMA',
    'PxMVFBMZKB8DEjQaBQcZExMC',
    '',
    'NhETEQsE',
    'Whg=',
    'Wg==',
    'MhUcHRARDhg=',
    'NBcPBxYeDQMF',
    'Lx4ODys+GhMC',
    'LgM7OwAKDyk6Cg4=',
    'Mx8SGQUvMQ==',
    'SA==',
    'ORoVGCQgERcCAxo=',
    'BTcAERcCAxo=',
    'BRg3ABEXAgMaFAo=',
    'SQ==',
    'OA8LGBsP',
    'GC8LGBsP',
    'Tg==',
    'PxAcBQ==',
    'Tw==',
    'KRsJDgE=',
    'TA==',
    'LQofHg4DBwsP',
    'TQ==',
    'PhMaNCwZAxoUDQUeGQ==',
    'PhMaNCwZAxoUDQUeGTU0GQIeBRsYEQ8=',
    'Qg==',
    'BWpUGxkfGRsZFxkbGR8ZGxkHGRsZHxkbGRcZG1MbGR8ZGxkXGRFpGxkfGRsZFxkbGR8ZGxkHGRsZHxkbGRcZGw==',
    'ORMRCyk0Exk8LQ==',
    'ORMRCyst']
)

我们看G.prototype.J这个函数, 发现在G.prototype.F这个函数里面对于后面那个数组进行了解密.

 

然后在函数G.prototype.v这个函数对于e进行解密, 得到了一个296个元素的数组, 保存在了b里面.

我们可以看这里, 对于数据的加密和处理都是基于这个函数的

try {
  this.M(r)
} catch (e) {
  if (this.R = e, !this.Q) {
    throw 'execption at ' + this.o + ': ' + e
  }
  this.o = this.Q
}

因此我们直接在G.prototype.M里面添加断点, 查看执行流程.

这个函数里面e的值是在数组里面取得的, 先把数组中前几个数拿出来看看.

0 = 57351
1 = 37632
2 = 39936
3 = 43008
4 = 39937
5 = 41984
6 = 0

这里有16个函数, 我们先来看看, 在这里我们先不对这些函数进行展开分析.

this.D = {
    0: i,
    1: h,
    2: A,
    3: n,
    4: e,
    5: a,
    6: c,
    7: o,
    8: r,
    9: k,
    10: B,
    11: f,
    12: u,
    13: C,
    14: b,
    15: g
}

我们先来分析一下function i() {}, 这个函数, 因为在函数G.prototype.J里面循环结束的条件是this.G = true

i.prototype.M = function(e) {
  e.G = !1
}

因此对于(new G).J(e, t, n)这个函数会在执行第七个数结束.

下面我会按照执行出现的顺序对那16个函数进行分析.

function b(e)

第一轮: e = 57351

这个函数完成了r函数的生成.

b.prototype.M = function(e) {
  var t = this, n = [0]
  e.k.forEach(function(e) {
    n.push(e)
  })
  var r = function(r) {
    var o = new G
    o.k = n
    o.k[0] = r
    o.J(e.b, t.A, e.g, e.w)
    return o.c[3]
  }
  // r.toString = function() {
  //   return '() { [native code] }'
  // }
  e.c[3] = r
}

function k(e)

第二轮: e = 37632

this.s = 0
this.i = 3
this.h = 768
this.A = 0
function k(e) {
  this.s = (4095 & e) >> 10
  this.i = (1023 & e) >> 8
  this.h = 1023 & e
  this.A = 63 & e
}

k.prototype.M = function(e) {
  switch (this.s) {
    case 0:
      e.f.push(e.c[this.i])
      break
    case 1:
      e.f.push(this.h)
      break
    case 2:
      e.f.push(e.k[this.A])
      break
    case 3:
      e.f.push(e.g[this.A])
  }
}

在我看来, 那个this.f缓存了需要处理的数据, 这里刚才this.c[3]是之前生成的那个r函数

第三轮: e = 39936

这里执行的还是函数9.

this.s = 3
this.i = 0
this.h = 0
this.A = 0

这里this.f压入了__ge.g就是上面那个解析出来的数组

function B(e)

第四轮: e = 43008

this.s = 2
this.n = 0
this.e = 0
function B(e) {
  this.s = (4095 & e) >> 10
  this.n = (1023 & e) >> 8
  this.e = (255 & e) >> 6
}

B.prototype.M = function(t) {
  switch (this.s) {
    case 0:
      var s = t.f.pop()
      t.c[this.n] = t.c[this.e][s]
      break
    case 1:
      var i = t.f.pop(), h = t.f.pop()
      t.c[this.e][i] = h
      break
    case 2:
      var A = t.f.pop()
      if (A === 'window') {
        A = {
          encodeURIComponent: function(url) {
            return encodeURIComponent(url)
          }
        }
      } else if (A === 'navigator') {
        A = {
          'userAgent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' +
            '(KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36'
        }
      }
      t.c[this.n] = eval(A)
  }
}

在上文里面f.pop()__g, 在执行完之后, G.c[0] = eval('__g') = {}

第五轮: e = 39937

在这里执行的函数是9

this.s = 3
this.i = 0
this.h = 1
this.A = 1

这里在this.f里面里面又压进去了_encrypt

第六轮: e = 41984

在这里执行函数10

this.s = 1
this.n = 0
this.e = 0

这里执行函数B.prototype.M中的1分支, 我们可以看到这里
i = _encrypth = r 之讲到的那个加密函数.

这里t = G, 因此我们可以看到t[__g]['_encrypt'] = r, 着这里加密函数就完全现身了.

function i()

第七轮: e = 0

function i() {}

i.prototype.M = function(e) {
  e.G = !1
}

这里给e.G赋值了一个false, 循环结束.

整个加密函数初始化结束, 接下来我们分析实际的加密算法.

之前我们说到加密函数实际上就是r那个函数, 因此我们直接在r上面下断点.

var r = function(r) {
    var o = new G
    o.k = n
    o.k[0] = r
    o.J(e.b, t.A, e.g, e.w)
    return o.c[3]
}

这里参数r是明文, e.b 是之前那个数组, e.g是那个函数数组, 最终返回密文G.c[3]

我们以加密字符串"a"为例, 来对加密算法进行分析.

作者注: 这里对于函数的分析有点多余, 可以先跳过.

function h(e)

第八轮: e = 6144

this.s = 1
this.i = 0
this.h = 0
this.A = 0
function h(e) {
  this.s = (2048 & e) >> 11
  this.i = (1536 & e) >> 9
  this.h = 511 & e
  this.A = 511 & e
}

h.prototype.M = function(e) {
  switch (this.s) {
    case 0:
      e.c[this.i] = this.h
      break
    case 1:
      e.c[this.i] = e.k[this.A]
  }
}

这个函数比较简单, 是对于G.c[this.i]进行赋值, 我们可以看到, 在第八轮里面我们把G.c[0] = G.k[0], 实际上这个操作将明文存到了G.c[0]中.

第九轮:

这里讲明文压入this.f

第十轮:

这里将"window"压入this.f

第十一轮:

赋值G.c[0] = { encodeURIComponent: function(url) { return encodeURIComponent(url) }}

下面我们依次对这些剩余函数进行分析

function A(e)

function A(e) {
  this.i = (3072 & e) >> 10
  this.A = 1023 & e
}

A.prototype.M = function(e) {
  e.k[this.A] = e.c[this.i]
}

从上面来分析, 我们可以看到this.i的取值只可能为0, 1, 2, 3, 后面函数就是一个拷贝赋值.

function n(e)

这个函数式一个比较长的函数, 但其实分析起来也并不是太复杂, 我们可以知道

3072 -> 0b110000000000
768  -> 0b001100000000
192  -> 0b000011000000

从上面那个对应关系, 我们很容易的看出n, e, a应该都只能取0, 1, 2, 3, 上面那个函数同理, 在这里一块解释一下了, 看到下面的函数, 我们可以知道this.s应该只能取到19.

这个函数的作用也很简单, 就是对e.c[this.e] (e.c[this.a]) 进行某些操作(运算操作) 并赋值给e.c[this.n]

function n(e) {
  this.n = (3072 & e) >> 10
  this.e = (768 & e) >> 8
  this.a = (192 & e) >> 6
  this.s = 63 & e
}

n.prototype.M = function(e) {
  switch (this.s) {
    case 0:
      e.c[this.n] = e.c[this.e] + e.c[this.a]
      break
    case 1:
      e.c[this.n] = e.c[this.e] - e.c[this.a]
      break
    case 2:
      e.c[this.n] = e.c[this.e] * e.c[this.a]
      break
    case 3:
      e.c[this.n] = e.c[this.e] / e.c[this.a]
      break
    case 4:
      e.c[this.n] = e.c[this.e] % e.c[this.a]
      break
    case 5:
      e.c[this.n] = e.c[this.e] == e.c[this.a]
      break
    case 6:
      e.c[this.n] = e.c[this.e] >= e.c[this.a]
      break
    case 7:
      e.c[this.n] = e.c[this.e] || e.c[this.a]
      break
    case 8:
      e.c[this.n] = e.c[this.e] && e.c[this.a]
      break
    case 9:
      e.c[this.n] = e.c[this.e] !== e.c[this.a]
      break
    case 10:
      e.c[this.n] = s(e.c[this.e])
      break
    case 11:
      e.c[this.n] = e.c[this.e] in e.c[this.a]
      break
    case 12:
      e.c[this.n] = e.c[this.e] > e.c[this.a]
      break
    case 13:
      e.c[this.n] = -e.c[this.e]
      break
    case 14:
      e.c[this.n] = e.c[this.e] < e.c[this.a]
      break
    case 15:
      e.c[this.n] = e.c[this.e] & e.c[this.a]
      break
    case 16:
      e.c[this.n] = e.c[this.e] ^ e.c[this.a]
      break
    case 17:
      e.c[this.n] = e.c[this.e] << e.c[this.a]
      break
    case 18:
      e.c[this.n] = e.c[this.e] >>> e.c[this.a]
      break
    case 19:
      e.c[this.n] = e.c[this.e] | e.c[this.a]
  }
}

我们顺便来解释一下函数s()

function s(e) {
  return (s = 'function' == typeof Symbol && 'symbol' == typeof Symbol.t ? function(e) {
    return typeof e
  } : function(e) {
    return e && 'function' == typeof Symbol && e.constructor === Symbol && e !== Symbol.prototype ? 'symbol' : typeof e
  })(e)
}

其实这个函数就是返回参数e的类型, 但是我暂时没发现这个函数进去过, 哈哈哈

4: function e(e)

实话说, 我也没发现这个函数啥时候调用过, 空指令吧. 为了防止他真的使用过, 我们对于函数G.prototype.M的调用来源进行分析, 发现他的所有的e来自哪个密钥数组, 那python简单写一下查看到

// 这里secretKey就是那个密钥数组
print(set([(61440 & i) >> 12 for i in secretKey]))
>>> {0, 1, 2, 3, 5, 6, 7, 8, 9, 10, 11, 14}

显然他没4这个函数, 我们在这里也就不分析了.

function e(e) {
  this.i = e >> 10 & 3
  this.h = 1023 & e
}

e.prototype.M = function(e) {
  e.r.push(e.o), e.B.push(e.k), e.o = e.c[this.i], e.k = []
  for (var t = 0; t < this.h; t++) e.k.unshift(e.f.pop())
  e.u.push(e.f), e.f = []
}

5: function a()

function a() {}

a.prototype.M = function(e) {
  e.o = e.r.pop()
  e.k = e.B.pop()
  e.f = e.u.pop()
}

这个函数是对于e.o, e.k, e.f进行赋值

6: function c(e)

这个函数和之前那个n比较类似, 但这个的最终结果是对于e.C进行赋值, 这里得到的是一个bool值.

function c(e) {
  this.n = (3072 & e) >> 10
  this.e = (768 & e) >> 8
  this.a = (192 & e) >> 6
  this.s = 63 & e
}

c.prototype.M = function(e) {
  switch (this.s) {
    case 0:
      e.C = e.c[this.n] >= e.c[this.e]
      break
    case 1:
      e.C = e.c[this.n] <= e.c[this.e]
      break
    case 2:
      e.C = e.c[this.n] > e.c[this.e]
      break
    case 3:
      e.C = e.c[this.n] < e.c[this.e]
      break
    case 4:
      e.C = e.c[this.n] == e.c[this.e]
      break
    case 5:
      e.C = e.c[this.n] != e.c[this.e]
      break
    case 6:
      e.C = e.c[this.n]
      break
    case 7:
      e.C = !e.c[this.n]
  }
}

7: function o(e)

这个函数的作用是根据e.C的值对e.o进行赋值, e.o是用来控制加密进度的, 我对代码进行了一次化简, 变得更加易读.

function o(e) {
  this.A = (4095 & e) >> 2
  this.s = 3 & e
}

o.prototype.M = function(e) {
  switch (this.s) {
    case 0:
      e.o = this.A
      break

    case 1:
      if (e.C) {
        e.o = this.A
      }
      break

    case 2:
      if (!e.C) {
        e.o = this.A
      }
      break

    case 3:
      e.o = this.A
      e.Q = null
  }

  e.C = false
}

8: function r(e)

同样先对代码进行一下美化, 这样看起来就比价舒服了, 虽然这个函数在列表里面, 但是我还是没遇到过.

这里this.i依然只能取0, 1, 2, 3

function r(e) {
  this.i = e >> 10 & 3
  this.h = e >> 2 & 255
  this.s = 3 & e
}

r.prototype.M = function(e) {
  switch (this.s) {
    case 0:
      var t = []

      for (var n = 0; n < this.h; n++) {
        t.unshift(e.f.pop())
      }

      e.c[3] = e.c[this.i](t[0], t[1])
      break

    case 1:
      var r = e.f.pop(), o = []

      for (var i = 0; i < this.h; i++) {
        o.unshift(e.f.pop())
      }

      e.c[3] = e.c[this.i][r](o[0], o[1])
      break

    case 2:
      var a = []

      for (var c = 0; c < this.h; c++) {
        a.unshift(e.f.pop())
      }

      e.c[3] = new e.c[this.i](a[0], a[1])
  }
}

11: function f(e)

这个函数是一个简单的赋值操作, 将操作g中的字符复制到e.c[this.i]

function f(e) {
  this.i = (3072 & e) >> 10
  this.A = 1023 & e
}

f.prototype.M = function(e) {
  e.c[this.i] = e.g[this.A]
}

以上所有的函数就基本解释完了, 下面分析一下他的具体加密流程.

为了便于分析流程, 我们对于密钥e.g进行一下分析处理.

从上到下我分析了这么多, 我感觉我之前的思路可能错了, 因此我决定换一个思路进行调试分析, 不再逐步分析代码了, 因为这样轮数太多了, 而且有很多无关的运算在里面, 因此我们直接去下断点调试.

首先我们来寻找程序的切入点, 找到具体加密在哪里执行的变换, 观察那16个函数, 我们发现这里(r.prototype.M)存在函数执行的痕迹.

r.prototype.M = function(e) {
  switch (this.s) {
    case 0:
      for (var t = [], n = 0; n < this.h; n++) t.unshift(e.f.pop())
      e.c[3] = e.c[this.i](t[0], t[1])
      break
    case 1:
      for (var r = e.f.pop(), o = [], i = 0; i < this.h; i++) o.unshift(e.f.pop())
      e.c[3] = e.c[this.i][r](o[0], o[1])
      break
    case 2:
      for (var a = [], c = 0; c < this.h; c++) a.unshift(e.f.pop())
      e.c[3] = new e.c[this.i](a[0], a[1])
  }
}

因此我们在这里添加断点来进一步验证这个猜测, 通过对程序运行的分析, 我们发现主要执行的是1分支, 直接在执行之前添加断点.

 

第一次到这里, 我们发现这个是对加密内容进行了一次url编码, 从上面函数分析"第十一轮"来看, 上面这一轮写入的函数是为了这个而服务的.

 

这一步是把userAgent全给改成了小写, 这一步我暂时没发现对于加密算法的影响.

然后接着向下调试, 下面两个函数一起分析


 

这两个函数做了对于字符串加密的变换, 从原始字符的ascii码值做一系列变换之后映射到"_-abcdefghijklmnopqrstuvwxyz1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ" 这64个字符上.

找到这里, 我终于算是找到了一个突破点, 接着分析.

我们接下来看n.prototype.M, 这里面的运算操作承接了对于加密的变换和字符串的拼接.

我们在运算上面分别下断点进行调试.

n.prototype.M = function(e) {
  switch (this.s) {
    case 0:
      e.c[this.n] = e.c[this.e] + e.c[this.a]
      break
    case 1:
      e.c[this.n] = e.c[this.e] - e.c[this.a]
      break
    case 2:
      e.c[this.n] = e.c[this.e] * e.c[this.a]
      break
    case 3:
      e.c[this.n] = e.c[this.e] / e.c[this.a]
      break
    case 4:
      e.c[this.n] = e.c[this.e] % e.c[this.a]
      break
    case 5:
      e.c[this.n] = e.c[this.e] == e.c[this.a]
      break
    case 6:
      e.c[this.n] = e.c[this.e] >= e.c[this.a]
      break
    case 7:
      e.c[this.n] = e.c[this.e] || e.c[this.a]
      break
    case 8:
      e.c[this.n] = e.c[this.e] && e.c[this.a]
      break
    case 9:
      e.c[this.n] = e.c[this.e] !== e.c[this.a]
      break
    case 10:
      e.c[this.n] = s(e.c[this.e])
      break
    case 11:
      e.c[this.n] = e.c[this.e] in e.c[this.a]
      break
    case 12:
      e.c[this.n] = e.c[this.e] > e.c[this.a]
      break
    case 13:
      e.c[this.n] = -e.c[this.e]
      break
    case 14:
      e.c[this.n] = e.c[this.e] < e.c[this.a]
      break
    case 15:
      e.c[this.n] = e.c[this.e] & e.c[this.a]
      break
    case 16:
      e.c[this.n] = e.c[this.e] ^ e.c[this.a]
      break
    case 17:
      e.c[this.n] = e.c[this.e] << e.c[this.a]
      break
    case 18:
      e.c[this.n] = e.c[this.e] >>> e.c[this.a]
      break
    case 19:
      e.c[this.n] = e.c[this.e] | e.c[this.a]
  }
}

[/s] 

[s]

 

然后我们可以发现他这个是三个字符一组进行加密的, 然后不足三个字符的后面会padding\u0000

下面是整个加密算法的核心.

我们修改部分代码便于下断点进行调试.

r.prototype.M = function(e) {
    // ...
    case 1:
      // tmp.push(e.o)
      for (var r = e.f.pop(), o = [], i = 0; i < this.h; i++) o.unshift(e.f.pop())
      if (r === 'charAt' && count > 0) {
        debugger
      }
      if (r === 'charCodeAt' && count > 0) {
        debugger
      }
      e.c[3] = e.c[this.i][r](o[0], o[1])
      break
    // ...
}

因为之前说过这两个函数是对于原始字符的加密处理的关键, 所以我们直接在这里添加断点, 后面的count是为了方便分析不同的加密轮次.

具体调试过程我就不在这里过多描述了, 我采用的单步调试, 这里步数太多了, 要有耐心.

一轮大约调试的次数, 在这个算法里面是通过G.o控制的, 因此我记录了一轮的G.o

['156', '157', '158', '159', '160', '161', '162', '163', '164', '165', '166', '167', '168', '169', '170', '171', '172', '173', '174', '175', '176', '177', '178', '179', '180', '181', '182', '183', '184', '185', '186', '187', '188', '189', '190', '191', '192', '193', '194', '195', '196', '197', '198', '199', '200', '201', '202', '203', '204', '205', '206', '207', '208', '209', '210', '211', '212', '213', '214', '215', '216', '217', '218', '219', '220', '221', '222', '223', '224', '225', '226', '227', '228', '229', '230', '231', '232', '233', '234', '235', '236', '237', '238', '239', '240', '241', '242', '243', '244', '245', '246', '247', '248', '249', '250', '251', '252', '253', '254', '255', '256', '257', '258', '259', '260', '261', '262', '263', '264', '265', '266', '267', '268', '269', '270', '271', '272', '273', '274', '275', '276', '277', '278', '279', '280', '281', '282', '283', '284', '285', '286', '287', '288', '289', '290', '291', '292', '293', '294', '156', '157', '158', '159', '160', '295', '296']

通过对于参数的控制, 可以分析得知他是4轮一组, 具体算法如下:

加密算法

先来附一张流程图.

 

function encrypt(s) {
  const table = '_-abcdefghijklmnopqrstuvwxyz1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ'
  s += '\u0000'.repeat(s.length % 3)
  let result = ''
  for (let i = s.length - 1 - 2, j = 0; i > 0; i-=3) {
    const c0 = s.charCodeAt(i + 2)
    const c1 = s.charCodeAt(i + 1)
    const c2 = s.charCodeAt(i)
    const flag = 57 << (8 * (j++ % 4))
    const tmp = ((flag ^ c0) ^ (c1 << 8)) ^ (c2 << 16)
    result += `${ table.charAt(tmp & 63) }${ table.charAt(tmp >> 6 & 63) }${ table.charAt(tmp >> 12 & 63) }${ table.charAt(tmp >> 18 & 63) }`
  }
  return result
}

解密算法

通过对上述加密算法进行分析, 我们得到了他的解密算法.

[/s] 

[s]

 

function decrypt(s) {
  const table = '_-abcdefghijklmnopqrstuvwxyz1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ'
  let result = []
  for (let i = 0, j = 0; i < s.length; i++) {
    if (i % 4 === 0) {
      const b3 = table.indexOf(s[i+3])
      const b2 = table.indexOf(s[i+2])
      const b1 = table.indexOf(s[i+1])
      const b0 = table.indexOf(s[i])

      const tmp = b3 << 18 | b2 << 12 | b1 << 6 | b0

      let c0=0, c1=0, c2=0

      if (j % 4 === 0) {
        c0 = (tmp & 0x0000ff)  ^  57
        c1 = (tmp & 0x00ff00)  >> 8
        c2 = (tmp & 0xff0000) >> 16
      } else if (j % 4 === 1) {
        c0 = (tmp & 0x0000ff)
        c1 = ((tmp & 0x00ff00) ^ (57 << 8)) >> 8
        c2 = (tmp & 0xff0000) >> 16
      } else if (j % 4 === 2) {
        c0 = (tmp & 0x0000ff)
        c1 = (tmp & 0x00ff00)  >> 8
        c2 = ((tmp & 0xff0000) ^ (57 << 16)) >> 16
      } else if (j % 4 === 3) {
        c0 = (tmp & 0x0000ff)
        c1 = (tmp & 0x00ff00)  >> 8
        c2 = (tmp & 0xff0000) >> 16
      }

      j++

      result.push(`${String.fromCharCode(c2)}${String.fromCharCode(c1)}${String.fromCharCode(c0)}`)
    }
  }
  return result.reverse().join('')
}

总结

通过对于这个加密算法的分析, 我感觉直接分析混淆后的js代码有时候难度比较大, 而且容易被带偏思路, 通过对于关键函数下断点, 从而找到突破口进行分析, 有时候会比较有效.

本教程仅可作为研究学习为目的, 请勿用作其他用途.

版权保护: 本文由吾爱破解所发布,转载请保留本文链接: http://www.yemogege.cn/nxxg-jswz/469.html

免责声明:蓝域安全网所发布的一切渗透技术视频文章,逆向脱壳病毒分析教程,以及相关实用源码仅限用于学习和研究目的
请勿用于商业用途。如有侵权、不妥之处,请联系站长并出示版权证明以便删除! 

侵权删帖/违法举报/投稿等事物联系邮箱:yemogege@vip.qq.com 网站地图