js逆向分析-某樱花动漫获取视频地址

前言

最近这些年看动漫比较多 由于国内和谐审核版权 很多动漫看不了 所以一般找这种动漫网 字幕组 看看盗版 樱花动漫网有很多个 都叫这名字 分析了其中一个
本来可以用IDM可以直接下载 不过看一些动漫 十来二十多集 或者更多 一个一个下 就比较麻烦 所以就分析一下 写个小工具 批量获取地址下载

准备工作

浏览器 chrome 火狐 edge都行 看自己顺手
辅助工具 fiddler 或者 HTTP Debugger 都行 看自己顺手
下载工具 IDM M3U8下载工具 常见的文本编辑器 EmEditor vscode Winhex之类

开始分析

不少视频站都 做了 JS混淆 F12的无限debugger 加密 之类
碰巧 这种就碰到了

浏览器打开F12 就无限debugger 暂停 让你无法调试 并且使浏览器占用大量内存 导致浏览器卡死 崩溃等
绕过方法
第一种
禁用断点 这样就不会卡住 但是也有缺点 你也无法使用断点调试
第二种
置空函数
直接把debugger相关的函数置空掉就行

Function函数置空


1
2
3
4
5
6
7
var AAA = Function;
Function = function (x) {
  if (x != 'debugger') {
    return AAA(x);
  }
  return function () {};
};

1
2
3
4
5
6
7
8
9
10
11
12
Function.prototype.__constructor_back = Function.prototype.constructor;
Function.prototype.constructor = function() {
    if(arguments && typeof arguments[0]==='string'){
        //alert("new function: "+ arguments[0]);
        if("debugger" === arguments[0]){
            //arguments[0]="console.log("anti debugger");";
            //arguments[0]=";";
            return
        }
    }
   return Function.prototype.__constructor_back.apply(this,arguments);
}

eval 函数


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
eval_ = eval;
//下面这样写,是为了过瑞数的 eval.toString 检测
eval = function(a){if(a=='debugger'){return ''}else{return eval_(a)}}    

// 或者如下:
var my_eval = eval;
eval = function (arg) {
    if (arg.indexOf('debugger') != -1) {
        return function () {
        };
    }
    return my_eval(arg);
}
var _old = Function.prototype.toString.call
Function.prototype.toString.call = function (arg) {
    if (arg === eval) {
        return "function eval() { [native code] }"
    }
    return _old(arg);
}

 

setInterval 函数


1
2
3
4
5
6
7
setInterval_back = setInterval
setInterval = function(a,b){
    if(a.toString().indexOf('debugger') != -1){
      return null;
    }
    return setInterval_back(a, b);
}

第三种
本地替换JS文件
通过 中间人 或者其他方法 替换js 删除debugger或者改写语句
可以先 全局搜索 debugger 这个关键字 或者 bugger bugg 之类 有一些会有混淆 或者 对字符串的 位移 编码 等

也可以通过断点下来的 调用堆栈 进行分析
通过堆栈分析

发现多处 断点命令 还有 自写的编码函数 加强了一些逆向难度 现在就可以把这个js文件保存下来 然后可以删除这条语句 或者 改写 把de 改成dd 注释之类
通过 fiddler 或者 HTTP Debugger 还有 reres (Chrome浏览器插件) 之类 把这个文件替换成掉 然后重新打开 逐步测试 直到 没有断点了 (开了HTHS 部分软件无效)

其它就是
要有耐心 分析 混淆过后的js
Never pause here
添加条件断点 之类 可以 找找资料 学习学习 一些姿势

绕过了就没限制了随便玩了 其实不绕也行 要有一定的js功底 进行分析

可以用IDM下载 先看看 是什么格式的视频 运气不错 是mp4的视频 如果是m3u8的 就要稍微麻烦一些 要获取 密钥 或者 要更改一些文件内容 还要合并 之类 会麻烦很多
现在就是分析 这个地址是怎么来的

通过翻找 几个html页面 发现 地址在 播放器的配置js里面

之前就发现 有加载 aes.js 预测多半是 aes加密

通过全局搜索 getVideoInfo 这个函数 然后进行断点调试 发现了  CryptoJS库的 AES加密 key 和 iv 都写在js里面猜测 整个网站的密钥都是一样的
不然网站管理员 运维 会很麻烦管理
混淆会有一定麻烦 可以自己替换一下


1
2
3
4
CryptoJS["AES"]["decrypt"]("/JPzXPhmaTLXAD+Y9MX/QcqxriJd0PLDcHxJvM18lAzHbyGHo2n/J4U+0TfXU7WzbRMpCqxHla8tqLsOdsPRVsne+ZkPA3F7+GyIEyUgr+5eTzQciy2rdZpi/VSgzR5miCivf5ZKS2ilDfeegv34StD4xiGeLLt6CuwbJdgsvzvjskU4owoFzoZqTm6dwKBjGdep+T4Xn+4wi+uk3BQUw7WJb7izi9qOTn46tQPUr9zkxS2UlApQajZQeOLS5PMqm+lJPnn0obZCvPCBRFpshgbdVNIw0l3o2oCSIyG8iJ4gxOoeTWhsjWxML1ZRbOR8gD8j/ITog49MAV31UsnM1yfeuLms14nWZ5hSuBsfy77ojMK9nnjvXLrHq+pwcrQU8Ei7hS2ddrXUutmjwRen26NUb12btocyTrCSjbxqqLbgcF0qC9APKic/MI+6S1l+4NR5E5SR11gxG8LGc/5ELe/s902to+L5fcVuFdgUKTKZA0pq3K0XB2LCXGLPR43sV0sj/XbZP5HfOwRBhq8fI/XB5YQipvsIt1urJ7DCZy4aLz+FkVblsJWG5WfB0UcNqxSbzEWI0q63vBscgnGFJw==",
_token_key,{
'iv': _token_iv
})["toString"](CryptoJS["enc"]["Utf8"]);

然后就是这样的 就简单很多了 CryptoJS的AES算法默认是CBC模式和PKCS#7填充,密钥大小为256位,IV是128位块大小
然后就发现

密钥和iv都是WordArray 格式的 就是 CryptoJS.enc.Utf8.parse转换后的
用CryptoJS.enc.Utf8.stringify()命令 转换回来 就得到密钥 和iv了

又仔细看了一看 发现 他的 iv 每个视频都不一样 密钥倒是固定的
var _token_iv = CryptoJS[_0x17f1('69', 'wW&e')]['Utf8'][_0x17f1('6a', '4ozi')](bt_token);
混淆过 稍微转换一下 CryptoJS['enc']['Utf8']['parse'](bt_token)


用了一个bt_token变量 找这个放哪里了 全局搜索 发现在 m3u8.php 里面 批量获取 就要正则匹配这个

然后测了几个视频 解密成功 拿到视频地址 还有一个问题 就是
播放器的文件在 /m3u8.php?url=F2pmdfAU%2BD%2Fz896SC59egiGxzdeRbHHERKpmNAnGgXyiRrBFYDCt7GrTJGE4%2BsL5cYGaKKCQiNum1gG1nyLDSw%3D%3D

这个地址是怎么来的 通过对几个文件分析 发现了

发现在第一个页面的 js中 至此 js逆向结束了 剩下的就是写个脚本 批量跑就是了
这个相对而言就简单一些 大概就需要 会一点 正则匹配出来 几个http请求就搞定了
然后就是注意 发生请求时 注意 请求头 可能会验证 Referer cookie token 一些之类
IP访问多了 可能会被屏蔽掉 通过更换代理IP 可解决

参考脚本

效果


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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
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
package main


import (
    "fmt"
    "net/http"
    "io/ioutil"
    "regexp"
    "strings"
    "crypto/cipher"
    "crypto/aes"
    "bytes"
    "encoding/base64"


)
var iv string = "844182a9dfe9c5ca"
const (
    key = "57A891D97E332A9D"
    url_dom="https://www.yhdm52.com"
)
func main() {
    video_get("https://www.yhdm52.com/video/581.html")
}



func video_get(url string) string {
    var html string = http_get(url)
    re := regexp.MustCompile("(/play/[\\d]+-1-\\d+.html)">")
    matches := re.FindAllStringSubmatch(html, -1)
    for _, match := range matches {
        fmt.Println("url:", url_dom+match[1])
        fmt.Println(mp4_get(url_dom+match[1]))
    }
    return string("")
}



func mp4_get(url string) string {
    var aes_data=string(aes_get(url))
    ds, _ := AesDecrypt(aes_data, []byte(key))
    //fmt.Println(string(ds))
    //iv 不一致进行处理
    match,_:=regexp.MatchString(",|"|!|;|{|}|`|#",string(ds))
    if match {
        re := regexp.MustCompile(".+akamaized.net")
        match := re.FindString(string(ds))
        jsonString2 := strings.ReplaceAll(string(ds),string(match),"http://v16m-default.akamaized.net")
        //fmt.Println(jsonString2)
        //fmt.Println("agsec-iv 不一致进行处理")
        return string(jsonString2)


    }else{
        //fmt.Println("agsec-成功")
        return string(ds)
    }
}


func aes_get(url string) string {
    var m3u8_get_html string = m3u8_get(url)
    var html string = http_get("https://danmu.yhdmjx.com/m3u8.php?url="+m3u8_get_html)
    re := regexp.MustCompile("getVideoInfo\\("[^"]+")
    match := re.FindString(html)
    jsonString2 := strings.ReplaceAll(string(match), "getVideoInfo("", "")
    re = regexp.MustCompile("bt_token = "([^"]+)"")
    matches := re.FindAllStringSubmatch(html, -1)
    for _, match := range matches {
        //fmt.Println("iv:", match[1])
        iv=match[1]
    }
    return jsonString2
}



func m3u8_get(url string) string {
    var html_a string =string(http_get(url))
    re := regexp.MustCompile(""url":"[^"]+","url_next"")
    match := re.FindString(html_a)
    jsonString := strings.ReplaceAll(string(match), ""url":"", "")
    jsonString = strings.ReplaceAll(string(jsonString), "","url_next"", "")
    re = regexp.MustCompile("<title>[^.]+</title>")
    match = re.FindString(html_a)
    jsonString2 := strings.ReplaceAll(string(match), "<title>", "")
    jsonString2 = strings.ReplaceAll(string(jsonString2), "</title>", "")
    jsonString2 = strings.ReplaceAll(string(jsonString2), "- 免费观看-在线观看完整版无修-樱花动漫网-无广告弹窗的动漫门户", "")
    fmt.Println(jsonString2)
    return jsonString
}


func http_get(url string) string {
    client := &http.Client{}


    // 创建一个 GET 请求
    req, err := http.NewRequest("GET", url, nil)
    if err != nil {
        return "-1"
    }


    // 发送请求并获取响应
    resp, err := client.Do(req)
    if err != nil {
        return "-1"
    }


    // 关闭响应体
    defer resp.Body.Close()


    // 输出响应状态码和响应体
    if resp.Status != "200 OK" {
        return string(resp.Status)
    }
    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        fmt.Println("Error reading response body:", err)
        return "-1"
    }
    return string(body)


}


func AesEncrypt(encodeStr string, key []byte) (string, error) {
    encodeBytes := []byte(encodeStr)
    //根据key 生成密文
    block, err := aes.NewCipher(key)
    if err != nil {
        return "", err
    }
 
    blockSize := block.BlockSize()
    encodeBytes = PKCS5Padding(encodeBytes, blockSize)
 
    blockMode := cipher.NewCBCEncrypter(block, []byte(iv))
    crypted := make([]byte, len(encodeBytes))
    blockMode.CryptBlocks(crypted, encodeBytes)
 
    return base64.StdEncoding.EncodeToString(crypted), nil
}
 
func PKCS5Padding(ciphertext []byte, blockSize int) []byte {
    padding := blockSize - len(ciphertext)%blockSize
    //填充
    padtext := bytes.Repeat([]byte{byte(padding)}, padding)
 
    return append(ciphertext, padtext...)
}
 
func AesDecrypt(decodeStr string, key []byte) ([]byte, error) {
    //先解密base64
    decodeBytes, err := base64.StdEncoding.DecodeString(decodeStr)
    if err != nil {
        return nil, err
    }
    block, err := aes.NewCipher(key)
    if err != nil {
        return nil, err
    }
    blockMode := cipher.NewCBCDecrypter(block, []byte(iv))
    origData := make([]byte, len(decodeBytes))
 
    blockMode.CryptBlocks(origData, decodeBytes)
    origData = PKCS5UnPadding(origData)
    return origData, nil
}
 
func PKCS5UnPadding(origData []byte) []byte {
    length := len(origData)
    unpadding := int(origData[length-1])
    return origData[:(length - unpadding)]
}

 

对golang 没那么熟 喝完酒 简单写了一个脚本 应该有一些错误 或者一些函数用法问题 问题不大 能运行 喝完酒写的 也懒得改了 随便看看
只写了 他网站的第一接口 其他没看 只写了单动漫爬虫 没写全部的爬虫
具有时效性 可能网站倒闭 密钥修改 接口更换 之类 仅供参考

总结

至此 从js逆向 到golang写一个批量获取下载地址的脚本 就完结了
喝完酒 写的 应该还行 勉勉强强 努力学习 分享一下
学无止境  by agsec 转载注明一下即可

版权声明:
作者:agsec
链接:https://agsec.xyz/archives/205
来源:AG安全团队博客
文章版权归作者所有,未经允许请勿转载。

THE END
分享
二维码
< <上一篇
下一篇>>