mirror of
https://github.com/Lakr233/vphone-cli.git
synced 2026-10-02 08:04:32 +08:00
A repo-wide security review found issues in the root CFW install, the Launchpad helper, VM bundle handling and the guest boundary. This fixes them and applies swift format. - cfw install mounts guest volumes nosuid,nodev,nobrowse in a root-only temp folder and does every guest read and write through an open folder handle, never following links. BuildManifest cryptex paths must stay in the restore folder, and only a private copy is attached. - Root no longer chowns or chmods the whole VM folder after an install. The shared walk skips hard links, symlinks, special files and other volumes. - Every Launchpad helper action needs administrator authorization. Only the user who started a CFW install can cancel it. The helper refuses setuid, hard-linked, special or escaping-symlink entries in a bundle. - Manifest file names must be single names in the bundle and point to regular files. vm import refuses links that leave the bundle. - Guest file names from the file browser, QuickLook, drag-out and crash logs are validated and written exclusively, without overwriting, and are quarantined. - The guest HTTP client no longer traps on a bare Content-Length, caps bodies and enforces a per-request deadline. - The --api-listen proxy needs a per-launch token. vphoned refuses browser-origin and non-local Host requests. - vphoned stops following links when it sets up Irisin and the camera files. Thanks to fresh-fx59 for reporting the guest file name, HTTP parsing, cfw install and manifest path issues in #469. Co-authored-by: Aleksey Aksenov <5788874+fresh-fx59@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
113 lines
4.3 KiB
Objective-C
113 lines
4.3 KiB
Objective-C
#import "Include/VphonedNative.h"
|
|
#include <CommonCrypto/CommonDigest.h>
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <mach-o/dyld.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <sys/stat.h>
|
|
#include <unistd.h>
|
|
|
|
static const char *cache_directory = "/var/root/Library/Caches";
|
|
static const char *cache = "/var/root/Library/Caches/vphoned";
|
|
static const char *marker = "/var/root/Library/Caches/vphoned.api-v2";
|
|
static const char *pending = "/var/root/Library/Caches/vphoned.api-v2.pending";
|
|
|
|
static const char *leaf(const char *path) {
|
|
const char *slash = strrchr(path, '/');
|
|
return slash ? slash + 1 : path;
|
|
}
|
|
|
|
// MARK: - Cached Binary Trust
|
|
|
|
// The launchd proxy runs as root and execs the cached binary, so only root
|
|
// may have been able to write it: the file, its marker, and the directory
|
|
// that holds them must be root-owned and not writable by group or other.
|
|
static bool root_only(int fd, mode_t type) {
|
|
struct stat info;
|
|
return fstat(fd, &info) == 0 && (info.st_mode & S_IFMT) == type && info.st_uid == 0 &&
|
|
(info.st_mode & (S_IWGRP | S_IWOTH)) == 0;
|
|
}
|
|
|
|
static bool cached_binary_matches_marker(int directory, int image) {
|
|
CC_SHA256_CTX context;
|
|
CC_SHA256_Init(&context);
|
|
unsigned char buffer[64 * 1024];
|
|
ssize_t count;
|
|
while ((count = read(image, buffer, sizeof(buffer))) != 0) {
|
|
if (count < 0) {
|
|
if (errno == EINTR) continue;
|
|
return false;
|
|
}
|
|
CC_SHA256_Update(&context, buffer, (CC_LONG)count);
|
|
}
|
|
|
|
int record = openat(directory, leaf(marker), O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
|
|
if (record < 0) return false;
|
|
char expected[65];
|
|
ssize_t length = root_only(record, S_IFREG) ? read(record, expected, sizeof(expected)) : -1;
|
|
close(record);
|
|
if (length != 64) return false;
|
|
expected[64] = '\0';
|
|
|
|
unsigned char digest[CC_SHA256_DIGEST_LENGTH];
|
|
CC_SHA256_Final(digest, &context);
|
|
static const char hex[] = "0123456789abcdef";
|
|
char actual[65];
|
|
for (int index = 0; index < CC_SHA256_DIGEST_LENGTH; index++) {
|
|
actual[index * 2] = hex[digest[index] >> 4];
|
|
actual[index * 2 + 1] = hex[digest[index] & 15];
|
|
}
|
|
actual[64] = '\0';
|
|
return strcmp(actual, expected) == 0;
|
|
}
|
|
|
|
// MARK: - Bootstrap
|
|
|
|
void vp_native_bootstrap_cached_binary(void) {
|
|
char current[4096];
|
|
uint32_t size = sizeof(current);
|
|
if (_NSGetExecutablePath(current, &size) != 0 || strcmp(current, cache) == 0) return;
|
|
|
|
// Keep the launchd process small: hash the cache in bounded chunks. The
|
|
// file is opened once without following a link, and the hash covers that
|
|
// descriptor.
|
|
int directory = open(cache_directory, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
|
|
if (directory < 0) return;
|
|
struct stat hashed;
|
|
int image = -1;
|
|
bool trusted = root_only(directory, S_IFDIR) &&
|
|
(image = openat(directory, leaf(cache), O_RDONLY | O_NOFOLLOW | O_CLOEXEC)) >= 0 &&
|
|
root_only(image, S_IFREG) && fstat(image, &hashed) == 0 &&
|
|
(hashed.st_mode & S_IXUSR) != 0 && cached_binary_matches_marker(directory, image);
|
|
if (image >= 0) close(image);
|
|
if (!trusted || renameat(directory, leaf(marker), directory, leaf(pending)) != 0) {
|
|
close(directory);
|
|
return;
|
|
}
|
|
|
|
// The guest has no fexecve. Exec by path only if the path still names the
|
|
// file that was hashed; otherwise the pending marker leaves the cache
|
|
// unconfirmed and the installed binary keeps running.
|
|
struct stat named;
|
|
bool same = fstatat(directory, leaf(cache), &named, AT_SYMLINK_NOFOLLOW) == 0 &&
|
|
named.st_dev == hashed.st_dev && named.st_ino == hashed.st_ino;
|
|
close(directory);
|
|
if (!same) {
|
|
fprintf(stderr, "vphoned proxy: cached binary changed after it was verified\n");
|
|
return;
|
|
}
|
|
char *const arguments[] = {(char *)cache, NULL};
|
|
execv(cache, arguments);
|
|
fprintf(stderr, "vphoned proxy: cached binary exec failed: %s\n", strerror(errno));
|
|
_exit(1);
|
|
}
|
|
|
|
void vp_native_confirm_cached_binary(void) {
|
|
char current[4096];
|
|
uint32_t size = sizeof(current);
|
|
if (_NSGetExecutablePath(current, &size) != 0 || strcmp(current, cache) != 0) return;
|
|
if (rename(pending, marker) != 0)
|
|
fprintf(stderr, "vphoned: could not confirm cached binary: %s\n", strerror(errno));
|
|
}
|