Содержание статьи
Java предоставляет два основных генератора псевдослучайных последовательностей (PRNG): java.util.Random
— криптографически нестойкий, но выдающий равномерно распределенную последовательность, и java.security.SecureRandom
— криптографически стойкий, поэтому может использоваться в реализации стойкой криптографии, например для генерации ключей. Поскольку Java широко используется, эти генераторы часто встречаются в реальных приложениях.
Внутреннее состояние PRNG изменяется функцией перехода (Ft) при вычислении каждого элемента выходной последовательности, а функция выхода (Fo) преобразует внутреннее состояние в выходные элементы S0, S1, ..., Sn
. Из общей схемы работы PRNG можно отметить следующие потенциальные уязвимости:
- Если внутреннее состояние относительно небольшое, можно просто перебрать все возможные комбинации, а зная внутреннее состояние и конструкцию PRNG (здесь и далее предполагается, что конструкция PRNG известна), можно воспроизвести его выходную последовательность.
- Если на основании выходной последовательности можно сделать предположения о внутреннем состоянии, то это сокращает перебор.
- Если для инициализации PRNG использовался слабый инициализатор (
seed
), то, имея выходную последовательность, можно подобратьseed
(еслиseed
короче внутреннего состояния, проще перебирать его).
Теория java.util.Random
Random
— линейный конгруэнтный генератор, имеющий линейную функцию перехода Ft. Внутреннее состояние изменяется так: Statei + 1 = A * Statei + C mod M
, где A = 0x5deece66d (A = 25 214 903 917)
, C = 11
и M = 248
. Длина внутреннего состояния — 48 бит. Начальное внутреннее состояние (State0) получается из seed следующим образом: A ⊕ seed mod M
, где ⊕
— битовый XOR. Механизм инициализации Random
будет рассмотрен далее.
Выходная последовательность генератора представляет собой либо модуль внутреннего состояния (иногда такой подход называют «модулярным» или «взятие снизу»), либо битово сдвинутое вправо произведение («мультипликативный», или «взятие сверху»). В зависимости от используемого метода и их параметров Random
использует оба подхода.
Далее будут рассмотрены атаки на внутреннее состояние, когда имеется выходная последовательность.
Метод nextInt(). Как видно из листинга, старшие 32 бита внутреннего состояния идут без изменения в выход, поэтому для восстановления внутреннего состояния необходимо перебрать только 16 младших бит. На ноутбуке с четырехъядерным Intel i5 (все дальнейшие оценки — на этом же ноутбуке) это занимает менее секунды.
protected int next(int bits) {
long oldseed, nextseed;
AtomicLong seed = this.seed;
do {
oldseed = seed.get();
nextseed = (oldseed * multiplier + addend) & mask;
} while (!seed.compareAndSet(oldseed, nextseed));
return (int)(nextseed >>> (48 - bits));
}
public int nextInt() {
return next(32);
}
Метод nextLong(). В данном случае внутреннее состояние изменяется дважды для генерации одного выходного элемента. Сценарий атаки здесь полностью повторяет nextInt()
. Как и прежде, атака занимает менее секунды.
protected int next(int bits) {
long oldseed, nextseed;
AtomicLong seed = this.seed;
do {
oldseed = seed.get();
nextseed = (oldseed * multiplier + addend) & mask;
} while (!seed.compareAndSet(oldseed, nextseed));
return (int)(nextseed >>> (48 - bits));
}
public long nextLong() {
return ((long)(next(32)) << 32) + next(32);
}
Метод nextInt(limit), где limit четное (но не степень 2). Атака на этот сценарий была известна ранее (goo.gl/uNl2sG, goo.gl/rLzE2g) и основана на использовании генератора с сокращенным внутренним состоянием для восстановления сначала младших 17 бит внутреннего состояния основного генератора, а остальное — перебором.
protected int next(int bits) {
long oldseed, nextseed;
AtomicLong seed = this.seed;
do {
oldseed = seed.get();
nextseed = (oldseed * multiplier + addend) & mask;
} while (!seed.compareAndSet(oldseed, nextseed));
return (int)(nextseed >>> (48 - bits));
}
public int nextInt(int n) {
if (n <= 0) throw new IllegalArgumentException("n must be positive"); if ((n & -n) == n) // i.e., n is a power of 2 return (int)((n * (long)next(31)) >> 31);
int bits, val;
do {
bits = next(31);
val = bits % n;
} while (bits - val + (n-1) < 0);
return val;
}
Четный limit
может быть представлен как n * 2p
, где n
— нечетное и p ≥ 1
. Генератор с сокращенным состоянием длины p + 17
(субгенератор) будет выдавать последовательность, где каждый элемент (Si′) однозначно связан с известной выходной последовательностью основного генератора: S0′ = S0 mod 2p, S1′ = S1 mod 2p, …, Sn′ = Sn mod 2p
. Если принять во внимание этот факт, корректные 17 бит внутреннего состояния могут быть восстановлены. На следующем шаге 31 старший бит внутреннего состояния подбирается в виде H31bits = (S0 + J * limit) mod 231
, то есть с шагом limit
. Описанные два шага взлома занимают не больше двух секунд.
Метод nextInt(limit) где limit — степень 2. В данном случае используется «мультипликативный» подход к формированию выхода.
protected int next(int bits) {
long oldseed, nextseed;
AtomicLong seed = this.seed;
do {
oldseed = seed.get();
nextseed = (oldseed * multiplier + addend) & mask;
} while (!seed.compareAndSet(oldseed, nextseed));
return (int)(nextseed >>> (48 - bits));
}
public int nextInt(int n) {
if (n <= 0) throw new IllegalArgumentException("n must be positive"); if ((n & -n) == n) // i.e., n is a power of 2 return (int)((n * (long)next(31)) >> 31);
int bits, val;
do {
bits = next(31);
val = bits % n;
} while (bits - val + (n-1) < 0);
return val;
}
Если внутреннее состояние пробовать взломать полным перебором, подбирая его в виде X = (S0 * 248 – p + t) mod 248
, необходимо перебрать все возможные t[0; 248 – p – 1]
. Но при анализе зависимости выхода Si от разных t было обнаружено, что Si изменяется на 1 mod limit
только когда t увеличится на некоторое число c
такое, что 213 – p < c < 214 – p, c ~ 213,44644 – p
. Такое поведение объясняется алгоритмом изменения внутреннего состояния и получения из него выхода: предыдущее внутреннее состояние умножается на большое целое A = 0x5deece66dL (~234,55)
, что эквивалентно сдвигу влево на ~34 бита, поэтому внутреннее состояние должно быть изменено более чем с
раз, прежде чем это отразится в выходе.
Знание зависимости выходной последовательности от t позволяет при переборе пропускать значения t, которые будут давать на выходе известные Si, что в общем случае при переборе позволяет пропускать (limit – 1) * c
значений t и сокращает сложность перебора с O(248 – p)
до O(248 – 2p)
. Например, если limit = 64
, сложность перебора составит ~236 вместо ~242.
Метод nextInt(limit), где limit нечетное. В данном случае используется «модульный» подход к формированию выхода.
protected int next(int bits) {
long oldseed, nextseed;
AtomicLong seed = this.seed;
do {
oldseed = seed.get();
nextseed = (oldseed * multiplier + addend) & mask;
} while (!seed.compareAndSet(oldseed, nextseed));
return (int)(nextseed >>> (48 - bits));
}
public int nextInt(int n) {
if (n <= 0) throw new IllegalArgumentException("n must be positive"); if ((n & -n) == n) // i.e., n is a power of 2 return (int)((n * (long)next(31)) >> 31);
int bits, val;
do {
bits = next(31);
val = bits % n;
} while (bits - val + (n-1) < 0);
return val;
}
Искать внутреннее состояние будем в виде X = (217H31bits + L17bits) mod 248
, где H31bits — старшие 31 и L17bits — младшие 17 бит. С учетом алгоритма работы генератора H31bits можно искать в следующем виде: H31bits = (S0 + J * limit) mod 231
, где S0 — первый элемент имеющейся выходной последовательности, то есть перебором через limit, а L17bits «в лоб» пришлось бы подбирать с шагом 1.
Однако при анализе зависимости выхода Si от L17bits при увеличении на 1 была обнаружена зависимость, показанная на рис. 7.
Элементы выходной последовательности генератора могут быть записаны в виде матрицы, имеющей d
столбцов и 217/d
строк, где d = min i:(A*i) >> 17 = (limit – 1) mod limit
, а A = 0x5deece66d
— то же A, что используется при смене внутреннего состояния генератора. В каждом столбце матрицы значения изменяются определенным образом: следующий элемент остается неизменным, либо уменьшается на 1 mod limit
, либо изменяется на p mod limit
, либо на (p + 1) mod limit
, где p = 231 mod limit
.
Изменение на p
и p + 1
происходит периодически, и период может быть рассчитан: period = 231 / (A d >> 17)
, где, как и прежде, A = 0x5deece66d
.
С учетом этой зависимости на первом шаге атаки необходимо предварительно рассчитать значения L17bit, где выход изменяется на p
или p + 1
— таких значений будет 217 / (d * period)
. На следующем шаге будем перебирать значение L17bit, пропуская значения L17bit, заведомо не приводящие к выходу следующего элемента известной последовательности S1 (то есть не выдающие взламываемую последовательность). В каждом столбце необходимо выполнить проверку 217 / (d * limit)
значений L17bit, вместо 217/d
. Сложность взлома с помощью указанного алгоритма составляет O(248/period)
вместо O(248/limit)
в случае полного перебора, что может быть значительно эффективнее, если period > limit
.
Инициализация Random. В JDK для инициализации используется время с момента старта системы в наносекундах (System.nanoTime()
). В GNU Classpath конструктор по умолчанию использует время в миллисекундах с начала эпохи (System.currentTimeMillis()
). Знания об инициализации PRNG позволяют эффективнее проводить взлом.
Основываясь на идеях, описанных выше, мы разработали утилиту командной строки JavaCG для эффективного взлома генераторов Random
по имеющейся выходной последовательности. Утилита написана на С++11 и работает под Windows и Linux, доступна на GitHub.
Практическая эксплуатация java.util.Random
От теории к практике! Мы поискали Java-приложения, доступные на sourceforge.net и использующие Random
для целей безопасности, и обнаружили множество парольных менеджеров с функциональностью генерации паролей. Например:
- MyPasswords;
- Mass Password Generator;
- PasswordGenerator;
- Java Password Generator;
- Safe Password Generator.
Если злоумышленнику известен один пароль, сгенерированный перечисленными инструментами, он может восстановить внутреннее состояние генератора — экземпляра Random
, что позволит предсказать последующие и предыдущие пароли, сгенерированные этим же генератором.
Далее мы поискали доступные в cети приложения, использующие Random
для сервисов безопасности. В результате был найден контейнер сервлетов Winstone. Jenkins (сервер непрерывной интеграции с открытым кодом, поддерживаемый на коммерческой основе CloudBees) и Hudson (также сервер непрерывной интеграции, поддерживаемый Eclipse Foundation) используют по умолчанию Winstone, который достаточно популярен, как можно видеть из рис. 9.
В Winstone Random
используется для генерации идентификатора сессии, алгоритм реализован в методе makeNewSession()
класса winstone.WinstoneRequest
. Логика генерации ID сессии следующая: фиксированная строка "Winstone"
конкатенируется с IP-адресом клиента, портом сервера, временем генерации в миллисекундах и выходом nextLong()
экземпляра Random
, от полученной строки вычисляется MD5. PRNG инициализируется временем в миллисекундах с начала эпохи во время старта сервера.
Время в миллисекундах дает 10 бит энтропии, поскольку атакующий может получить время в секундах из заголовка Date HTTP-ответа. Единственное, что неизвестно — количество миллисекунд.
Следующее, что нужно угадать атакующему — значение выхода nextLong()
. Для воспроизведения выходной последовательности необходимо знать seed
, которым был инициализирован генератор, — энтропия 14 бит, и количество сгенерированных случайных чисел с момента старта генератора (это то же, что и количество сессий с сервером с момента старта Winstone — энтропия log2(количество сгенерированных сессий)
). Логично предположить, что Winstone стартует вместе с запуском сервера, поэтому инициализирующее генератор значение времени будет близко к времени его работы. Можно найти разные способы определения времени работы сервера, например в Linux можно использовать значение TCP timestamp
, рекомендованное RFC1323. TCP timestamp
— это 32-битное значение (timestamp clock) в опциях TCP, используемое для корректировки интервала RTO (retransmission timeout) и в механизме PAWS (Protection Against Wrapping Sequence). Timestamp clock
инициализируется известным значением во время старта сервера и инкрементируется с фиксированной частотой. Эта функция включена по умолчанию на большинстве дистрибутивов Linux. Например, в Ubuntu 12.04 LTS timestamp clock инициализируется значением –300 и частота инкрементирования составляет 1000 Гц. Итак, можно оценить время работы сервера, имея значение timestamp clock
, а само TCP timestamp
можно узнать Nmap’ом с ключом –O
.
Предполагаем, что атакующему известен IP-адрес жертвы или объем перебора небольшой, поэтому можно пренебречь энтропией IP-адреса.
Итого, общая энтропия ID сессии 24 + log2(количество сгенерированных сессий)
. Если даже на момент атаки был сформирован миллион сессий, общая энтропия не составит 44 бита, что сравнительно немного и может быть перебрано «в лоб» на практике. Атака реализуется следующим образом:
- Атакующий получает новый ID сессии; из заголовка HTTP Date извлекает время в секундах и использует его для оценки времени генерации ID сессии в миллисекундах.
- Атакующий оценивает время работы сервера, например с помощью описанной выше техники TCP timestamp.
- Атакующий в офлайне подбирает внутреннее состояние PRNG.
- Атакующий начинает периодически обращаться на сервер, получая новые ID сессии, определяя по ним, на сколько изменилось внутреннее состояние PRNG: если обнаружено изменение на более чем одно состояние, значит, между нашими периодическими подключениями кто-то еще подключился и для него был сгенерирован ID сессии; атакующий сохраняет значение внутреннего состояния PRNG и период, в течение которого была произведена генерация сессии.
- Узнав каким-либо образом IP-адрес подключившегося, атакующий подбирает его ID сессии.
Сценарий атаки представлен на рис. 11, скрипты для взлома приведены в GitHub.
Видео с демонстрацией атаки доступно здесь. В настоящий момент уязвимость CVE-2014-2060 исправлена.
Теория java.security.SecureRandom
SecureRandom
наследован от Random
, использует детерминированный алгоритм для формирования псевдослучайной последовательности из истинно случайного seed
. В SecureRandom
реализована неочевидная логика работы, зависящая от операционной системы, параметров -Djava.security.egd
, securerandom.source
и механизма инициализации. Чтобы использовать SecureRandom
безопасно, нужно хорошо разбираться во всех этих особенностях.
Провайдер JCE по умолчанию, sun.security.provider.Sun
, имеет две реализации SecureRandom
: NativePRNG
и SHA1PRNG
. Реализация NativePRNG
используется по умолчанию в Linux и Solaris, его выход представляет собой значение SHA1PRNG
, сложенное (XOR) с байтами из /dev/random
или /dev/urandom
, поэтому создание экземпляра SecureRandom
с фиксированным seed
вполне безопасно и не приводит к проблемам с производительностью. NativePRNG
используется, когда securerandom.source=file:/dev/urandom
(по умолчанию) или file:/dev/random
.
Следующей реализацией SecureRandom
является SHA1PRNG
— детерминированный генератор, использующий SHA1 для формирования выхода из его внутреннего состояния. По умолчанию применяется в Windows.
Инициализация SHA1PRNG
может быть явной и неявной. По умолчанию поддерживаются три варианта неявной инициализации (получения seed
): NativeSeedGenerator
, URLSeedGenerator
, ThreadedSeedGenerator
. Какой из них будет использоваться — зависит от операционной системы и параметра securerandom.source. Байты seed
, полученные одним из трех способов, конкатенируются с байтами из функции getSystemEntropy
и затем результат перемешивается с помощью SHA1: State0= SHA1(getSystemEntropy() || seed)
, где || означает конкатенацию.
Реализация NativeSeedGenerator
работает в случае, если securerandom.source=file:/dev/urandom
или file:/dev/random
. В Linux и Solaris эта реализация читает байты из /dev/random
, а в Windows используется Microsoft CryptoAPI
.
URLSeedGenerator
работает, когда securerandom.source!=file:/dev/urandom
или file:/dev/random
. Он просто читает байты из указанного источника. Реализация ThreadedSeedGenerator
работает, если параметр securerandom.source
не указан и использует несколько потоков исполнения (threads
).
Явная инициализация SHA1PRNG
происходит при вызове либо конструктора SecureRandom(byte[] seed)
(при этом State0 будет присвоено SHA1(seed)
), либо методов setSeed(long seed)
или setSeed(byte[] seed)
— здесь Statei будет присвоено значение Statei ⊕ seed
.
Важно отметить, что в Linux и Solaris рекомендуется изменять значение по умолчанию securerandom.source
ввиду проблем с производительностью: по умолчанию при неявной инициализации NativeSeedGenerator
читает байты из /dev/random
, а он блокируется при нехватке энтропии, что приводит к зависанию приложения.
Теперь, когда мы знаем теорию работы SecureRandom
, пора перейти к практике. Возможны два случая небезопасного использования SecureRandom
:
Вариант 1. SecureRandom
инициализируется через конструктор значением seed
с малой энтропией (обычно временем):
SecureRandom rng = new SecureRandom((new Date()).toString().getBytes());
Вариант 2. SecureRandom
инициализируется методом setSeed
значением seed
с малой энтропией перед вызовом nextBytes
:
SecureRandom rng = new SecureRandom();
rng.setSeed(System.currentTimeMillis());
byte[] randomBytes = new byte[20];
rng.nextBytes(randomBytes);
Практическая эксплуатация небезопасного использования SecureRandom
Первым примером приложения, использующего SecureRandom
небезопасно, будет Tiny Java Web Server — небольшой быстрый контейнер сервлетов, способный работать на Android и Blackberry, написанный Дмитрием Рогаткиным. SecureRandom
здесь используется для генерации ID сессии и инициализируется временем в секундах. В году всего ~225 с, поэтому энтропия такой инициализации очень мала. Эта уязвимость может применяться для перехвата пользовательской сессии (session hijacking). С помощью Shodan можно обнаружить сервисы дистанционного банковского обслуживания, использующие RestEasy и TJWS, а так же была обнаружена MetricStream — GRC-система, использующая TJWS.
srandom = new SecureRandom((arguments.get(ARG_SESSION_SEED) == null ? "TJWS" + new Date() : (String) arguments.get(ARG_SESSION_SEED)).getBytes());
synchronized String generateSessionId() {
srandom.nextBytes(uniqer);
// TODO swap randomly bytes
return Utils.base64Encode(uniqer);
}
Следующим примером будет Oracle WebLogic — сервер приложений Java EE, у которого есть коннектор WTC — WebLogic Tuxedo connector к серверу приложений Tuxedo, используемому для приложений на C, C++, Cobol. WTC реализует протокол LLE — Link-Level encryption для защиты соединения WebLogic с Tuxedo. Связка WebLogic — Tuxedo используется, например, при развертывании Oracle PeopleSoft.
Фрагменты кода класса weblogic.wtc.jatmi.tplle
, выполняющие инициализацию LLE:
private byte[] getMyPublicValue() throws Exception {
if ((this.g == null) || (this.p == null)) {
throw new Exception("must get parameters before paublic value");
}
if (rnd == null) {
rnd = new SecureRandom();
}
try {
rnd.setSeed(System.currentTimeMillis());
rnd.setSeed(Runtime.getRuntime().freeMemory());
rnd.setSeed(Runtime.getRuntime().totalMemory());
rnd.setSeed(System.getProperty("java.version", "default").getBytes());
rnd.setSeed(System.getProperty("java.vendor", "default").getBytes());
rnd.setSeed(System.getProperty("os.name", "default").getBytes());
rnd.setSeed(System.getProperty("os.version", "default").getBytes());
rnd.setSeed(System.getProperty("user.name", "default").getBytes());
rnd.setSeed(System.getProperty("user.dir", "default").getBytes());
rnd.setSeed(System.getProperty("user.home", "default").getBytes());
rnd.setSeed(System.getProperty("java.home", "default").getBytes());
rnd.setSeed(System.getProperty("java.class.path", "default").getBytes());
rnd.setSeed(System.currentTimeMillis());
} catch (Exception localException) {}
this.x1 = new BigInteger(128, rnd);
this.y1 = this.g.modPow(this.x1, this.p);
return unpad(this.y1.toByteArray());
}
BigInteger localBigInteger = this.y2.modPow(this.x1, this.p);
this.x1 = null;
byte[] arrayOfByte2 = unpad(localBigInteger.toByteArray());
localBigInteger = null;
for (j = 0; j < this.sendKey.length; j++) {
this.sendKey[j] = arrayOfByte2[j];
}
for (j = 0; j < this.recvKey.length; j++) {
this.recvKey[j] = arrayOfByte2[(arrayOfByte2.length / 2 + j)];
}
LLC использует алгоритм Диффи — Хеллмана (DH) для создания пары ключей RC4. Приватный ключ DH для WebLogic генерируется SecureRandom
, инициализируемым методом setSeed
. Случайность инициализации берется из времени в миллисекундах (10 бит энтропии) и количества свободного памяти в куче (10 бит энтропии). Интервал между вызовами currentTimeMillis()
в этой реализации не превышает одной миллисекунды, поэтому следующий вызов добавит лишь один бит энтропии. Оставшиеся параметры могут быть определены, если взять систему аналогичной конфигурации. Данная уязвимость позволяет атакующему, перехватившему публичный DH-ключ Tuxedo, расшифровать защищаемый трафик. Данная уязвимость была исправлена в Oracle Critical Patch Update за октябрь 2014 года.
Третьим интересным примером плохой инициализации SecureRandom
является JacORB — реализация ORB (Object Request Broker) в Java. CORBA используется для построения распределенных приложений, работающих на разных платформах. Серверы приложений JBoss и JOnAS содержат библиотеки JacORB, а протокол IIOP (Internet Inter-Orb Protocol) используется для взаимодействия. IIOP может защищаться с использованием SSL (SSLIOP). Вот как SSLIOP реализован в JacORB — фрагмент класса org.jacorb.security.ssl.sun_jsse.JSRandomImpl
:
public SecureRandom getSecureRandom() {
SecureRandom rnd = new SecureRandom();
rnd.setSeed(4711);
return rnd;
}
Фрагмент класса org.jacorb.security.ssl.sun_jsse.SSLSocketFactory
:
SSLContext ctx = SSLContext.getInstance("TLS");
ctx.init( (kfm == null)? null : kfm.getKeyManagers(),
trustManagers,
sslRandom.getSecureRandom() );
return ctx.getSocketFactory();
В классе JSRandomImpl
создается экземпляр SecureRandom
, инициализируется значением 4711 методом setSeed
и передается в экземпляр класса SSLContext
во время инициализации SSL с помощью метода init
. Чтобы понять, как SecureRandom
используется в SSLContext
, необходимо внимательно ознакомиться, как случайные числа используются в SSL в случае применения RSA OAEP.
Если SecureRadnom
плохо инициализирован на клиенте, атакующий, перехватывающий SSL-трафик, может расшифровать его. Эта атака демонстрируется здесь: с помощью ARP-спуфинга атакующий перехватывает трафик SSLIOP, извлекает элементы SSL handshake и передает их в скрипт compute-master.py, который вычисляет ключ. Затем ключ передается в Wireshark, который расшифровывает SSL.
GNU Classpath
GNU Classpath — свободная альтернативная реализация стандартной библиотеки классов Java 5. GNU Classpath используется свободными JVM (Java Virtual Machine), такими, например, как JamVM, SableVM, Kaffe, CACAO. Любопытно, что в SableVM SecureRandom неявно инициализируется с помощью вызова new Random(0), — это серьезная уязвимость всех приложений, работающих на SableVM. Широко известный дистрибутив Linux для анонимного интернет-серфинга Liberté Linux используется GNU Classpath и JamVM.
SecureRandom
в GNU Classpath реализован проще, чем в JDK. При неявной инициализации используется 32-байтное значение seed
, логика формирования которого реализована в классе SecureRandomAdapter
. Выходная последовательность представляет собой SHA512 от внутреннего состояния.
При инициализации GNU Classpath пытается извлечь случайность из следующих источников:
- из файла, заданного параметром
securerandom.source
в/usr/local/classpath/lib/security/classpath.security
; - из файла, заданного параметром командной строки
java.security.egd
; - при помощи метода
generateSeed
из классаjava.security.VMSecureRandom
.
Метод generateSeed
получает seed
, используя механизм генерации с помощью потоков выполнения (threads
): создаются восемь потоков (рабочие), которые инкрементируют свои внутренние счетчики, а другой поток (основной) запускает цикл из 32 итераций и на каждой из них вычисляет один байт seed путем сложения (XOR) значений счетчиков рабочих потоков. Затем основной поток вызывает Thread.yield()
, который информирует планировщик потоков, что вызывающий поток желает уступить свое процессорное время:
for (int i = offset; i < length; i++) {
buffer[i] = (byte) (spinner[0].value ^ spinner[1].value ^ spinner[2].value
^ spinner[3].value ^ spinner[4].value ^ spinner[5].value
^ spinner[6].value ^ spinner[7].value);
Thread.yield();
}
Проблема в том, что планировщик может игнорировать это «желание» потока и продолжить его исполнение.
Рис. 14 показывает, что происходит при вызове generateSeed
на системе с одним процессом с одним ядром, — все сгенерированные 32 байта одинаковы! На самом деле первый байт может отличаться, но остальные всегда одинаковы.
Для исследования такого поведения класс VMSecureRandom
был модифицирован для вывода значений счетчиков всех рабочих потоков (spinner) на каждой итерации. Рис. 15 показывает 10 из 32 итераций: видно, что счетчики рабочих потоков отличаются только на первой итерации, а все остальные остаются неизменными. Причина такого поведения в стремлении планировщика «честно» распределить процессорное время после первой итерации, и все последующие итерации он игнорирует просьбу основного потока отдать его процессорное время рабочим, поэтому цикл прокручивается 31 раз без изменения счетчиков рабочих процессов.
Если метод generateSeed
вызывается на системе с двумя процессорами, тогда все байты seed кажутся различными.
Но, вызвав generateSeed
модифицированного VMSecureRandom
, можно видеть, что счетчик только одного рабочего потока изменяется на всех итерациях, а счетчики остальных не меняются после первой итерации.
Если запустить параллельно какую-нибудь задачу, потребляющую процессорное время, то сформированный seed
вновь содержит одинаковые байты.
Если запустить модифицированный VMSecureRandom
параллельно с задачей, потребляющей процессор, видно, что счетчики рабочих потоков изменяются только на первой итерации. В общем случае сформированный seed
будет иметь 32 одинаковых байта.
Для демонстрации описанной выше уязвимости использован контейнер сервлетов Jetty, работающий на JamVM и GNU Classpath. В прошлом Jetty имел уязвимость перехвата сессии (CVE-2007-5614), поэтому сейчас там используется SecureRandom
для реализации SSL и генерации ID сессии.
Логика генерации ID сессии расположена в классе org.eclipse.jetty.server.session.AbstractSessionIdManager
: инициализация — в методе public void initRandom()
, непосредственно генерация — в методе public String newSessionId(HttpServletRequest request, long created)
. Пример ID сессии — JSESSIONID=1s3v0f1dneqcv1at4retb2nk0u
.
// Метод initRandom
_random = new SecureRandom();
_random.setSeed(_random.nextLong() ^ System.currentTimeMillis() ^ hashCode() ^ Runtime.getRuntime().freeMemory());
// Метод newSessionId
long r0 = _random.nextLong(); long r1 = _random.nextLong();
if (r0 < 0) r0 = -r0; if (r1 < 0) r1 = -r1;
id = Long().toString(r0,36) + Long().toString(r1,36);
Рассмотрим внимательнее процесс инициации.
_random.nextLong()
в реализации SecureRandom
в GNU Classpath дает 16 бит энтропии (здесь инициализация происходит через механизм потоков исполнения), для взлома атакующий может перебрать все возможные комбинации. System.currentTimeMillis()
— время в миллисекундах с начала эпохи дает 13 бит энтропии — атакующий может оценить это время, используя описанную ранее технику TCP timestamp
. Runtime.getRuntime().freeMemory()
— количество свободной памяти в байтах — дает 10 бит энтропии, которые атакующий может оценить, взяв систему аналогичной конфигурации. hashCode()
— адрес объекта в куче JVM, что дает 12 бит энтропии и может быть оценено на основании известного значения hashcode другого объекта, создаваемого во время старта сервера Jetty.
Для целей демонстрации класс AbstractSessionIdManager
был незначительно модифицирован: оставлена только часть _random.nextLong()
.
_random = new SecureRandom();
_random.setSeed(_random.nextLong());
Демонстрация атаки приведена в YouTube, а используемый скрипт доступен на GitHub.
Данную уязвимость можно исправить, заменив вызов Thread.yeild()
на Thread.sleep(1)
в коде метода generateSeed
.
Заключение
Как же можно бороться с описанными уязвимостями?
Во-первых, Random
никогда не должен использоваться для целей, связанных с безопасностью, вместо него должен использоваться SecureRandom
. Во-вторых, никогда не следует заниматься изобретением своего криптографически стойкого PRNG: значительно безопаснее использовать какой-либо из готовых. В-третьих, при использовании SecureRandom
необходимо быть уверенным, что он правильно инициализирован, то есть что энтропия достаточно велика. И последнее: добавление новой свежей энтропии в SecureRandom
методом setSeed
значительно повысит безопасность.