[逐行分析] 原神抽卡脚本如何获取你的抽卡记录
前言
查看抽卡记录是抽卡规划中重要的一环,但是常规做法每次需要打开原神本体,然后不停翻页,效率较低。如果你曾退坑过一段时间,六个月后云端记录清除,将产生误差。因此一个能够便捷获取抽卡记录、同步云端的小程序出现了。对于记录获取,小程序中给出了这样一种解决方案:
在桌面按下“Win+R”打开运行,输入“powershell”,然后回车,输入以下代码(国服)
iex(irm 'https://img.lelaer.com/cn.ps1')
安全意识较强的会发现这指向一个网页链接,并且如果你对 powershell 有一些了解,可能会产生脚本安全性的顾虑。那么现在就来逐行分析这个脚本的代码,看看她究竟干了些什么事情。
获取源代码
在这段 powershell 代码中,除了网址之外,最令人注意的就是 iex 和 irm。前者是 Invoke-Expression 命令的略缩式(输入 Get-Alias iex 可见),用来执行一段脚本;后者是 Invoke-RestMethod 命令的略缩式,用来从指定的网址下载文件。
结合起来就是——从网址 https://img.lelaer.com/cn.ps1 下载脚本文件 cn.ps1(.ps1 为 powershell 脚本后缀),并执行。删去 iex 不让它执行,即输入 irm 'https://img.lelaer.com/cn.ps1',我们将得到这个脚本完整的源代码:
$logLocation = "%userprofile%\AppData\LocalLow\miHoYo\Genshin Impact\output_log.txt";$logLocationChina = "%userprofile%\AppData\LocalLow\miHoYo\$([char]0x539f)$([char]0x795e)\output_log.txt";
$reg = $args[0]
Write-Host "$([char]0x6b63)$([char]0x5728)$([char]0x83b7)$([char]0x53d6)$([char]0x62bd)$([char]0x5361)$([char]0x5206)$([char]0x6790)$([char]0x94fe)$([char]0x63a5)..." -ForegroundColor GreenWrite-Host " "$logLocation = $logLocationChina
$tmps = $env:TEMP + '\pm.ps1';if ([System.IO.File]::Exists($tmps)) { ri $tmps}
$path = [System.Environment]::ExpandEnvironmentVariables($logLocation);if (-Not [System.IO.File]::Exists($path)) { Write-Host "$([char]0x627e)$([char]0x4e0d)$([char]0x5230)$([char]0x539f)$([char]0x795e)$([char]0x65e5)$([char]0x5fd7)$([char]0x6587)$([char]0x4ef6)$([char]0x8bf7)$([char]0x81f3)$([char]0x5c11)$([char]0x6253)$([char]0x5f00)$([char]0x7948)$([char]0x613f)$([char]0x5386)$([char]0x53f2)$([char]0x754c)$([char]0x9762)$([char]0x4e00)$([char]0x6b21)" -ForegroundColor Red
if (-NOT ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) { Write-Host "$([char]0x662f)$([char]0x5426)$([char]0x4ee5)$([char]0x7ba1)$([char]0x7406)$([char]0x5458)$([char]0x8eab)$([char]0x4efd)$([char]0x6765)$([char]0x8fd0)$([char]0x884c)$([char]0x811a)$([char]0x672c)$([char]0x003f)$([char]0x6309)$([char]0x0045)$([char]0x006e)$([char]0x0074)$([char]0x0065)$([char]0x0072)$([char]0x952e)$([char]0x6765)$([char]0x7ee7)$([char]0x7eed)$([char]0x6216)$([char]0x6309)$([char]0x5176)$([char]0x4ed6)$([char]0x952e)$([char]0x6765)$([char]0x53d6)$([char]0x6d88)" $keyInput = [Console]::ReadKey($true).Key if ($keyInput -ne "13") { return }
$myinvocation.mycommand.definition > $tmps
Start-Process powershell -Verb runAs -ArgumentList "-noexit", $tmps, $reg break }
return}
$logs = Get-Content -Path $path$m = $logs -match "(?m).:/.+(GenshinImpact_Data|YuanShen_Data)"$m[0] -match "(.:/.+(GenshinImpact_Data|YuanShen_Data))" >$null
if ($matches.Length -eq 0) { Write-Host "$([char]0x627e)$([char]0x4e0d)$([char]0x5230)$([char]0x539f)$([char]0x795e)$([char]0x65e5)$([char]0x5fd7)$([char]0x6587)$([char]0x4ef6)$([char]0x8bf7)$([char]0x81f3)$([char]0x5c11)$([char]0x6253)$([char]0x5f00)$([char]0x7948)$([char]0x613f)$([char]0x5386)$([char]0x53f2)$([char]0x754c)$([char]0x9762)$([char]0x4e00)$([char]0x6b21)" -ForegroundColor Red return}
$gamedir = $matches[1]$cachefile = "$gamedir/webCaches/2.52.0.0/Cache/Cache_Data/data_2"$tmpfile = "$env:TEMP/ch_data_2"
Copy-Item $cachefile -Destination $tmpfile
$content = Get-Content -Encoding UTF8 -Raw $tmpfile$splitted = $content -split "1/0/"$found = $splitted -match "https.+webstatic.mihoyo.com/.+?game_biz=hk4e_(global|cn)"
$found = $found[$found.Length - 1] -match "(https.+?game_biz=hk4e_(global|cn))"
Remove-Item $tmpfile
if (-Not $found) { Write-Host "$([char]0x627e)$([char]0x4e0d)$([char]0x5230)$([char]0x539f)$([char]0x795e)$([char]0x65e5)$([char]0x5fd7)$([char]0x6587)$([char]0x4ef6)$([char]0x8bf7)$([char]0x81f3)$([char]0x5c11)$([char]0x6253)$([char]0x5f00)$([char]0x7948)$([char]0x613f)$([char]0x5386)$([char]0x53f2)$([char]0x754c)$([char]0x9762)$([char]0x4e00)$([char]0x6b21)" -ForegroundColor Red return}
$wishHistoryUrl = $matches[0] + "#/log"
Write-Host $wishHistoryUrlSet-Clipboard -Value $wishHistoryUrlWrite-Host " "Write-Host "$([char]0x62bd)$([char]0x5361)$([char]0x5206)$([char]0x6790)$([char]0x5730)$([char]0x5740)$([char]0x83b7)$([char]0x53d6)$([char]0x6210)$([char]0x529f)$([char]0xff0c)$([char]0x8bf7)$([char]0x590d)$([char]0x5236)$([char]0x5230)$([char]0x201c)$([char]0x63d0)$([char]0x74e6)$([char]0x7279)$([char]0x5c0f)$([char]0x52a9)$([char]0x624b)$([char]0x201d)$([char]0x4f7f)$([char]0x7528)" -ForegroundColor GreenWrite-Host "$([char]0x63d0)$([char]0x74e6)$([char]0x7279)$([char]0x5c0f)$([char]0x52a9)$([char]0x624b)$([char]0x795d)$([char]0x60a8)$([char]0x6c14)$([char]0x6ee1)$([char]0x6ee1)$([char]0xff0c)$([char]0x5341)$([char]0x8fde)$([char]0x51fa)$([char]0x91d1)$([char]0xff0c)$([char]0x5c0f)$([char]0x4fdd)$([char]0x5e95)$([char]0x5fc5)$([char]0x4e2d)$([char]0xff01)" -ForegroundColor Yellow国服源代码分析
$logLocation = "%userprofile%\AppData\LocalLow\miHoYo\Genshin Impact\output_log.txt";$logLocationChina = "%userprofile%\AppData\LocalLow\miHoYo\$([char]0x539f)$([char]0x795e)\output_log.txt";这两行声明两个变量,分别指向原神国际服、国服日志文件的目录。%userprofile% 是环境变量,指向“用户”文件夹。
第二行形如 $([char]0x539f) 的代码用来指定一个中文字符。具体原理是:将十六进制数字 0x539f 强制转换为对应编码的字符,这里是“原”字,那么不言而喻下一个字就是“神”了。
$reg = $args[0] 无作用。
Write-Host "$([char]0x6b63)$([char]0x5728)$([char]0x83b7)$([char]0x53d6)$([char]0x62bd)$([char]0x5361)$([char]0x5206)$([char]0x6790)$([char]0x94fe)$([char]0x63a5)..." -ForegroundColor Green 用来以绿色字体输出:“正在获取抽卡分析链接…”
Write-Host " " 紧接着输出空行,$logLocation = $logLocationChina 指定了使用国服的日志文件。
$tmps = $env:TEMP + '\pm.ps1';if ([System.IO.File]::Exists($tmps)) { ri $tmps}这段代码用来存储一份脚本的备份文件 pm.ps1 到系统缓存目录。并且删除原先存储的 pm.ps1 文件。这段代码用于后续提升管理员权限后重新调用脚本。
$path = [System.Environment]::ExpandEnvironmentVariables($logLocation);if (-Not [System.IO.File]::Exists($path)) { Write-Host "$([char]0x627e)$([char]0x4e0d)$([char]0x5230)$([char]0x539f)$([char]0x795e)$([char]0x65e5)$([char]0x5fd7)$([char]0x6587)$([char]0x4ef6)$([char]0x8bf7)$([char]0x81f3)$([char]0x5c11)$([char]0x6253)$([char]0x5f00)$([char]0x7948)$([char]0x613f)$([char]0x5386)$([char]0x53f2)$([char]0x754c)$([char]0x9762)$([char]0x4e00)$([char]0x6b21)" -ForegroundColor Red
if (-NOT ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) { Write-Host "$([char]0x662f)$([char]0x5426)$([char]0x4ee5)$([char]0x7ba1)$([char]0x7406)$([char]0x5458)$([char]0x8eab)$([char]0x4efd)$([char]0x6765)$([char]0x8fd0)$([char]0x884c)$([char]0x811a)$([char]0x672c)$([char]0x003f)$([char]0x6309)$([char]0x0045)$([char]0x006e)$([char]0x0074)$([char]0x0065)$([char]0x0072)$([char]0x952e)$([char]0x6765)$([char]0x7ee7)$([char]0x7eed)$([char]0x6216)$([char]0x6309)$([char]0x5176)$([char]0x4ed6)$([char]0x952e)$([char]0x6765)$([char]0x53d6)$([char]0x6d88)" $keyInput = [Console]::ReadKey($true).Key if ($keyInput -ne "13") { return }
$myinvocation.mycommand.definition > $tmps
Start-Process powershell -Verb runAs -ArgumentList "-noexit", $tmps, $reg break }
return}这段代码有点长,描述了脚本文件不存在时的错误处理逻辑。首先用 ExpandEnvironmentVariables 来将先前 %userprofile% 展开成绝对路径。紧接着用 System.IO.File 库的 Exists 方法检测文件存在性,如果不存在则进入内层代码。
如果不存在文件,内层第一行代码用红色字体输出“找不到原神日志文件,请至少打开祈愿历史界面一次”。当然,运行权限不足也有可能造成无法访问到对应文件,因此脚本调用 GetCurrent 方法获取脚本运行信息,再用 IsInRole 方法判断是否未用管理员权限运行,若权限不足,输出“是否以管理员身份来运行脚本?按 Enter 键来继续或按其他键来取消”。用户按下回车后则将当前脚本写入方才指定的临时脚本备份中(按下其他键则直接终止运行),然后用管理员权限运行备份脚本。下面,非循环代码中的 break 语句直接终止脚本运行,用以处理提权的情况;再往下的 return 语句在日志文件不存在时终止脚本运行,二者处理的情况是不同的。
$logs = Get-Content -Path $path$m = $logs -match "(?m).:/.+(GenshinImpact_Data|YuanShen_Data)"$m[0] -match "(.:/.+(GenshinImpact_Data|YuanShen_Data))" >$null第一行读取了先前指定的日志文件。第二行匹配包含游戏数据目录 GenshinImpact_Data 或者 YuanShen_Data 的多个行((?m) 指定多行匹配模式)。第三行在第一个匹配行中再次匹配出完整路径,>$null 忽略输出,最终自动生成的变量 $matches 就包含了游戏本体目录。
if ($matches.Length -eq 0) { Write-Host "$([char]0x627e)$([char]0x4e0d)$([char]0x5230)$([char]0x539f)$([char]0x795e)$([char]0x65e5)$([char]0x5fd7)$([char]0x6587)$([char]0x4ef6)$([char]0x8bf7)$([char]0x81f3)$([char]0x5c11)$([char]0x6253)$([char]0x5f00)$([char]0x7948)$([char]0x613f)$([char]0x5386)$([char]0x53f2)$([char]0x754c)$([char]0x9762)$([char]0x4e00)$([char]0x6b21)" -ForegroundColor Red return}若未找到游戏本体目录,则输出红色错误信息并退出。
$gamedir = $matches[1]$cachefile = "$gamedir/"$tmpfile = "$env:TEMP/ch_data_2"Copy-Item $cachefile -Destination $tmpfile找到了游戏本体目录,那么在其后拼接 /webCaches/2.52.0.0/Cache/Cache_Data/data_2 得到存储抽卡记录网址的缓存文件,并在系统缓存目录下备份一份,命名为 ch_data_2。值得注意的是:拼接的文字中 2.52.0.0 可能在某次游戏更新后改变,脚本并未做相应处理。
$content = Get-Content -Encoding UTF8 -Raw $tmpfile$splitted = $content -split "1/0/"$found = $splitted -match "https.+webstatic.mihoyo.com/.+?game_biz=hk4e_(global|cn)"$found = $found[$found.Length - 1] -match "(https.+?game_biz=hk4e_(global|cn))"Remove-Item $tmpfile
if (-Not $found) { Write-Host "......" -ForegroundColor Red return}用 UTF-8 编码读取备份的缓存文件,以 1/0/ 为分隔符把文件内容分成若干块,每一块中查询是否有符合抽卡记录地址的文字,并将地址储存在变量 $found 中。删除备份文件来收尾。若未找到相应地址,则输出红色错误并退出。
$wishHistoryUrl = $matches[0] + "#/log"Write-Host $wishHistoryUrlSet-Clipboard -Value $wishHistoryUrlWrite-Host " "Write-Host "$([char]0x62bd)$([char]0x5361)$([char]0x5206)$([char]0x6790)$([char]0x5730)$([char]0x5740)$([char]0x83b7)$([char]0x53d6)$([char]0x6210)$([char]0x529f)$([char]0xff0c)$([char]0x8bf7)$([char]0x590d)$([char]0x5236)$([char]0x5230)$([char]0x201c)$([char]0x63d0)$([char]0x74e6)$([char]0x7279)$([char]0x5c0f)$([char]0x52a9)$([char]0x624b)$([char]0x201d)$([char]0x4f7f)$([char]0x7528)" -ForegroundColor GreenWrite-Host "$([char]0x63d0)$([char]0x74e6)$([char]0x7279)$([char]0x5c0f)$([char]0x52a9)$([char]0x624b)$([char]0x795d)$([char]0x60a8)$([char]0x6c14)$([char]0x6ee1)$([char]0x6ee1)$([char]0xff0c)$([char]0x5341)$([char]0x8fde)$([char]0x51fa)$([char]0x91d1)$([char]0xff0c)$([char]0x5c0f)$([char]0x4fdd)$([char]0x5e95)$([char]0x5fc5)$([char]0x4e2d)$([char]0xff01)" -ForegroundColor Yellow在获取的网址后追加 #/log 得到可直接查看的祈愿记录地址。在控制台输出地址,同时自动复制到剪贴板。输出空行,再分别输出绿色的“抽卡分析地址获取成功,请复制到‘提瓦特小助手’使用”和黄色的“提瓦特小助手祝您气满满,十连出金,小保底必中!”信息。
综合评价:在权限不足时提权可能涉及到 UAC 弹窗,允许即可;版本号 2.52.0.0 硬编码,可能在某次更新后失效。
国际服代码分析
同样的方法,从 https://img.lelaer.com/oc.ps1 下载源码。尽管标注为“国际服”,实际上这是一个通用脚本。此处仅分析与国服脚本的主要差异点。
$reg = $args[0]$apiHost = "public-operation-hk4e-sg.hoyoverse.com"if ($reg -eq "china") { Write-Host "Using China cache location" $logLocation = $logLocationChina $apiHost = "public-operation-hk4e.mihoyo.com"}这段代码处理了命令行参数,默认获取国际服。若获取国服需要加入命令行参数,而 iex 不支持参数,因此需下载后额外启动一次。
$gamedir = $matches[1]# Thanks to @jogerj for getting the latest webchache dir$webcachePath = Resolve-Path "$gamedir/webCaches"$cacheVerPath = Get-Item (Get-ChildItem -Path $webcachePath | Sort-Object LastWriteTime -Descending | Select-Object -First 1).FullName$cachefile = Resolve-Path "$cacheVerPath/Cache/Cache_Data/data_2"这段代码实现动态查找最新 webCaches 版本号的功能(见井号开头注释),避免硬编码缺陷。
function testUrl($url) { $ProgressPreference = 'SilentlyContinue' $uri = [System.UriBuilder]::New($url) $uri.Path = "gacha_info/api/getGachaLog" $uri.Host = $apiHost $uri.Fragment = "" $params = [System.Web.HttpUtility]::ParseQueryString($uri.Query) $params.Set("lang", "en"); $params.Set("gacha_type", 301); $params.Set("size", "5"); $params.Add("lang", "en-us"); $uri.Query = $params.ToString() $apiUrl = $uri.Uri.AbsoluteUri
$response = Invoke-WebRequest -Uri $apiUrl -ContentType "application/json" -UseBasicParsing -TimeoutSec 10 | ConvertFrom-Json $testResult = $response.retcode -eq 0 return $testResult}这里定义了一个测试函数,应该是开发过程中用于检测网址拼接的调试函数。对正常运行无影响。
$link = $false$linkFound = $falsefor ($i = $found.Length - 1; $i -ge 0; $i -= 1) { $t = $found[$i] -match "(https.+?game_biz=)" $link = $matches[0] Write-Host "`rChecking Link $i" -NoNewline $testResult = testUrl $link if ($testResult -eq $true) { $linkFound = $true break } Sleep 1}这里从新到旧地遍历每一个候选链接,取用最新的,能够保证获取到最新的链接。遍历间隔一秒。
综合评价:间隔一秒的设计可能在存在多个候选链接时拖慢进度。
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!



































































































































































