Skip to content

Commit

Permalink
add filesystem permissions check
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
Adam- and YvesW committed Mar 17, 2024
1 parent 4ef6ed7 commit d18cd88
Show file tree
Hide file tree
Showing 6 changed files with 385 additions and 14 deletions.
2 changes: 1 addition & 1 deletion liblauncher/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
113 changes: 113 additions & 0 deletions liblauncher/acl.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
#include <Windows.h>
#include <aclapi.h>
#include <jni.h>
#include <sddl.h>

#include <memory>

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<jstring>(env->GetObjectArrayElement(sidsJa, i));
const jchar *sid = env->GetStringChars(sidJs, nullptr);

PSID pSid = NULL;
if (!ConvertStringSidToSidW(reinterpret_cast<LPCWSTR>(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<LPCWSTR>(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<char[]> 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<const jchar *>(pstrSid), static_cast<jsize>(wcslen(pstrSid)));

LocalFree(pstrSid);

return ret;
}
17 changes: 15 additions & 2 deletions liblauncher/elevation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
#include <jni.h>

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;
}
Expand All @@ -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<const wchar_t *>(pathString), reinterpret_cast<const wchar_t *>(argsString),
nullptr, SW_SHOWNORMAL);

env->ReleaseStringChars(pathObj, pathString);
env->ReleaseStringChars(argsObj, argsString);

return static_cast<jlong>(ret);
}
2 changes: 1 addition & 1 deletion liblauncher/reg.cpp
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#include <Windows.h>
#include <jni.h>

static void rlThrow(JNIEnv *env, const char *msg) {
void rlThrow(JNIEnv *env, const char *msg) {
if (env->ExceptionCheck()) {
return;
}
Expand Down
233 changes: 233 additions & 0 deletions src/main/java/net/runelite/launcher/FilesystemPermissions.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
/*
* Copyright (c) 2024, Adam <[email protected]>
* 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);
}
}
Loading

0 comments on commit d18cd88

Please sign in to comment.