first commit

This commit is contained in:
MikeWang000000 2022-12-17 11:37:06 +08:00
commit 63aba90556
6 changed files with 319 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
.DS_Store
._*
.pkg

25
README.md Normal file
View File

@ -0,0 +1,25 @@
# Parallels Desktop for Mac PKG
This script automatically fetch the lastest version of Parallels Desktop for Mac, patch it and build the full PKG installer.
## Compatibility
Parallels Desktop 15, 16, 17, 18 and newer
## Usage
```bash
bash build_pkg.sh # to build the latestet version
bash build_pkg.sh 18.0.0-53049 # to build the specified verion
```
## Dependencies
You may need [Homebrew](https://brew.sh) to install dependencies.
This script requires the following dependencies:
```bash
brew install radare2
python3 -m pip install r2pipe
```
## Disclaimer
THIS SOFTWARE IS ONLY FOR LEARNING AND RESEARCH USE.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.

47
build_pkg.sh Normal file
View File

@ -0,0 +1,47 @@
#!/bin/bash
set -e
TMPDIR=`mktemp -d`
function cleanup()
{
echo " * Cleaning Up..."
hdiutil detach "${TMPDIR}/mnt" -quiet >/dev/null 2>&1 || true
rm -rf "${TMPDIR}" || true
}
trap cleanup EXIT
if [ -z "$1" ]
then
echo " * Getting latest version..."
PD_VER=`python3 script/get_latest_version.py`
echo " * Latest version: ${PD_VER}"
else
PD_VER="$1"
echo " * Selected version: ${PD_VER}"
fi
PD_URL="https://download.parallels.com/desktop/v${PD_VER%%.*}/${PD_VER}/ParallelsDesktop-${PD_VER}.dmg"
echo " * Downloading DMG..."
wget -q --show-progress -O "${TMPDIR}/ParallelsDesktop-${PD_VER}.dmg" "${PD_URL}" || { echo "Download failure!"; exit 1; }
echo " * Extracting app from DMG..."
mkdir -p "${TMPDIR}/mnt" && hdiutil attach "${TMPDIR}/ParallelsDesktop-${PD_VER}.dmg" -quiet -mountpoint "${TMPDIR}/mnt" -noverify -nobrowse
mkdir -p "${TMPDIR}/payload" && ditto "${TMPDIR}/mnt/Parallels Desktop.app" "${TMPDIR}/payload/Parallels Desktop.app"
hdiutil detach "${TMPDIR}/mnt" -quiet && rmdir "${TMPDIR}/mnt"
xattr -d com.apple.FinderInfo "${TMPDIR}/payload/Parallels Desktop.app"
chflags nohidden "${TMPDIR}/payload/Parallels Desktop.app"
echo " * Patching..."
python3 script/gen_patch_script.py "${PD_VER}" "${TMPDIR}/payload/Parallels Desktop.app/Contents/MacOS/Parallels Service.app/Contents/MacOS/prl_disp_service" "${TMPDIR}"
echo " * Creating PKG..."
pkgbuild --quiet --analyze --root "${TMPDIR}/payload" "${TMPDIR}/component.plist"
plutil -replace BundleIsRelocatable -bool NO "${TMPDIR}/component.plist"
plutil -replace BundleIsVersionChecked -bool NO "${TMPDIR}/component.plist"
plutil -replace BundleHasStrictIdentifier -bool NO "${TMPDIR}/component.plist"
plutil -replace BundleOverwriteAction -string upgrade "${TMPDIR}/component.plist"
pkgbuild --quiet --root "${TMPDIR}/payload" --install-location "/Applications/" --component-plist "${TMPDIR}/component.plist" --scripts "${TMPDIR}/scripts" "./ParallelsDesktop-${PD_VER}.pkg"
cleanup && trap - EXIT
echo " * Done!"

124
script/gen_patch_script.py Normal file
View File

@ -0,0 +1,124 @@
#!/usr/bin/python3
import hashlib
import logging
import r2pipe
import shutil
import shlex
import sys
import os
logging.basicConfig(
level=logging.INFO,
format=' %(levelname)s %(message)s', datefmt='%Y/%m/%d %H:%M:%S'
)
logging.addLevelName(logging.INFO, '`--')
logging.addLevelName(logging.WARNING, '<!>')
logging.addLevelName(logging.ERROR, '<X>')
logger = logging.getLogger(sys.argv[0])
class InvalidArchException(Exception):
pass
def generate_postinstall(**kwargs):
with open(os.path.join(os.path.dirname(__file__), "postinstall.template"), "r") as fin:
content = fin.read()
for k, v in kwargs.items():
content = content.replace("{{" + k + "}}", str(v))
return content
def patch_pdfm_srv(pdfm_srv_path, arch = "arm"):
p1 = p2 = -1
if arch == "arm":
asm = "mov x0, 0x1; ret;"
elif arch == "x86":
asm = "push 0x1; pop rax; ret;"
else:
raise InvalidArchException("arch should be either 'arm' or 'x86'")
r2 = r2pipe.open(pdfm_srv_path, flags=["-2", "-a", arch])
# Verify binary architecture
for b in r2.cmdj(f"!rabin2 -j -A {shlex.quote(pdfm_srv_path)}")['bins']:
if b['arch'] == arch:
break
else:
raise InvalidArchException(f"arch '{arch}' not found in this binary")
# Run binary analysis
r2.cmd("aaa")
# Calculate virtual address -> binary offset
file_map = r2.cmdj("om.j")
offset = file_map["delta"] - file_map["from"]
# Recover class info from RTTI, then find the patch points
r2.cmd("avrr")
p1res1 = r2.cmdj("acllj DspLic::SignCheckerImpl")["methods"]
p1res2 = r2.cmdj("acllj Signature::SignCheckerImpl")["methods"]
p2res1 = r2.cmdj("axtj str._usr_bin_codesign")
if p1res1:
p1 = p1res1[-1]["addr"]
elif p1res2:
p1 = p1res2[-1]["addr"]
if p2res1:
p2 = p2res1[-1]["fcn_addr"]
# Write patch code
if p1 >= 0:
r2.cmd(f"oo+")
r2.cmd(f"s {p1}")
r2.cmd(f'"wa {asm}"')
if p2 >= 0:
r2.cmd(f"oo+")
r2.cmd(f"s {p2}")
r2.cmd(f'"wa {asm}"')
r2.quit()
return p1 + offset, p2 + offset
def main():
if len(sys.argv) < 3:
print(f"Usage: python3 {sys.argv[0]} VERSION /path/to/prl_disp_service /path/to/output_dir\n")
return
full_version = sys.argv[1]
pdfm_srv_in_path = sys.argv[2]
output_dir = sys.argv[3]
pdfm_srv_out_path = os.path.join(output_dir, "prl_disp_service.patched")
shutil.copy(pdfm_srv_in_path, pdfm_srv_out_path)
a01 = a02 = i01 = i02 = -1
for arch in "arm", "x86":
try:
logger.info(f"Patching {arch} code...")
x01, x02 = patch_pdfm_srv(pdfm_srv_out_path, arch)
if x01 < 0 or x02 < 0:
logger.warning(f"One or more patch points not found in {arch} {(x01, x02)}")
if arch == "arm":
a01, a02 = x01, x02
elif arch == "x86":
i01, i02 = x01, x02
except InvalidArchException as ex:
logger.warning(str(ex))
logger.info(f"Generating postinstall script...")
with open(pdfm_srv_in_path, "rb") as fin:
md5_before = hashlib.md5(fin.read()).hexdigest()
with open(pdfm_srv_out_path, "rb") as fin:
md5_after = hashlib.md5(fin.read()).hexdigest()
if md5_before == md5_after:
raise RuntimeError("Nothing is patched!")
os.makedirs(os.path.join(output_dir, "scripts"), exist_ok=True)
with open(os.path.join(output_dir, "scripts", "postinstall"), "w") as fout:
fout.write(generate_postinstall(
full_version = full_version,
hash_before = md5_before,
hash_after = md5_after,
i_01 = i01,
i_02 = i02,
a_01 = a01,
a_02 = a02
))
os.chmod(os.path.join(output_dir, "scripts", "postinstall"), mode=0o755)
logger.info(f"Patch info: {i01}/{i02}/{a01}/{a02}")
if __name__ == '__main__':
main()

View File

@ -0,0 +1,37 @@
import xml.etree.ElementTree
import urllib.request
def get_lastest_parallels_version():
main_version = -1
for i in range(15, 100):
try:
req = urllib.request.Request(
f"https://update.parallels.com/desktop/v{i}/parallels/parallels_updates.xml",
headers = {
"User-Agent": "Parallels Desktop"
}
)
with urllib.request.urlopen(req) as res:
ret = res.read()
except urllib.error.HTTPError as ex:
if ex.code == 404:
main_version = i - 1
break
else:
raise
version_ele = xml.etree.ElementTree.fromstring(ret).find("Product").find("Version")
return (
int(version_ele.find("Major").text),
int(version_ele.find("Minor").text),
int(version_ele.find("SubMinor").text),
int(version_ele.find("SubSubMinor").text)
)
def main():
v1, v2, v3, v4 = get_lastest_parallels_version()
print(f"{v1}.{v2}.{v3}-{v4}")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,83 @@
#!/bin/bash
export PATH="/bin:/sbin:/usr/bin:/usr/sbin"
export LANG=C
export LC_CTYPE=C
export LC_ALL=C
PD_VER="{{full_version}}"
PD_BEF="{{hash_before}}"
PD_AFT="{{hash_after}}"
PD_I01={{i_01}}
PD_I02={{i_02}}
PD_A01={{a_01}}
PD_A02={{a_02}}
PD_IIN="\x6a\x01\x58\xc3"
PD_AIN="\x20\x00\x80\xd2\xc0\x03\x5f\xd6"
PD_LOC="/Library/Preferences/Parallels/parallels-desktop.loc"
PD_DIR="/Applications/Parallels Desktop.app"; [ -f "${PD_LOC}" ] && PD_DIR=$(cat "${PD_LOC}")
PD_MAC="${PD_DIR}/Contents/MacOS"
PD_SRV="${PD_MAC}/Parallels Service.app/Contents/MacOS/prl_disp_service"
PD_LIC="/Library/Preferences/Parallels/licenses.json"
MY_VST=$(defaults read "${PD_DIR}/Contents/Info.plist" CFBundleShortVersionString)
MY_VID=$(defaults read "${PD_DIR}/Contents/Info.plist" CFBundleVersion)
MY_VER="${MY_VST}-${MY_VID}"
function throw() {
osascript -e 'display alert "PD Patcher" message "'"$1"'"'
exit 1
}
[ "$EUID" -ne 0 ] && throw "Permission Error"
[ -d "${PD_DIR}" ] || {
throw "Error: Cannot find PD ${PD_VER}, please retry"
}
[ "${PD_VER}" = "${MY_VER}" ] || {
throw "Version mismatch: current ${MY_VER} != ${PD_VER}, please uninstall previous version"
}
[ `md5 -q "${PD_SRV}"` = "${PD_BEF}" ] || {
throw "Error: PD Service is corrupted or already patched"
}
"${PD_MAC}/inittool" init && sleep 3 || {
throw "Error: PD initialization failed"
}
pgrep -x "prl_disp_service" || "${PD_MAC}/Parallels Service" service_start
"${PD_MAC}/prlsrvctl" web-portal signout
killall -9 prl_client_app prl_disp_service
cp "${PD_SRV}" "${PD_SRV}.bak" || {
throw "Permission denied: no write access"
}
chmod a-x "${PD_SRV}.bak"
[ "${PD_I01}" -ne -1 ] && { printf "${PD_IIN}" | dd of="${PD_SRV}" obs=1 seek="${PD_I01}" conv=notrunc; }
[ "${PD_I02}" -ne -1 ] && { printf "${PD_IIN}" | dd of="${PD_SRV}" obs=1 seek="${PD_I02}" conv=notrunc; }
[ "${PD_A01}" -ne -1 ] && { printf "${PD_AIN}" | dd of="${PD_SRV}" obs=1 seek="${PD_A01}" conv=notrunc; }
[ "${PD_A02}" -ne -1 ] && { printf "${PD_AIN}" | dd of="${PD_SRV}" obs=1 seek="${PD_A02}" conv=notrunc; }
[ `md5 -q "${PD_SRV}"` = "${PD_AFT}" ] || {
throw "Error: Checksum mismatch"
}
chflags noschg "${PD_LIC}"
chflags nouchg "${PD_LIC}"
rm -f "${PD_LIC}" && echo '{"license":"{\"product_version\":\"'"${PD_VER%%.*}"'.*\",\"edition\":2,\"platform\":3,\"product\":7,\"offline\":true,\"cpu_limit\":32,\"ram_limit\":131072}"}' > "${PD_LIC}" || {
throw "Error: Cannot write license file"
}
chflags schg "${PD_LIC}"
codesign -f -s - --timestamp=none --all-architectures "${PD_SRV}" || {
throw "Error: Cannot sign app"
}
open -j -a "${PD_DIR}"
sleep 3 && killall -9 prl_client_app
open -a "${PD_DIR}"
exit 0