TL;DR;
Это было целое приключение по изучению кода, обрабатывающего сериализацию TreeMap, но мне удалось найти золото. Чтобы найти золото (код), прокрутите ответ до конца. Если вы хотите следовать процессу дедукции, чтобы вы могли делать это с другими классами, вам придется бороться с моим бредом.
Я мог бы сделать это более кратким, но я только что провел 7 часов, читая код и экспериментируя, мне это надоело на данный момент, и этот пост может быть поучительным для других, которые хотят предпринять это приключение.
Введение
Моя атака заключается в том, что десериализация всего этого занимает слишком много памяти, выделяя объекты, которые вы, возможно, не хотите использовать, или занимая оперативную память. Поэтому я решил просто прочитать необработанные данные и проверить размер TreeMap. Таким образом, у нас есть единственный фрагмент данных, который нам нужен, чтобы оценить, следует ли нам принимать или нет. Да, это означает считывание данных дважды, если они приняты, но это компромисс, на который вам нужно пойти, если вы хотите использовать это. Этот код пропускает множество шагов проверки, которые использует java, потому что нас это не интересует. Нам просто нужен надежный способ получить размер TreeMap без необходимости загружать всю карту дерева со всеми данными.
Там, где обычно вы загружаете все данные, читаете весь файл/поток байтов и используете его для инициализации, нам нужно только прочитать части начала файла. Сокращение работы, которую необходимо выполнить, и времени, которое необходимо тратить впустую. Нам просто нужно переместить указатель чтения файла вперед надежным способом, чтобы мы всегда получали правильные байты для нашего процесса. Это значительно сокращает рабочий процесс для процесса Java. После того, как размер был проверен быстрым обычным чтением файла, его можно подвергнуть фактическому процессу сериализации или отбросить. Это лишь небольшие накладные расходы по сравнению с обычной работой, но они служат эффективным барьером, оставаясь при этом гибкими в ваших принятых критериях.
Начало приключения
Глядя на исходный код TreeMap https://github.com/openjdk-mirror/jdk7u-jdk/blob/master/src/share/classes/java/util/TreeMap.java#L123 мы видим, что размер переходное значение. Это означает, что он не кодируется в сериализованных данных, поэтому при быстрой проверке его нельзя проверить, прочитав значение поля из отправленных байтов.
Но... не вся надежда потеряна. Потому что, если мы проверим writeObject()
, мы увидим, что размер закодирован https://github.com/openjdk-mirror/jdk7u-jdk/blob/master/src/share/classes/java/util/TreeMap.java#L2268
Это означает, что у нас есть значения байтов, которые мы можем проверить в отправленных необработанных данных!.
Теперь давайте проверим, что делает defaultReadObject.
L492 Сначала он проверяет, десериализуется ли он, если нет, то блокируется. Ладно, нам это неинтересно.
L495 Затем ему нужен экземпляр объекта, SerialCallbackContext был инициализирован с этим, поэтому он не выполняет чтение.
L496 Затем он получает экземпляр ObjectStreamClass из SerialCallbackContext, так что теперь мы будем работать с ObjectStream.
L497 Некоторые режимы были изменены, но затем мы идем читать поля.
Хорошо, переходим на ObjectInputStream
L1944 снова ссылка на класс, которая была предоставлена экземпляру потока объектов (для быстрого изложения L262, который установлен в L442), поэтому чтение не выполняется.
L1949 получение размера полей по умолчанию с помощью getPrimDataSize, который задается в computeFieldOffsets. Это полезно для нас, только позор... это недоступно, поэтому давайте разберемся, как это эмулировать, просто как примечание.
L1255 Он использует переменную поля. Это установлено в getSerialFields, который, к сожалению, также является приватным. В этот момент у меня сложилось впечатление, что я возился с силами, которых не должен касаться. Но я иду дальше, игнорируя запретный знак, меня ждут приключения!
getDeclaredSerialFields и getDefaultSerialFields вызывается в этом методе, поэтому мы можем использовать его содержимое для эмуляции его функциональности.
Анализ getDeclaredSerialFields мы см., это действует только в том случае, если serialPersistentFields объявлен в файле класс TreeMap. Ни TreeMap, ни его родитель AbstractMap содержит это поле. Поэтому мы игнорируем метод getDeclaredSerialFields. На getDefaultSerialFields
Итак, если мы возьмем этот код, повозимся с ним, мы сможем получить значимые данные и увидим, что TreeMap имеет одно поле, и теперь у нас есть динамический метод для «эмулирования» получения полей по умолчанию, если что-то изменится по какой-либо причине.
https://ideone.com/UqqKSG (я оставил имена классов с полными путями, чтобы было легче увидеть, какие классы я пользуюсь)
java.lang.reflect.Field[] clFields = TreeMap.class.getDeclaredFields();
ArrayList<java.lang.reflect.Field> list = new ArrayList<>();
int mask = java.lang.reflect.Modifier.STATIC | java.lang.reflect.Modifier.TRANSIENT;
for (int i = 0; i < clFields.length; i++) {
// Check for non transient and non static fields.
if ((clFields[i].getModifiers() & mask) == 0) {
list.add(clFields[i]);
System.out.println("Found field " + clFields[i].getName());
}
}
int size = list.size();
System.out.println(size);
Компаратор найденных полей
1
L1951 Вернувшись в ObjectInputStream, мы видим, что этот размер используется для создания массива, который будет использоваться в качестве буфера для чтения, а затем они считываются полностью, с аргументами пустой массив, смещение 0, длина полей (1) и ЛОЖЬ. Этот метод вызывается в BlockDataInputStream, а значение false означает, что он не будет скопирован. Это всего лишь вспомогательный метод для обработки потока данных с помощью PeekInputStream(in), мы можем использовать те же методы в потоке, который мы собираемся использовать с некоторой возней, хотя сейчас нам это не нужно, потому что нет примитивных типов хранятся в TreeMap. Поэтому я оставлю этот ход мыслей для этого ответа.
L1964 вызывает readObject0, который считывает компаратор, используемый в TreeMap. Он проверяет oldMode, который возвращает, читается ли поток в режиме блочных данных или нет, и мы можем видеть, что он был установлен в режим потока (false) в readFields, поэтому я пропущу эту часть.
L1315 простая проверка того, что рекурсия не происходит более одного раза, но просматривается один байт. Давайте посмотрим, что для этого может предоставить TreeMap. Это заняло у меня больше времени, чем я ожидал. Я не могу опубликовать код здесь, он слишком длинный, но он у меня есть на ideone и суть.
- В основном вам нужно скопировать встроенный класс BlockDataInputStream,
- добавьте
private static native void bytesToFloats(byte[] src, int srcpos, float[] dst, int dstpos, int nfloats);private static native void bytesToDoubles(byte[] src, int srcpos, double[] dst, int dstpos, int ndoubles);
в BlockDataInputStream. Если вам действительно нужно использовать эти методы, замените их чем-то Java. Это будет выдает ошибку времени выполнения.
- скопировать встроенный класс PeekInputStream
- скопируйте класс java.io.Bits.
- Ссылки TC_ должны указывать на
java.io.ObjectStreamConstants.TC_
BlockDataInputStream bin = new BlockDataInputStream(getTreeMapInputStream());
bin.setBlockDataMode(false);
byte b = bin.peekByte();
System.out.println("Does b ("+String.format("%02X ", b)+") equals TC_RESET?" + (java.io.ObjectStreamConstants.TC_RESET == b ? "yes": "no"));
Соответствует ли b (-84) TC_RESET? нет
Мы видим, что читаем 0xAC, давайте срезать путь и заглянуть в java.io.ObjectStreamConstants, что это такое. Для чисто 0xAC нет записи, но похоже, что это часть заголовка.
Давайте проверим работоспособность с readStreamHeader и вставьте содержимое этого метода прямо перед нашим кодом peekByte, снова обновив ссылки TC_. Теперь мы получаем вывод 0x73. Прогресс!
0x73 — это TC_OBJECT, так что давайте перейдем к L1347
Там мы находим, что readOrdinaryObject, который выполняет readByte().
Затем classDescription считывается, что приводит к переходу на readNonProxy
Затем у нас есть вызов readUTF(), readLong(), readByte(), readShort, чтобы читать поля..., затем для каждого поле a readByte(), readUTF ().
Итак, давайте подражать этому. Первое, с чем я сталкиваюсь, это то, что он пытается читать за пределами длины строки (имя класса из 29184 символов? Вы так не думаете ) для имени класса, поэтому я что-то упускаю. Я понятия не имею, чего мне не хватает на данный момент, но я запускаю его на ideone, и, возможно, он работает на версии java, где они добавили дополнительный байт перед чтением UTF. Мне не терпится посмотреть, если честно. Работает, я доволен. В любом случае, после считывания лишнего байта все работает отлично, и мы имеем желаемое. TODO: выяснить, где считывается лишний байт
BlockDataInputStream bin = new BlockDataInputStream(getTreeMapInputStream());
bin.setBlockDataMode(false);
short s0 = bin.readShort();
short s1 = bin.readShort();
if (s0 != java.io.ObjectStreamConstants.STREAM_MAGIC || s1 != java.io.ObjectStreamConstants.STREAM_VERSION) {
throw new StreamCorruptedException(
String.format("invalid stream header: %04X%04X", s0, s1));
}
byte b = bin.readByte();
if(b == java.io.ObjectStreamConstants.TC_OBJECT) {
bin.readByte();
String name = bin.readUTF();
System.out.println(name);
System.out.println("Is string ("+name+")it a java.util.TreeMap? "+(name.equals("java.util.TreeMap") ? "yes":"no"));
bin.readLong();
bin.readByte();
short fields = bin.readShort();
for(short i = 0; i < fields; i++) {
bin.readByte();
System.out.println("Read field name "+bin.readUTF());
}
}
Теперь мы продолжаем на Строка 1771, чтобы увидеть, что читается после прочтения описания класса. После этого происходит много проверок экземпляров объектов и т. д. Это похоже на спагетти, через которые мне не хочется разбираться. Давайте взламываем и анализируем данные.
Данные в виде строки
tLjava/util/Comparator;xppwsrjava.lang.Integer¬ᅠᄂチヌ8Ivaluexrjava.lang.Numberニᆲユヤ¢ヒxptData1sq~tData5sq~tData4sq~tData2sq~FtData3x -74 -00 -16 -4C -6A -61 -76 - 61 -2Ф -75 -74 -69 -6С -2Ф -43 -6Ф -6Д -70 -61 -72 -61 -74 -6Ф -72 -3Б -78 -70 -70 -77 -04 -00 -00 - 00 -05 -73 -72 -00 -11 -6А -61 -76 -61 -2Е -6С -61 -6Е -67 -2Е -49 -6Е -74 -65 -67 -65 -72 -12 -Е2 - А0 -А4 -Ф7 -81 -87 -38 -02 -00 -01 -49 -00 -05 -76 -61 -6С -75 -65 -78 -72 -00 -10 -6А -61 -76 -61 - 2Е -6С -61 -6Е -67 -2Е -4Е -75 -6Д -62 -65 -72 -86 -АС -95 -1Д -0Б -94 -Е0 -8Б -02 -00 -00 -78 -70 - 00 -00 -00 -01 -74 -00 -05 -44 -61 -74 -61 -31 -73 -71 -00 -7Е -00 -03 -00 -00 -00 -02 -74 -00 -05 - 44 -61 -74 -61 -35 -73 -71 -00 -7Е -00 -03 -00 -00 -00 -04 -74 -00 -05 -44 -61 -74 -61 -34 -73 -71 - 00 -7Е -00 -03 -00 -00 -00 -17 -74 -00 -05 -44 -61 -74 -61 -32 -73 -71 -00 -7Е -00 -03 -00 -00 -00 - 46 -74 -00 -05 -44 -61 -74 -61 -33 -78
Т. Мы знаем, что размер элементов записывается перед элементами. Поля Data1 - Date5 — это значения, хранящиеся на карте. Поэтому, когда часть Data1sq приходит после этого, все спорно. Давайте добавим элемент на карту, чтобы увидеть, какое значение изменится!
74 -00 -16 -4С -6А -61 -76 -61 -2Ф -75 -74 -69 -6С -2Ф -43 -6Ф -6Д -70 -61 -72 -61 -74 -6Ф -72 -3Б - 78 -78 -70 -70 -77 -04 -00 -00 -00 -05 -73 -72 -00 -11 -6A -61 -76 -61 -2E
74 -00 -16 -4C -6A -61 -76 -61 -2F -75 -74 -69 -6C -2F -43 -6F -6D -70 -61 -72 -61 -74 -6F -72 -3B -78 -78 -70 -70 -77 -04 -00 -00 -00 -06 -73 -72 -00 -11 -6А -61 -76 -61 -2Е
Хорошо, теперь мы знаем, сколько укусов нам еще предстоит зарезать. Давайте посмотрим, сможем ли мы вывести здесь некоторую логику с заданными значениями.
Первое значение — 74. Проверяя ObjectStreamConstants, мы видим, что это означает строку. Давайте прочитаем этот байт, а затем UTF.
Теперь у нас осталось -70 -70 -77 -04 -00 -00 -00 -06
. Положим это помимо констант.
NULL - NULL - BLOCKDATA - значение 4 - значение 0 - значение 0 - значение 0 - значение 6
Мы могли бы теоретизировать здесь:
После данных блока записывается целое число. Целое число составляет четыре байта. отсюда и четверка. Следующие четыре позиции составляют целое число.
Давайте посмотрим, что произойдет, если мы добавим компаратор в древовидную карту.
xpsr'java.util.Collections$ReverseComparatordハSNJxpwsrjava.lang.Integer¬ᅠᄂチヌ8I
-78 -70 -73 -72 -00 -27 -6A -61 -76 -61 -2E - 75 -74 -69 -6C -2E -43 -6F -6C -6C -65 -63 -74 -69 -6F -6E -73 -24 -52 -65 -76 -65 -72 -73 -65 -43 - 6Ф -6Д -70 -61 -72 -61 -74 -6Ф -72 -64 -04 -8А -Ф0 -53 -4Е -4А -Д0 -02 -00 -00 -78 -70 -77 -04 -00 - 00 -00 -06
Мы видим END_BLOCK, NULL, OBJECT
Хорошо. Итак, теперь мы знаем, что второй Null является держателем данных Comparator. Так что мы можем взглянуть на это. Нам нужно пропустить два байта, а затем посмотреть, является ли это байтом объекта. Если это так, нам нужно прочитать данные объекта, чтобы мы могли добраться до желаемой позиции.
Давайте сделаем паузу и просмотрим код на данный момент: https://ideone.com/ma6nQy
BlockDataInputStream bin = new BlockDataInputStream(getTreeMapInputStream());
bin.setBlockDataMode(false);
short s0 = bin.readShort();
short s1 = bin.readShort();
if (s0 != java.io.ObjectStreamConstants.STREAM_MAGIC || s1 != java.io.ObjectStreamConstants.STREAM_VERSION) {
throw new StreamCorruptedException(
String.format("invalid stream header: %04X%04X", s0, s1));
}
byte b = bin.peekByte();
if(b == java.io.ObjectStreamConstants.TC_OBJECT) {
Ideone.readObject(bin,true);
}
if(bin.readByte() == java.io.ObjectStreamConstants.TC_STRING) {
String className = bin.readUTF();
System.out.println(className + "starts with L "+(className.charAt(0) == 'L' ? "yes": "no"));
if(className.charAt(0) == 'L') {
// Skip two bytes
bin.readByte();
bin.readByte();
b = bin.peekByte();
if(b == java.io.ObjectStreamConstants.TC_OBJECT) {
System.out.println("reading object");
Ideone.readObject(bin,true);
}
else {
// remove the null byte so we end up at same position
bin.readByte();
}
}
}
int length = 50;
byte[] bytes = new byte[length];
for(int c=0;c<length;c++) {
bytes[c] = bin.readByte();
System.out.print((char)(bytes[c]));
}
for(int c=0;c<length;c++) {
System.out.print("-"+String.format("%02X ", bytes[c]));
}
}
public static void readObject(BlockDataInputStream bin, boolean doExtra) throws Exception {
byte b = bin.readByte();
if(b == java.io.ObjectStreamConstants.TC_OBJECT) {
if(doExtra) {
bin.readByte();
}
String name = bin.readUTF();
System.out.println(name);
System.out.println("Is string ("+name+")it a java.util.TreeMap? "+(name.equals("java.util.TreeMap") ? "yes":"no"));
bin.readLong();
bin.readByte();
short fields = bin.readShort();
for(short i = 0; i < fields; i++) {
bin.readByte();
System.out.println("Read field name "+bin.readUTF());
}
}
}
Компаратор найденных полей
1
java.util.TreeMap
Является ли строка (java.util.TreeMap) java.util.TreeMap? yes
Чтение компаратора имен полей
Ljava/util/Comparator; начинается с L yes
чтение объекта
java.util.Collections$ReverseComparator
Является строкой (java.util.Collections$ReverseComparator) это java.util.TreeMap? no
xpwsrjava.lang.Integer¬ᅠᄂチヌ8Ivaluexr
-78 -70 -77 -04 -00 -00 -00 -06 -73 -72 -00 -11 -6A -61 -76 -61 -2Е -6С -61 -6Е -67 -2Е -49 -6Е -74 -65 -67 -65 -72 -12 -Е2 -А0 -А4 -Ф7 -81 -87 -38 -02 -00 -01 -49 -00 -05 -76 -61 -6С -75 -65 -78 -72
К сожалению, мы не оказываемся в одних и тех же точках на временной шкале.
Когда есть компаратор, мы заканчиваем:
-78 -70 -77 -04 -00 -00 -00 -06
Когда компаратор удален, мы заканчиваем:
-77 -04 -00 -00 -00 -06
Хм. Эти BLOCK END и NULL выглядят очень знакомо. Это те самые байты, которые мы пропустили при чтении компаратора. Эти два байта всегда удаляются, но, по-видимому, компаратор также объявляет свои собственные значения BLOCK END и NULL.
Итак, если есть компаратор, удалите два конечных байта, чтобы мы получили то, что хотим, последовательно. https://ideone.com/pTu8Fd
-77 -04 -00 -00 -00 -06
Затем мы пропускаем следующий BLOCKDATA маркер (77) и доберитесь до золота!
Добавляя дополнительные строки, мы получаем наш вывод: https://ideone.com/wy0uF2
System.out.println(String.format("%02X ", bin.readByte()));
if(bin.readByte() == (byte)4) {
System.out.println("The length is "+ bin.readInt());
}
77
Длина 6
И у нас есть магическое число, которое нам нужно!
Хорошо. Вывод сделан, давайте очистим его
Полезные вещи, о которых вы заботитесь
Запускаемый фрагмент кода: https://ideone.com/J6ovMy
Полный код также как суть: https://gist.github.com/tschallacka/8f89982e9569d0b9974dff37d8f45faf
/**
This is dual licensed under MIT. You can choose wether you want to use CC-BY-SA or MIT.
Copyright 2020 Tschallacka
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import java.util.*;
import java.lang.*;
import java.io.*;
/* Name of the class has to be "Main" only if the class is public. */
class Ideone
{
public static void main (String[] args) throws java.lang.Exception
{
doTest(1,true);
doTest(1,false);
doTest(20,true);
doTest(20,false);
doTest(4,true);
doTest(19,false);
}
public static void doTest(int size, boolean comparator) throws java.lang.Exception {
SerializedTreeMapAnalyzer analyzer = new SerializedTreeMapAnalyzer();
System.out.println(analyzer.getSize(Ideone.getTreeMapInputStream(size,comparator)));
}
public static ByteArrayInputStream getTreeMapInputStream(int size, boolean comparator) throws Exception {
TreeMap<Integer, String> tmap =
new TreeMap<Integer, String>(comparator?Collections.reverseOrder():null);
/*Adding elements to TreeMap*/
for(int i = 0; size > 0 && i < size; i++) {
tmap.put(i, "Data"+i);
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream( baos );
oos.writeObject( tmap );
oos.close();
return new ByteArrayInputStream(baos.toByteArray());
}
}
class SerializedTreeMapAnalyzer
{
public int getSize(InputStream stream) throws IOException, StreamCorruptedException, Exception {
BlockDataInputStream bin = new BlockDataInputStream(stream);
bin.setBlockDataMode(false);
short s0 = bin.readShort();
short s1 = bin.readShort();
if (s0 != java.io.ObjectStreamConstants.STREAM_MAGIC || s1 != java.io.ObjectStreamConstants.STREAM_VERSION) {
throw new StreamCorruptedException(
String.format("invalid stream header: %04X%04X", s0, s1));
}
byte b = bin.peekByte();
if(b == java.io.ObjectStreamConstants.TC_OBJECT) {
this.readObject(bin,true);
}
if(bin.readByte() == java.io.ObjectStreamConstants.TC_STRING) {
String className = bin.readUTF();
if(className.charAt(0) == 'L') {
// Skip two bytes
bin.readByte();
bin.readByte();
b = bin.peekByte();
if(b == java.io.ObjectStreamConstants.TC_OBJECT) {
this.readObject(bin,true);
bin.readByte();
bin.readByte();
}
else {
// remove the null byte so we end up at same position
bin.readByte();
}
}
}
bin.readByte();
if(bin.readByte() == (byte)4) {
return bin.readInt();
}
return -1;
}
protected void readObject(BlockDataInputStream bin, boolean doExtra) throws Exception {
byte b = bin.readByte();
if(b == java.io.ObjectStreamConstants.TC_OBJECT) {
if(doExtra) {
bin.readByte();
}
String name = bin.readUTF();
bin.readLong();
bin.readByte();
short fields = bin.readShort();
for(short i = 0; i < fields; i++) {
bin.readByte();
bin.readUTF();
}
}
}
}
1
1
20
20
4
19
person
Tschallacka
schedule
07.02.2020
maxarray
(неmaxArraySize
, а также DoS, а не DDoS здесь) затронулHashMap
, так как он не использует массив в качестве своей последовательной формы. Однако принципиально эти меры неэффективны — при любой разумной конфигурации с обратными ссылками можно иметь несколько уровней коллекций по сотне элементов в каждой. Единственное преимуществоTreeMap
заключается в том, что для этого требуетсяComparator
илиComparable
. Мне было бы интересно услышать другое. - person Tom Hawtin - tackline   schedule 07.02.2020java.io.
, вместо этого заставив ее создавать фиктивные объекты. Тем не менее, вы должны быть крайне заинтересованы в сохранении формата сериализации Java, если повторная реализация стоимостной стороны вещей с соответствующими проверками работоспособности того стоит. - person Tom Hawtin - tackline   schedule 08.02.2020readObject
должен вызыватьdefaultReadObject
или эквивалент. Это будет читать любые поля, которые злоумышленник захочет поместить в поток. (ObjectStreamClass
- это действительно несколько классов в одном. Трудно читать исходный код, но макет в потоке вообще не должен соответствовать классу.) - person Tom Hawtin - tackline   schedule 08.02.2020