保姆级某红薯X-s、X-S-Common协议逆向

注:此文章是基于 2024年6月12号的某红薯网页版进行分析的。此前没发现可以使用MarkDown编写,使用的默认的,所以代码有点混乱,将就的看吧

1. 前言

· 其实刚开始我也不知道jsvmp是什么,之前也没接触过jsvmp之类的东西,某红薯算是我第一个接触的项目。

· 我以前是做Windows逆向的,接触的是俄罗斯佬弄的VMProtect,像PC上的微信、企业微信等一些软件都经过VMProtect的加密,我的认知都是来源于这些,在Windows上的VMProtect有几种模式:

代码膨胀:一条汇编,用N多多条汇编代替实现,中间塞入很多毫无意义的汇编代码

- 虚拟化(真正的vmp):把Intel处理器能识别运行的机器码转成另一种字节码,然后用一个专有的虚拟机读取这些字节码进行解析执行

· 搜索了一些相关由于文章,已经有人逆向出源码了,所以直接上手逆向源码把,省略扣代码,补环境的过程(ps:我怎么可能告诉你,我不擅长扣代码呢)

2. 前期分析

由于网上版本都是不写时间的,所以以防万一,还是从头开始看协议是否有变化。刚开始的时候,我是登录以后,在发文章那里开始逆的,后来发现,其实停留在登录二维码扫码页面也会不断地被调用,所以下面就停留在扫码页面吧。

打开某红薯首页,点开登录页面如下确认:


确认是否会生成X-S

如上图,可以正常看到X-S请求参数,那么我们继续,去找关键代码,赋值X-S的地方,搜索:["X-S"]。

找到赋值X-S的地方

接着双击搜索到的X-S赋值地方,如果有多个,可以一个个的分析,是否是属于算法出来以后赋值的地方,复杂的可能需要下断点从Call Stack往回找,庆幸某红薯直接算法出来就赋值了

找到关键代码

找到如上代码以后,已经离成功很近了,接着在 var c = getRealUrl(r, n, o) 开始的三行都下个断点把,等断下来,

命中断点

好了,现在代码已经断在外面下的第一行断点上了,此时按 F10(跳过函数细节单步),执行到下一条代码,此时代码为 

, l = (a && void 0 !== window._webmsxyw ? window._webmsxyw : encrypt_sign)(c, i) || {};

看代码,其实我们并不清楚解析去是执行 window._webmsxyw 还是 encrypt_sign (我们Network都看到X-S有值了,所以不可能直接返回最后的 {} 的)。此时,可以鼠标移上 window._webmsxyw 看他是否有值,或者在Console输入 window._webmsxyw 看是否为null,

查看函数

可以看到,不管是 window._webmsxyw 还是 encrypt_sign 都是一个函数,根据上面的代码如果 window._webmsxyw 不为空,那么就执行 

l = window._webmsxyw(c, i);

而,window._webmsxyw 指向的函数为(双击一下Console 输出就可以调到对应函数,如果不清楚双击哪里,那么按 F11 (步入函数的单步))  _ace_1ae3c(function() { ...

定位jsvmp

此时,我们就在x-s算法核心部分了。

3. 分析jsvmp中X-S生成算法

此时,这整个js由于是jsvmp处理的,就不要尝试单步去跟踪了,除非你想逆jsvmp的vm实现。

解析去,可以稍微看下,整个文件的代码结构:

jsvmp虚拟机

然后,再上下翻一翻vm的代码,可以看到很有规律的各种运算占大部分:

jsvmp执行字节码运算

粗略看完,可以知道 _ace_34d1 是取操作数,_ace_1ae3c 是执行操作,我们知道各种算法代码无非是对数据进行 加减乘除与或非抑或左移右移、表替换 这些运算,那么我们把所有的这些操作都打印出来不就好了 (#^.^#),不知道是哪个小天才想出来的好方法。

接下去在当前文件中搜索 _ace_1ae3c(_ace_34d1(p0, p1), 在相关的算法上面进行下日志断点把,方法如下:


日志断点

我下断点的几个方法(我并没有对==, && ,!=,>=,<=,instanceof,in 等做日志处理):

_ace_1ae3c(_ace_34d1(p0, p1) / _ace_34d1(p2, p3), _ace_be07c, _ace_be07c, 0);

_ace_1ae3c(_ace_34d1(p0, p1) & _ace_34d1(p2, p3), _ace_be07c, _ace_be07c, 0);

_ace_1ae3c(_ace_34d1(p0, p1) >>> _ace_34d1(p2, p3), _ace_be07c, _ace_be07c, 0);

_ace_1ae3c(_ace_34d1(p0, p1) % _ace_34d1(p2, p3), _ace_be07c, _ace_be07c, 0);

_ace_1ae3c(_ace_34d1(p0, p1) + _ace_34d1(p2, p3), _ace_be07c, _ace_be07c, 0);

_ace_1ae3c(_ace_34d1(p0, p1) * _ace_34d1(p2, p3), _ace_be07c, _ace_be07c, 0);

_ace_1ae3c(_ace_34d1(p0, p1) << _ace_34d1(p2, p3), _ace_be07c, _ace_be07c, 0);

_ace_1ae3c(_ace_34d1(p0, p1) - _ace_34d1(p2, p3), _ace_be07c, _ace_be07c, 0);

_ace_1ae3c(_ace_34d1(p0, p1) | _ace_34d1(p2, p3), _ace_be07c, _ace_be07c, 0);

_ace_1ae3c(_ace_34d1(p0, p1) >> _ace_34d1(p2, p3), _ace_be07c, _ace_be07c, 0);

_ace_1ae3c(_ace_34d1(p0, p1) ^ _ace_34d1(p2, p3), _ace_be07c, _ace_be07c, 0);

日志(把里面的?替换成对应的运算符即可):

"运算:参数1:", _ace_34d1(p0, p1), " 参数2:", _ace_34d1(p2, p3), " => ", _ace_34d1(p0, p1) _ace_34d1(p2, p3)

下完所有断点以后,先清空下Console,然后点击运行,等待运行结束,经过漫长的等待,浏览器会断到 e.headers["X-t"] = l["X-t"], 这一行 

打日志

等待断在  e.headers["X-t"] = l["X-t"] ,可以把Console的输出日志保存一份,过程有些卡顿,耐心等一下即可

保存日志

用notepad++或者VSCode打开,或你任意喜欢的编辑器都可以,我们拉到末尾看下:

日志末尾

是的,你没看错,日志有19万行,接下去就是苦逼的分析并还原工作了,不过这有些技巧,比如看到 eyJzaWduU3Z............... 先不要急着分析,这种一看就是Base64,这个转码工具或者网站进行转码,这里可以使用这个 Base64在线编码解码 。

base64

看到这里,是不是很开心啊,直接用 {"signSvn 到日志里面搜索就可以了:

signSvn生成

可以看到json是一个个字符拼接起来的(说明是一个个解码出来,大概率是前面就已经存在的),其中 signSvn、signType、

appId、signVersion都很好理解,但 payload 一大串十六进制,是哪里来的呢,直接拿出来搜索一下,发现是一个个byte转出来凑上去的,

payload

再往上翻一点,有一片混乱编码,目前还不知道是什么,然后前面就是各种算法了

乱码

先别急着分析,我们猜测下,这个是什么,会不会是这个乱码转成的上面的十六进制字符串呢(其实就是),还记得我们的 payload 最后的字节码吗?,不记得翻上去看看:.....cf233254dee6443698d326051c0c8e3b760f3359

有没有发现,就是这里生成的呀?因为这里显示的是十进制,所以你看不出来,我标记下你就知道了 

payload的字节

再看生成基本是右移8、16、24得来的,是不是32位取字节算法?我们直接拿个 1980707673 转成 十六进制看看,0x760f3359,是不是就是我们找到payload末尾呀?既然都是从32位出来的,那么我们拿最开始的32位出来找:0xf0170120,记得转成十进制搜索,4028039456,又因为日志里面有符号数,(取反+1: ( 0xFFFFFFFF ^ 0xf0170120 ) + 1 ),那么应该是搜索 -266927840  

payload计算

好了,记住这里的行数 146251,从这里往上找 ,看到charCodeAt 就是一个循环的算法了

等等,先别急着去逆算法,再看看这个参数是哪里来的(为了方便看,把中间charCodeAt的拼接算法删除了):

算法数据来源

由于这些值都是出于一个32位数,所以先把前面的 0x65、0x44、0x45、0x39 组合起来看看,0x65444539,放入二进制文件看看?

数据

是不是又有新的惊喜啦,拿这个字符串去搜索一下吧,"eDE9YWFi"

搜索结果

看着是不是还是像base64呀,所以,再解码看看

base64解码结果

啊,这,大部分参数不就来了么,x3是内部版本号( 是Cookie里面的a1的值,可以使用 js_cookie.A.get(LOCAL_ID_KEY)  获取),x2固定值,x4时间搓,x1 这个是url的md5。

那,那,现在只需要逆向 145641 ~ 146239 的代码不就可以了吗 (#^.^#),

保姆级教程到此结束,剩下这部分逆向就交给各位啦,提示逆向可参考 js实现DES算法,这篇文章我验证过,算法大部分相似,稍微改动即可,各位爬虫er,干巴爹~~


4. 参考的文章:

某红薯 [X-s] [X-t] 参数逆向分享

某红薯 x-s 最新 JS逆向 jsvmp



5. 关于X-S-Common的算法逆向

这个X-S-Common应该是今年新加的,相对于上面的那个啥jsvmp,这个X-S-Common就弄起来很简单了。

首先,起步就是搜索 ["X-S-Common"],可以发现只有一个哦

找到关键代码


找到关键代码:

e.headers["X-S-Common"] = encrypt_b64Encode(encrypt_encodeUtf8(stringify_default()(h)))

可以在Console里面先运行一下,各个函数的功能,比如查看 h 值,虽然上面就是 h 的赋值过程

数据

可以看到 stringify_default、encrypt_encodeUtf8 都不需要分析,我们看下 encrypt_b64Encode 出来的是不是base64编码呀


尝试base64解码

很遗憾,对比发现,并不是base64,那么怎么办呢? 先进入 encrypt_b64Encode 函数看看,发现正常函数,并没有jsvmp加密

function encrypt_b64Encode(t) {

    var e = 664

        , r = 634

        , n = 448

        , o = 599

        , i = 315

        , a = 416

        , u = 512

        , s = 361

        , c = 406

        , l = 487

        , f = 496

        , p = 333

        , h = 630

        , d = 639

        , v = 548

        , g = 582

        , m = 447

        , y = 468

        , w = 375

        , b = 331

        , _ = 149

        , E = 382

        , x = 265

        , k = 625

        , T = 570

        , S = 551

        , A = 582

        , L = 581

        , R = 638

        , I = 618

        , O = 606

        , C = 429

        , N = 651

        , P = 667

        , B = 817

        , M = 333

        , j = 567

        , F = 747

        , D = 561

        , q = 570

        , U = 676

        , G = 840

        , H = 240

        , V = {

        udFrB: function(t, e) {

            return t % e

        },

        cCZFe: function(t, e) {

            return t === e

        },

        jevwl: function(t, e) {

            return t - e

        },

        aqlTy: function(t, e) {

            return t + e

        },

        rceYY: function(t, e) {

            return t >> e

        },

        OwjMq: function(t, e) {

            return t & e

        },

        kSGXO: function(t, e) {

            return t << e

        },

        veNiI: function(t, e) {

            return t === e

        },

        QLthP: function(t, e) {

            return t + e

        },

        wDtJz: function(t, e) {

            return t + e

        },

        nYqUQ: function(t, e) {

            return t & e

        },

        TCArD: function(t, e) {

            return t << e

        },

        RHteb: function(t, e) {

            return t - e

        },

        mZPJZ: function(t, e) {

            return t < e

        },

        zDETq: function(t, e, r, n) {

            return t(e, r, n)

        },

        YlZGp: function(t, e) {

            return t > e

        }

    };

    function W(t, e) {

        return a0_0x10f4ac(e, t - -H)

    }

    for (var X = (W(-413, -442) + W(-e, -r) + "7")[W(-n, -o)]("|"), z = 0; ; ) {

        switch (X[z++]) {

        case "0":

            var Y;

            continue;

        case "1":

            var K = [];

            continue;

        case "2":

            var J = V[W(-i, -a)]($, 3);

            continue;

        case "3":

            var $ = t[W(-350, -u)];

            continue;

        case "4":

            V[W(-s, -c)](J, 1) ? (Y = t[V[W(-l, -f)]($, 1)],

            K[W(-p, -346)](V[W(-h, -d)](encrypt_lookup[V[W(-503, -v)](Y, 2)] + encrypt_lookup[V[W(-g, -741)](V[W(-331, -m)](Y, 4), 63)], "=="))) : V[W(-y, -w)](J, 2) && (Y = V[W(-b, -_)](t[$ - 2], 8) + t[V[W(-l, -E)]($, 1)],

            K[W(-333, -x)](V[W(-k, -505)](V[W(-T, -S)](encrypt_lookup[Y >> 10], encrypt_lookup[V[W(-A, -L)](Y >> 4, 63)]) + encrypt_lookup[V[W(-R, -I)](V[W(-O, -C)](Y, 2), 63)], "=")));

            continue;

        case "5":

            var Q = 16383;

            continue;

        case "6":

            for (var Z = 0, tt = V[W(-509, -N)]($, J); V[W(-P, -B)](Z, tt); Z += Q)

                K[W(-M, -153)](V[W(-j, -F)](encrypt_encodeChunk, t, Z, V[W(-D, -413)](Z + Q, tt) ? tt : V[W(-q, -501)](Z, Q)));

            continue;

        case "7":

            return K[W(-U, -G)]("")

        }

        break

    }

}

看着没有加密,要不,扣代码试试?但是扣完发现有个 esm_typeof.A 报错,我解决不了,有知道这个怎么解决的可以告诉我吗?这里,还是继续分析逆向吧,

函数对应

从上面分析可以知道,W(-s, -c) 结果是 'cCZFe' ,对应的是上面的

cCZFe: function(t, e) {

       return t === e

},

所以,这个代码可以精简成  t === e,在Console已经验证是一致的。所以可以把for循环整个解密替换以后

for (var X = '0|3|2|1|5|6|4|7'.split("|"), z = 0; ; ) {

    switch (X[z++]) {

    case "0":

        var Y;

        continue;

    case "1":

        var K = [];

        continue;

    case "2":

        var J = $%3;

        continue;

    case "3":

        var $ = t.length;

        continue;

    case "4":

        (J === 1) ? (Y = t[$ - 1], K.push((encrypt_lookup[(Y >> 2)] + encrypt_lookup[((Y << 4) & 63)]) + "==")) :

        (J === 2) &&

        (Y = (t[$ - 2] << 8) + t[($ - 1)], K.push((encrypt_lookup[Y >> 10] + encrypt_lookup[((Y >> 4) & 63)]) + encrypt_lookup[((Y << 2) & 63)] + "="));

        continue;

    case "5":

        var Q = 16383;

        continue;

    case "6":

        for (var Z = 0, tt = ($ - J); (Z < tt); Z += Q)

            K.push(encrypt_encodeChunk(t, Z, ((Z + Q) > tt) ? tt : (Z + Q)));

        continue;

    case "7":

        return K.join("")

    }

    break

}

代码细细的看,可以发现主要功能是 encrypt_encodeChunk(t, Z, ((Z + Q) > tt) ? tt : (Z + Q)) ,以 0x3fff (16383)块大小进行加密。接下去同理分析 encrypt_encodeChunk 方法,精简如下:

function encrypt_encodeChunk(t, e, r) {

    for (var _ = e; _ < r; _ += 3)

        n = ((t[_] << 16) & 16711680) + ((t[_ + 1]<< 8) & 65280) + (t[(_ + 2)] & 255),

        w.push(encrypt_tripletToBase64(n));

    return w[b(-v, -g)]("")

}

这里代码看一下,把输入进来的字节码,三个三个转成一个数值以后传入 encrypt_tripletToBase64 加密再组合起来,验证这些可以在Console里面调用看看,是不是分析的一致。

接着打开 encrypt_tripletToBase64 分析(需要自己精简一下)如下:

function encrypt_tripletToBase64(t) {

    var y = g;

    return y[m(s, c)](encrypt_lookup[63 & y[m(l, -75)](t, 18)], encrypt_lookup[y[m(r, f)](y[m(p, h)](t, 12), 63)]) + encrypt_lookup[t >> 6 & 63] + encrypt_lookup[y[m(-a, -d)](t, 63)]

}

看到这里,豁朗开朗,终于到头了,这里只用到了 encrypt_lookup 表替换,我们看看这个表是什么把,直接Console输入 

encrypt_lookup 

咿,好像是跟Base64算法一致,只是把表替换了,那么把我们猜测的用工具运行一下(可惜我的base64加密解密.exe不在身边,只能自己手动处理啦),这里就使用 "123456789' 吧

校验结果


还记得前面的0x3fff限制吗,把整个数据拉进来验证下,以防有变

验证

可以确定,最终 encrypt_b64Encode 算法只是替换了私有表的base64。再来看下参数来源吧

各参数

这里只有 x9 来源是需要经过 encrypt_mcr() 函数计算的,去逆向一下吧。

根据前面的经验,这里很容易就还原出了代码

// 生成crc32表

for (var H, V, W = U, X = 3988292384, z = 256, Y = []; z--; Y[z] = H)

    for (V = 8, H = z; V--; )

        H = (H & 1) ? (H >> 1) ^ X : (H >> 1);

// 计算hash

return function(t) {

    function e(t, e) {

        return G(e - 1181, t)

    }

    // 只需要看这部分

    if ((typedef(t) == "string")) {

        for (var r = 0, n = -1; r < t.length; ++r)

            n = (Y[(n & 255) ^ t.charCodeAt(r)]) ^ (n >>> 8);

        return (n ^ -1) ^ X

    }

    for (r = 0,

    n = -1; (r < t[e(O, C)]); ++r)

        n = (Y[((n & 255) ^ t[r])]) ^ (n >>> 8);

    return ((n ^ -1) ^ X)

}

用 C语言写一个还原代码

unsigned int crc32_table[256];

void make_crc32_table()

{

    unsigned int c;

    int i = 0;

    int bit = 0;

    for(i = 0; i < 256; i++)

    {

        c  = (unsigned int)i;

        for(bit = 0; bit < 8; bit++)

        {

            if(c&1)

            {

                c = (c >> 1)^(0xEDB88320);

            }

            else

            {

                c =  c >> 1;

            }

        }

        crc32_table[i] = c;

    }

}

unsigned int make_crc32(unsigned char *str, unsigned int size)

{

    static bool binit = false;

    if (!binit)

    {

        make_crc32_table();

        binit = true;

    }

    unsigned int crc = 0xFFFFFFFF;

    while(size--)

        crc = crc32_table[(crc&0xFF) ^ *str++] ^ (crc >> 8);

    return (crc^-1)^0xEDB88320;

}

验证一下:

crc32算法

好了,X-S-Common 逆向完毕

写好,测试下数据接口吧

测试数据

6. 参考

2024年某红薯最新x-s-common签名算法分析以及点赞api接口测试nodejs(2024-01-05)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,590评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,808评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,151评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,779评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,773评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,656评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,022评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,678评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,038评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,756评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,411评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,005评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,973评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,053评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,495评论 2 343

推荐阅读更多精彩内容