first commit
This commit is contained in:
commit
63aba90556
|
@ -0,0 +1,3 @@
|
|||
.DS_Store
|
||||
._*
|
||||
.pkg
|
|
@ -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.
|
|
@ -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!"
|
|
@ -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()
|
|
@ -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()
|
|
@ -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
|
Loading…
Reference in New Issue