From d18cd8809b168f21e350e830a6bcacc567e09add Mon Sep 17 00:00:00 2001 From: Adam Date: Thu, 14 Mar 2024 19:44:54 -0400 Subject: [PATCH] add filesystem permissions check This tries to ensure .runelite and its contents are writable by the user by changing the file acls whenever is it elevated. Co-authored-by: YvesW <7929021+YvesW@users.noreply.github.com> --- liblauncher/CMakeLists.txt | 2 +- liblauncher/acl.cpp | 113 +++++++++ liblauncher/elevation.cpp | 17 +- liblauncher/reg.cpp | 2 +- .../launcher/FilesystemPermissions.java | 233 ++++++++++++++++++ .../java/net/runelite/launcher/Launcher.java | 32 ++- 6 files changed, 385 insertions(+), 14 deletions(-) create mode 100644 liblauncher/acl.cpp create mode 100644 src/main/java/net/runelite/launcher/FilesystemPermissions.java diff --git a/liblauncher/CMakeLists.txt b/liblauncher/CMakeLists.txt index d66bdbc3..91355d1b 100644 --- a/liblauncher/CMakeLists.txt +++ b/liblauncher/CMakeLists.txt @@ -5,7 +5,7 @@ set(CMAKE_CXX_STANDARD 17) find_package(JNI REQUIRED) -add_library(launcher SHARED main.cpp reg.cpp elevation.cpp) +add_library(launcher SHARED main.cpp reg.cpp elevation.cpp acl.cpp) include_directories(../detours/include ${JNI_INCLUDE_DIRS}) if (CMAKE_GENERATOR_PLATFORM STREQUAL "x64") set_target_properties(launcher PROPERTIES OUTPUT_NAME "launcher_amd64") diff --git a/liblauncher/acl.cpp b/liblauncher/acl.cpp new file mode 100644 index 00000000..ba7b9154 --- /dev/null +++ b/liblauncher/acl.cpp @@ -0,0 +1,113 @@ +#include +#include +#include +#include + +#include + +extern void rlThrow(JNIEnv *env, const char *msg); + +// https://learn.microsoft.com/en-us/windows/win32/secauthz/creating-a-security-descriptor-for-a-new-object-in-c--?redirectedfrom=MSDN +extern "C" JNIEXPORT void JNICALL Java_net_runelite_launcher_Launcher_setFileACL(JNIEnv *env, jclass clazz, jstring folderJs, jobjectArray sidsJa) { + SECURITY_DESCRIPTOR securityDescriptor; + if (!InitializeSecurityDescriptor(&securityDescriptor, SECURITY_DESCRIPTOR_REVISION)) { + rlThrow(env, "unable to initialize security descriptor"); + return; + } + + int numSid = env->GetArrayLength(sidsJa); + EXPLICIT_ACCESSW *explicitAccesses = new EXPLICIT_ACCESSW[numSid]; + ZeroMemory(explicitAccesses, sizeof(EXPLICIT_ACCESSW) * numSid); + + for (int i = 0; i < numSid; ++i) { + jstring sidJs = static_cast(env->GetObjectArrayElement(sidsJa, i)); + const jchar *sid = env->GetStringChars(sidJs, nullptr); + + PSID pSid = NULL; + if (!ConvertStringSidToSidW(reinterpret_cast(sid), &pSid)) { + rlThrow(env, "unable to convert string SID to SID"); + env->ReleaseStringChars(sidJs, sid); + goto freesid; + } + + EXPLICIT_ACCESSW &explicitAccess = explicitAccesses[i]; + explicitAccess.grfAccessPermissions = GENERIC_ALL; + explicitAccess.grfAccessMode = SET_ACCESS; + explicitAccess.grfInheritance = OBJECT_INHERIT_ACE | CONTAINER_INHERIT_ACE; + explicitAccess.Trustee.TrusteeForm = TRUSTEE_IS_SID; + explicitAccess.Trustee.TrusteeType = TRUSTEE_IS_GROUP; + explicitAccess.Trustee.ptstrName = (LPWSTR)pSid; + + env->ReleaseStringChars(sidJs, sid); + } + + PACL pAcl = NULL; + DWORD dwResult = SetEntriesInAclW(numSid, explicitAccesses, NULL, &pAcl); + if (dwResult != ERROR_SUCCESS) { + rlThrow(env, "unable to set entries in ACL"); + goto freesid; + } + + if (!SetSecurityDescriptorDacl(&securityDescriptor, TRUE, pAcl, FALSE)) { + rlThrow(env, "error setting security descriptor DACL"); + goto freeacl; + } + + const jchar *folder = env->GetStringChars(folderJs, nullptr); + if (!SetFileSecurityW(reinterpret_cast(folder), DACL_SECURITY_INFORMATION, &securityDescriptor)) { + rlThrow(env, "error setting file security"); + } + env->ReleaseStringChars(folderJs, folder); + +freeacl: + LocalFree(pAcl); + +freesid: + for (int i = 0; i < numSid; ++i) { + EXPLICIT_ACCESSW &explicitAccess = explicitAccesses[i]; + if (explicitAccess.Trustee.ptstrName) { + LocalFree((PSID)explicitAccess.Trustee.ptstrName); + } + } + + delete[] explicitAccesses; +} + +extern "C" JNIEXPORT jstring JNICALL Java_net_runelite_launcher_Launcher_getUserSID(JNIEnv *env, jclass clazz) { + HANDLE hToken; + if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &hToken)) { + rlThrow(env, "error opening process token"); + return nullptr; + } + + DWORD returnLength = 0; + GetTokenInformation(hToken, TokenUser, nullptr, 0, &returnLength); + if (GetLastError() != ERROR_INSUFFICIENT_BUFFER) { + rlThrow(env, "unable to get TokenUser buffer size"); + CloseHandle(hToken); + return nullptr; + } + + std::unique_ptr tokenInfoBuffer(new char[returnLength]); + if (!GetTokenInformation(hToken, TokenUser, tokenInfoBuffer.get(), returnLength, &returnLength)) { + rlThrow(env, "error getting token information"); + CloseHandle(hToken); + return nullptr; + } + + CloseHandle(hToken); + + PTOKEN_USER tokenUser = (PTOKEN_USER)tokenInfoBuffer.get(); + + LPWSTR pstrSid; + if (!ConvertSidToStringSidW(tokenUser->User.Sid, &pstrSid)) { + rlThrow(env, "error converting SID to string"); + return nullptr; + } + + jstring ret = env->NewString(reinterpret_cast(pstrSid), static_cast(wcslen(pstrSid))); + + LocalFree(pstrSid); + + return ret; +} diff --git a/liblauncher/elevation.cpp b/liblauncher/elevation.cpp index b3af59fc..45894e9f 100644 --- a/liblauncher/elevation.cpp +++ b/liblauncher/elevation.cpp @@ -2,7 +2,7 @@ #include extern "C" JNIEXPORT jboolean JNICALL Java_net_runelite_launcher_Launcher_isProcessElevated(JNIEnv *env, jclass clazz, jlong pid) { - HANDLE process = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, (DWORD) pid); + HANDLE process = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, (DWORD)pid); if (process == nullptr) { return false; } @@ -20,5 +20,18 @@ extern "C" JNIEXPORT jboolean JNICALL Java_net_runelite_launcher_Launcher_isProc CloseHandle(process); - return (jboolean) ret; + return (jboolean)ret; +} + +extern "C" JNIEXPORT jlong JNICALL Java_net_runelite_launcher_Launcher_runas(JNIEnv *env, jclass clazz, jstring pathObj, jstring argsObj) { + const jchar *pathString = env->GetStringChars(pathObj, nullptr); + const jchar *argsString = env->GetStringChars(argsObj, nullptr); + + INT_PTR ret = (INT_PTR)ShellExecuteW(nullptr, L"runas", reinterpret_cast(pathString), reinterpret_cast(argsString), + nullptr, SW_SHOWNORMAL); + + env->ReleaseStringChars(pathObj, pathString); + env->ReleaseStringChars(argsObj, argsString); + + return static_cast(ret); } \ No newline at end of file diff --git a/liblauncher/reg.cpp b/liblauncher/reg.cpp index f79222f0..90ef6a19 100644 --- a/liblauncher/reg.cpp +++ b/liblauncher/reg.cpp @@ -1,7 +1,7 @@ #include #include -static void rlThrow(JNIEnv *env, const char *msg) { +void rlThrow(JNIEnv *env, const char *msg) { if (env->ExceptionCheck()) { return; } diff --git a/src/main/java/net/runelite/launcher/FilesystemPermissions.java b/src/main/java/net/runelite/launcher/FilesystemPermissions.java new file mode 100644 index 00000000..956f9e3f --- /dev/null +++ b/src/main/java/net/runelite/launcher/FilesystemPermissions.java @@ -0,0 +1,233 @@ +/* + * Copyright (c) 2024, Adam + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.launcher; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import javax.swing.SwingUtilities; +import lombok.extern.slf4j.Slf4j; +import static net.runelite.launcher.Launcher.LAUNCHER_EXECUTABLE_NAME_WIN; +import static net.runelite.launcher.Launcher.RUNELITE_DIR; +import static net.runelite.launcher.Launcher.isProcessElevated; +import static net.runelite.launcher.Launcher.nativesLoaded; +import static net.runelite.launcher.Launcher.setFileACL; + +@Slf4j +class FilesystemPermissions +{ + // https://learn.microsoft.com/en-us/windows/win32/secauthz/well-known-sids + private static final String SID_SYSTEM = "S-1-5-18"; + private static final String SID_ADMINISTRATORS = "S-1-5-32-544"; + + static boolean check() + { + if (!nativesLoaded) + { + log.debug("Launcher natives were not loaded. Skipping filesystem permission check."); + return false; + } + + final boolean elevated = isProcessElevated(ProcessHandle.current().pid()); + // It is possible for .runelite to exist but be not writable, even when elevated. But we can update the ACLs + // always when elevated, so attempt to fix the ACLs first. + if (elevated) + { + log.info("RuneLite is running as an administrator. This is not recommended because it can cause the files " + + "RuneLite writes to {} to have more strict permissions than would otherwise be required.", + RUNELITE_DIR); + + try + { + final var sid = Launcher.getUserSID(); + log.info("RuneLite is updating the ACLs of the files in {} to be: NT AUTHORITY\\SYSTEM, BUILTIN\\Administrators, " + + "and {} (your user SID). To avoid this, don't run RuneLite with elevated permissions.", + RUNELITE_DIR, sid); + + // Files.walk is depth-first, which doesn't work if the permissions on the root don't allow traversal. + // So we do our own walk. + setTreeACL(RUNELITE_DIR, sid); + } + catch (Exception ex) + { + log.error("Unable to update file permissions", ex); + } + } + + if (!RUNELITE_DIR.exists()) + { + if (!RUNELITE_DIR.mkdirs()) + { + log.error("unable to create directory {} elevated: {}", RUNELITE_DIR, elevated); + + String message; + if (elevated) + { + message = "Unable to create RuneLite directory " + RUNELITE_DIR + " while elevated. Check your filesystem permissions are correct."; + } + else + { + message = "Unable to create RuneLite directory " + RUNELITE_DIR + ". Check your filesystem permissions are correct. If you rerun RuneLite" + + " as an administrator, RuneLite will attempt to create the directory again and fix its permissions."; + } + SwingUtilities.invokeLater(() -> + { + var dialog = new FatalErrorDialog(message); + if (!elevated) + { + dialog.addButton("Run as administrator", FilesystemPermissions::runas); + } + dialog.open(); + }); + return true; + } + + if (elevated) + { + // Set the correct permissions on the newly created folder. This sets object inherit and container inherit, + // so all future files in .runelite should then have the correct permissions. + try + { + final var sid = Launcher.getUserSID(); + setTreeACL(RUNELITE_DIR, sid); + } + catch (Exception ex) + { + log.error("Unable to update file permissions", ex); + } + } + } + + if (!checkPermissions(RUNELITE_DIR)) + { + String message; + if (elevated) + { + // This means the previous ACL update above did not work...? + message = "The file permissions of " + RUNELITE_DIR + ", or a file within it, is not correct. Check the logs for more details."; + } + else + { + message = "The file permissions of " + RUNELITE_DIR + ", or a file within it, is not correct. Check the logs for more details." + + " If you rerun RuneLite as an administrator, RuneLite will attempt to fix the file permissions."; + } + SwingUtilities.invokeLater(() -> + { + var dialog = new FatalErrorDialog(message); + if (!elevated) + { + dialog.addButton("Run as administrator", FilesystemPermissions::runas); + } + dialog.open(); + }); + return true; + } + + return false; + } + + private static boolean checkPermissions(File tree) + { + boolean ok = true; + for (File file : tree.listFiles()) + { + if (file.isDirectory()) + { + if (!checkPermissions(file)) + { + ok = false; + } + } + else + { + Path path = file.toPath(); + if (!Files.isReadable(path) || !Files.isWritable(path)) + { + log.error("Permissions for {} are incorrect. Readable: {} writable: {}", + file, Files.isReadable(path), Files.isWritable(path)); + ok = false; + } + } + } + return ok; + } + + private static void setTreeACL(File tree, String sid) throws IOException + { + log.debug("Setting ACL on {}", tree.getAbsolutePath()); + setFileACL(tree.getAbsolutePath(), new String[]{ + SID_SYSTEM, + SID_ADMINISTRATORS, + sid + }); + Files.setAttribute(tree.toPath(), "dos:readonly", false); + + for (File file : tree.listFiles()) + { + if (file.isDirectory()) + { + setTreeACL(file, sid); + } + else + { + log.debug("Setting ACL on {}", file.getAbsolutePath()); + setFileACL(file.getAbsolutePath(), new String[]{ + SID_SYSTEM, + SID_ADMINISTRATORS, + sid + }); + Files.setAttribute(file.toPath(), "dos:readonly", false); + } + } + } + + private static void runas() + { + log.info("Relaunching as administrator"); + + ProcessHandle current = ProcessHandle.current(); + var command = current.info().command(); + if (command.isEmpty()) + { + log.error("Running process has no command"); + System.exit(-1); + return; + } + + Path path = Paths.get(command.get()); + if (!path.getFileName().toString().equals(LAUNCHER_EXECUTABLE_NAME_WIN)) + { + log.error("Running process is not the launcher: {}", path.getFileName().toString()); + System.exit(-1); + return; + } + + String commandPath = path.toAbsolutePath().toString(); + Launcher.runas(commandPath, ""); + System.exit(0); + } +} diff --git a/src/main/java/net/runelite/launcher/Launcher.java b/src/main/java/net/runelite/launcher/Launcher.java index a5bfd929..12f6a415 100644 --- a/src/main/java/net/runelite/launcher/Launcher.java +++ b/src/main/java/net/runelite/launcher/Launcher.java @@ -88,9 +88,9 @@ @Slf4j public class Launcher { - private static final File RUNELITE_DIR = new File(System.getProperty("user.home"), ".runelite"); - public static final File LOGS_DIR = new File(RUNELITE_DIR, "logs"); - private static final File REPO_DIR = new File(RUNELITE_DIR, "repository2"); + static final File RUNELITE_DIR = new File(System.getProperty("user.home"), ".runelite"); + static final File LOGS_DIR = new File(RUNELITE_DIR, "logs"); + static final File REPO_DIR = new File(RUNELITE_DIR, "repository2"); public static final File CRASH_FILES = new File(LOGS_DIR, "jvm_crash_pid_%p.log"); private static final String USER_AGENT = "RuneLite/" + LauncherProperties.getVersion(); static final String LAUNCHER_EXECUTABLE_NAME_WIN = "RuneLite.exe"; @@ -290,12 +290,26 @@ public static void main(String[] args) } } + // fix up permissions before potentially removing the RUNASADMIN compat key + if (FilesystemPermissions.check()) + { + // check() opens an error dialog + return; + } + if (JagexLauncherCompatibility.check()) { // check() opens an error dialog return; } + if (!REPO_DIR.exists() && !REPO_DIR.mkdirs()) + { + log.error("unable to create directory {}", REPO_DIR); + SwingUtilities.invokeLater(() -> new FatalErrorDialog("Unable to create RuneLite directory " + REPO_DIR.getAbsolutePath() + ". Check your filesystem permissions are correct.").open()); + return; + } + SplashScreen.stage(.05, null, "Downloading bootstrap"); Bootstrap bootstrap; try @@ -331,13 +345,6 @@ public static void main(String[] args) // update packr vmargs to the launcher vmargs from bootstrap. PackrConfig.updateLauncherArgs(bootstrap); - if (!REPO_DIR.exists() && !REPO_DIR.mkdirs()) - { - log.error("unable to create repo directory {}", REPO_DIR); - SwingUtilities.invokeLater(() -> new FatalErrorDialog("Unable to create RuneLite directory " + REPO_DIR.getAbsolutePath() + ". Check your filesystem permissions are correct.").open()); - return; - } - // Determine artifacts for this OS List artifacts = Arrays.stream(bootstrap.getArtifacts()) .filter(a -> @@ -987,4 +994,9 @@ private static void initDllBlacklist() static native boolean regDeleteValue(String key, String subKey, String value); static native boolean isProcessElevated(long pid); + + static native void setFileACL(String folder, String[] sids); + static native String getUserSID(); + + static native long runas(String path, String args); }