Песочница против вредоносного кода в приложении Java

92

В среде сервера моделирования, где пользователям разрешено отправлять свой собственный код для запуска на сервере, было бы явно выгодно, чтобы любой отправленный пользователем код выполнялся в изолированной программной среде, в отличие от апплетов внутри браузера. Я хотел иметь возможность использовать саму JVM, а не добавлять еще один уровень виртуальной машины для изоляции этих представленных компонентов.

Такое ограничение представляется возможным при использовании существующей модели песочницы Java, но существует ли динамический способ включить это только для отправленных пользователем частей работающего приложения?

Алан Крюгер
источник

Ответы:

110
  1. Запустите ненадежный код в отдельном потоке. Это, например, предотвращает проблемы с бесконечными циклами и т. Д. И упрощает дальнейшие шаги. Попросите основной поток дождаться завершения потока, а если занимает слишком много времени, убейте его с помощью Thread.stop. Thread.stop устарел, но поскольку ненадежный код не должен иметь доступа к каким-либо ресурсам, его можно безопасно убить.

  2. Установите SecurityManager в этом потоке. Создайте подкласс SecurityManager, который переопределяет checkPermission (Permission perm), чтобы просто выбросить SecurityException для всех разрешений, кроме нескольких избранных. Здесь есть список методов и необходимых разрешений: Разрешения в Java TM 6 SDK .

  3. Используйте собственный ClassLoader для загрузки ненадежного кода. Ваш загрузчик классов будет вызываться для всех классов, которые использует ненадежный код, поэтому вы можете делать такие вещи, как отключение доступа к отдельным классам JDK. Что нужно сделать, это создать белый список разрешенных классов JDK.

  4. Возможно, вы захотите запустить ненадежный код в отдельной JVM. Хотя предыдущие шаги сделали бы код безопасным, есть одна неприятная вещь, которую изолированный код все еще может делать: выделять как можно больше памяти, что приводит к увеличению видимого следа основного приложения.

JSR 121: Спецификация API изоляции приложений была разработана для решения этой проблемы, но, к сожалению, еще не реализована.

Это довольно подробная тема, и я в основном пишу все это не в своей голове.

Но в любом случае какой-то несовершенный, рискованный, возможно, ошибочный (псевдо) код:

ClassLoader

class MyClassLoader extends ClassLoader {
  @Override
  public Class<?> loadClass(String name) throws ClassNotFoundException {
    if (name is white-listed JDK class) return super.loadClass(name);
    return findClass(name);
  }
  @Override
  public Class findClass(String name) {
    byte[] b = loadClassData(name);
    return defineClass(name, b, 0, b.length);
  }
  private byte[] loadClassData(String name) {
    // load the untrusted class data here
  }
}

Менеджер по безопасности

class MySecurityManager extends SecurityManager {
  private Object secret;
  public MySecurityManager(Object pass) { secret = pass; }
  private void disable(Object pass) {
    if (pass == secret) secret = null;
  }
  // ... override checkXXX method(s) here.
  // Always allow them to succeed when secret==null
}

Нить

class MyIsolatedThread extends Thread {
  private Object pass = new Object();
  private MyClassLoader loader = new MyClassLoader();
  private MySecurityManager sm = new MySecurityManager(pass);
  public void run() {
    SecurityManager old = System.getSecurityManager();
    System.setSecurityManager(sm);
    runUntrustedCode();
    sm.disable(pass);
    System.setSecurityManager(old);
  }
  private void runUntrustedCode() {
    try {
      // run the custom class's main method for example:
      loader.loadClass("customclassname")
        .getMethod("main", String[].class)
        .invoke(null, new Object[]{...});
    } catch (Throwable t) {}
  }
}
вакас
источник
4
Этот код может потребовать доработки. Вы не можете действительно защититься от доступности JVM. Будьте готовы убить процесс (возможно, автоматически). Код попадает в другие потоки - например, в поток финализатора. Thread.stopвызовет проблемы в коде библиотеки Java. Точно так же для кода библиотеки Java потребуются разрешения. Намного лучше позволить SecurityManagerиспользовать java.security.AccessController. Загрузчик классов, вероятно, также должен разрешать доступ к собственным классам пользовательского кода.
Том Хотин - tackline
4
Учитывая, что это такая сложная тема, нет ли существующих решений для безопасной работы с «плагинами» Java?
Ник Спейсек
10
Проблема этого подхода в том, что когда вы устанавливаете SecurityManager на System, это не только влияет на работающий поток, но также влияет на другой поток!
Gelin Luo
2
Извините, но thread.stop () можно поймать с помощью throwable. Вы можете while (thread.isAlive) Thread.stop (), но тогда я могу рекурсивно вызвать функцию, которая перехватывает исключение. Протестировано на моем компьютере, рекурсивная функция выигрывает у stop (). Теперь у вас есть мусорная ветка,
ворующая ЦП
9
Помимо того факта, что System.setSecurityManager(…)это повлияет на всю JVM, а не только на поток, вызывающий этот метод, идея принятия решений безопасности на основе потока была оставлена, когда Java переключилась с 1.0 на 1.1. В это время было признано, что ненадежный код может вызывать доверенный код и наоборот, независимо от того, какой поток выполняет код. Ни один разработчик не должен повторять ошибку.
Хольгер
18

Очевидно, что такая схема вызывает всевозможные проблемы безопасности. Java имеет строгую структуру безопасности, но это нетривиально. Нельзя упускать из виду возможность облажаться и позволить непривилегированному пользователю получить доступ к жизненно важным компонентам системы.

Помимо этого предупреждения, если вы вводите пользовательский ввод в виде исходного кода, первое, что вам нужно сделать, это скомпилировать его в байт-код Java. AFIAK, это невозможно сделать изначально, поэтому вам нужно сделать системный вызов javac и скомпилировать исходный код в байт-код на диске. Вот руководство, которое можно использовать в качестве отправной точки для этого. Изменить : как я узнал в комментариях, вы действительно можете скомпилировать код Java из исходного кода с помощью javax.tools.JavaCompiler

Если у вас есть байт-код JVM, вы можете загрузить его в JVM с помощью функции defineClass в ClassLoader . Чтобы установить контекст безопасности для этого загруженного класса, вам необходимо указать ProtectionDomain . Минимальный конструктор для ProtectionDomain требует как CodeSource, так и PermissionCollection . PermissionCollection - это объект, который вы здесь в первую очередь используете - вы можете использовать его, чтобы указать точные разрешения, которые имеет загруженный класс. Эти разрешения должны быть в конечном итоге обеспечены AccessController JVM .

Здесь много возможных ошибок, и вы должны быть очень осторожны, чтобы полностью понять все, прежде чем что-либо реализовывать.

shsmurfy
источник
2
Компиляция Java довольно проста с использованием JDK 6 javax.tools API.
Алан Крюгер,
10

Java-Sandbox библиотека для выполнения Java - кода с ограниченным набором разрешений. Его можно использовать для разрешения доступа только к набору классов и ресурсов из белого списка. Кажется, он не может ограничить доступ к отдельным методам. Для этого используется система с настраиваемым загрузчиком классов и менеджером безопасности.

Я не использовал его, но он выглядит хорошо продуманным и достаточно хорошо документированным.

@waqas дал очень интересный ответ, объясняющий, как это можно реализовать самостоятельно. Но гораздо безопаснее оставить такой критически важный для безопасности и сложный код экспертам.

Заметим, однако, что проект не обновлялся с 2013 года, и создатели называют его «экспериментальным». Его домашняя страница исчезла, но запись Source Forge осталась.

Пример кода, адаптированный с веб-сайта проекта:

SandboxService sandboxService = SandboxServiceImpl.getInstance();

// Configure context 
SandboxContext context = new SandboxContext();
context.addClassForApplicationLoader(getClass().getName());
context.addClassPermission(AccessType.PERMIT, "java.lang.System");

// Whithout this line we get a SandboxException when touching System.out
context.addClassPermission(AccessType.PERMIT, "java.io.PrintStream");

String someValue = "Input value";

class TestEnvironment implements SandboxedEnvironment<String> {
    @Override
    public String execute() throws Exception {
        // This is untrusted code
        System.out.println(someValue);
        return "Output value";
    }
};

// Run code in sandbox. Pass arguments to generated constructor in TestEnvironment.
SandboxedCallResult<String> result = sandboxService.runSandboxed(TestEnvironment.class, 
    context, this, someValue);

System.out.println(result.get());
Lii
источник
4

Чтобы решить проблему в принятом ответе, в соответствии с которым настройка SecurityManagerбудет применяться ко всем потокам в JVM, а не для отдельных потоков, вы можете создать настройку, SecurityManagerкоторая может быть включена / отключена для определенных потоков следующим образом:

import java.security.Permission;

public class SelectiveSecurityManager extends SecurityManager {

  private static final ToggleSecurityManagerPermission TOGGLE_PERMISSION = new ToggleSecurityManagerPermission();

  ThreadLocal<Boolean> enabledFlag = null;

  public SelectiveSecurityManager(final boolean enabledByDefault) {

    enabledFlag = new ThreadLocal<Boolean>() {

      @Override
      protected Boolean initialValue() {
        return enabledByDefault;
      }

      @Override
      public void set(Boolean value) {
        SecurityManager securityManager = System.getSecurityManager();
        if (securityManager != null) {
          securityManager.checkPermission(TOGGLE_PERMISSION);
        }
        super.set(value);
      }
    };
  }

  @Override
  public void checkPermission(Permission permission) {
    if (shouldCheck(permission)) {
      super.checkPermission(permission);
    }
  }

  @Override
  public void checkPermission(Permission permission, Object context) {
    if (shouldCheck(permission)) {
      super.checkPermission(permission, context);
    }
  }

  private boolean shouldCheck(Permission permission) {
    return isEnabled() || permission instanceof ToggleSecurityManagerPermission;
  }

  public void enable() {
    enabledFlag.set(true);
  }

  public void disable() {
    enabledFlag.set(false);
  }

  public boolean isEnabled() {
    return enabledFlag.get();
  }

}

ToggleSecurirtyManagerPermission- это простая реализация, java.security.Permissionгарантирующая, что только авторизованный код может включать / отключать диспетчер безопасности. Выглядит это так:

import java.security.Permission;

public class ToggleSecurityManagerPermission extends Permission {

  private static final long serialVersionUID = 4812713037565136922L;
  private static final String NAME = "ToggleSecurityManagerPermission";

  public ToggleSecurityManagerPermission() {
    super(NAME);
  }

  @Override
  public boolean implies(Permission permission) {
    return this.equals(permission);
  }

  @Override
  public boolean equals(Object obj) {
    if (obj instanceof ToggleSecurityManagerPermission) {
      return true;
    }
    return false;
  }

  @Override
  public int hashCode() {
    return NAME.hashCode();
  }

  @Override
  public String getActions() {
    return "";
  }

}
альфа-петля
источник
3
Ссылаясь на ваши (собственные) источники: alphaloop.blogspot.com/2014/08/… и github.com/alphaloop/selective-security-manager .
ziesemer
Очень разумное использование ThreadLocal, чтобы сделать диспетчеры безопасности с системной областью действия эффективно распределенными по потокам (что хотелось бы большинству пользователей). Также рассмотрите возможность использования InheritableThreadLocal для автоматической передачи запрещенного свойства потокам, порожденным ненадежным кодом.
Ник
4

Что ж, уже очень поздно давать какие-либо предложения или решения, но все же я столкнулся с подобной проблемой, более ориентированной на исследования. В основном я пытался обеспечить предоставление и автоматическую оценку заданий по программированию для курса Java на платформах электронного обучения.

  1. одним из способов может быть создание отдельных виртуальных машин (не JVM), а реальных виртуальных машин с минимально возможной конфигурацией ОС для каждого студента.
  2. Установите JRE для Java или библиотеки в соответствии с вашими языками программирования, в зависимости от того, что вы хотите, чтобы студенты компилировали и выполняли на этих машинах.

Я знаю, что это звучит довольно сложно и требует большого количества задач, но Oracle Virtual Box уже предоставляет Java API для динамического создания или клонирования виртуальных машин. https://www.virtualbox.org/sdkref/index.html (обратите внимание, даже VMware также предоставляет API для того же)

А для получения информации о минимальном размере и конфигурации дистрибутива Linux вы можете обратиться к этому здесь http://www.slitaz.org/en/ ,

Итак, теперь, если ученик испортит или попытается это сделать, может быть, с памятью, файловой системой или сетью, сокетом, максимум он может повредить свою собственную виртуальную машину.

Также внутри этих виртуальных машин вы можете обеспечить дополнительную безопасность, такую ​​как Sandbox (менеджер безопасности) для Java или создать учетные записи для конкретных пользователей в Linux и, таким образом, ограничить доступ.

Надеюсь это поможет !!

Шрикант Хэвале
источник
3

Вот поточно-ориентированное решение проблемы:

https://svn.code.sf.net/p/loggifier/code/trunk/de.unkrig.commons.lang/src/de/unkrig/commons/lang/security/Sandbox.java

package de.unkrig.commons.lang.security;

import java.security.AccessControlContext;
import java.security.Permission;
import java.security.Permissions;
import java.security.ProtectionDomain;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.WeakHashMap;

import de.unkrig.commons.nullanalysis.Nullable;

/**
 * This class establishes a security manager that confines the permissions for code executed through specific classes,
 * which may be specified by class, class name and/or class loader.
 * <p>
 * To 'execute through a class' means that the execution stack includes the class. E.g., if a method of class {@code A}
 * invokes a method of class {@code B}, which then invokes a method of class {@code C}, and all three classes were
 * previously {@link #confine(Class, Permissions) confined}, then for all actions that are executed by class {@code C}
 * the <i>intersection</i> of the three {@link Permissions} apply.
 * <p>
 * Once the permissions for a class, class name or class loader are confined, they cannot be changed; this prevents any
 * attempts (e.g. of the confined class itself) to release the confinement.
 * <p>
 * Code example:
 * <pre>
 *  Runnable unprivileged = new Runnable() {
 *      public void run() {
 *          System.getProperty("user.dir");
 *      }
 *  };
 *
 *  // Run without confinement.
 *  unprivileged.run(); // Works fine.
 *
 *  // Set the most strict permissions.
 *  Sandbox.confine(unprivileged.getClass(), new Permissions());
 *  unprivileged.run(); // Throws a SecurityException.
 *
 *  // Attempt to change the permissions.
 *  {
 *      Permissions permissions = new Permissions();
 *      permissions.add(new AllPermission());
 *      Sandbox.confine(unprivileged.getClass(), permissions); // Throws a SecurityException.
 *  }
 *  unprivileged.run();
 * </pre>
 */
public final
class Sandbox {

    private Sandbox() {}

    private static final Map<Class<?>, AccessControlContext>
    CHECKED_CLASSES = Collections.synchronizedMap(new WeakHashMap<Class<?>, AccessControlContext>());

    private static final Map<String, AccessControlContext>
    CHECKED_CLASS_NAMES = Collections.synchronizedMap(new HashMap<String, AccessControlContext>());

    private static final Map<ClassLoader, AccessControlContext>
    CHECKED_CLASS_LOADERS = Collections.synchronizedMap(new WeakHashMap<ClassLoader, AccessControlContext>());

    static {

        // Install our custom security manager.
        if (System.getSecurityManager() != null) {
            throw new ExceptionInInitializerError("There's already a security manager set");
        }
        System.setSecurityManager(new SecurityManager() {

            @Override public void
            checkPermission(@Nullable Permission perm) {
                assert perm != null;

                for (Class<?> clasS : this.getClassContext()) {

                    // Check if an ACC was set for the class.
                    {
                        AccessControlContext acc = Sandbox.CHECKED_CLASSES.get(clasS);
                        if (acc != null) acc.checkPermission(perm);
                    }

                    // Check if an ACC was set for the class name.
                    {
                        AccessControlContext acc = Sandbox.CHECKED_CLASS_NAMES.get(clasS.getName());
                        if (acc != null) acc.checkPermission(perm);
                    }

                    // Check if an ACC was set for the class loader.
                    {
                        AccessControlContext acc = Sandbox.CHECKED_CLASS_LOADERS.get(clasS.getClassLoader());
                        if (acc != null) acc.checkPermission(perm);
                    }
                }
            }
        });
    }

    // --------------------------

    /**
     * All future actions that are executed through the given {@code clasS} will be checked against the given {@code
     * accessControlContext}.
     *
     * @throws SecurityException Permissions are already confined for the {@code clasS}
     */
    public static void
    confine(Class<?> clasS, AccessControlContext accessControlContext) {

        if (Sandbox.CHECKED_CLASSES.containsKey(clasS)) {
            throw new SecurityException("Attempt to change the access control context for '" + clasS + "'");
        }

        Sandbox.CHECKED_CLASSES.put(clasS, accessControlContext);
    }

    /**
     * All future actions that are executed through the given {@code clasS} will be checked against the given {@code
     * protectionDomain}.
     *
     * @throws SecurityException Permissions are already confined for the {@code clasS}
     */
    public static void
    confine(Class<?> clasS, ProtectionDomain protectionDomain) {
        Sandbox.confine(
            clasS,
            new AccessControlContext(new ProtectionDomain[] { protectionDomain })
        );
    }

    /**
     * All future actions that are executed through the given {@code clasS} will be checked against the given {@code
     * permissions}.
     *
     * @throws SecurityException Permissions are already confined for the {@code clasS}
     */
    public static void
    confine(Class<?> clasS, Permissions permissions) {
        Sandbox.confine(clasS, new ProtectionDomain(null, permissions));
    }

    // Code for 'CHECKED_CLASS_NAMES' and 'CHECKED_CLASS_LOADERS' omitted here.

}

Прокомментируйте, пожалуйста!

CU

Арно

Арно Ункриг
источник