Not an issue, but a question
Keksilton opened this issue · comments
Recently I have found an interesting project called Kaitai, they are collaborating with construct developers to enable import of python struct code to their format, but it's not available yet.
I have been trying to port it myself, so the formats would not be tied to python, but I have run into some issues trying to understand the format.
not_packed_stream = Struct(
"data_start_offset" / Tell,
"filename_table_offset" / Int32ul,
"files_count" / Int32ul,
Seek(8, 1),
"filedata_table_offset" / Int32ul,
"filename_table" / filename_table,
"file_data_table" / file_data_table,
)
Here is what the file looks like:
I have to read the values after the file has been parsed and discovered that :
data_start_offset
is equal to 0
filename_table_offset
is equal to 32
files_count
is equal to 23
filedata_table_offsed
is equal to 672
I don't understand why Tell is returning 0 in this case, since the pointer would have moved, after reading the header.
I also don't understand where the 32 is coming from.
I'm assuming the 00 00 00
is the tail from zstd_stream
, then there is 17 00
which translated to 23
, but I would expect a 4 byte integer, not just 2 bytes
After this 23, there is a0 02
it translates to 672
, which also fits the number I need, but it's 2 bytes wide again.
It also contradicts the Seek(8,1)
line
I have tried searching entire file for a a 32 bit, unsigned, little-endian integer equal to 672 and found nothing.
The section after after_obfs
does not seem to contain any of those numbers.
I have also tried to look for a "visualizer" for python's construct, to better understand the relation between the structure description and file contents location, but was unable to find any.
Could you please explain at least some of those issues?
I would really appreciate it.
I could translate this to Russian if it's necessary.
You were looking at an obfuscated compressed stream.
To get to the unpacked stream, you had to do a few simple transformations of it:
not_packed_bytes = unzstd(deobfuscate(file_bytes))
Yesterday I was dealing with files unpacking and made an auxiliary tool.
partial_constructor.py
For clarity, I implemented the unpacking adapter as a composition of adapters:
ZstdPackedCon = NotPackedStreamAdapter(ZstdStreamAdapter(DeobfsAdapter(Bytes(packed_size))))
Next, the first 128 bytes from the trace files at the decompression stages.
char.vromfs.bin
00000000 56 52 46 78 00 00 50 43 50 17 09 00 bd eb 08 c0 |VRFx..PCP.......|
00000010 08 00 00 00 97 00 07 02 7d 1f 7a 57 af a0 18 f9 |........}.zW....|
00000020 55 4e 49 a5 d4 ca 55 32 00 00 00 17 00 a0 02 e0 |UNI...U2........|
00000030 f6 06 01 1e 35 47 5f 72 8a a3 b4 c7 e0 f3 09 02 |....5G_r........|
00000040 19 29 3a 4e 61 76 88 90 63 6f 6e 66 69 67 2f 61 |.):Nav..config/a|
00000050 74 74 61 63 68 61 62 6c 65 2e 62 6c 6b 62 6f 74 |ttachable.blkbot|
00000060 73 63 6c 61 6e 5f 72 65 77 61 72 64 72 65 77 5f |sclan_rewardrew_|
00000070 73 6b 69 6c 6c 64 65 63 61 66 61 75 6c 74 5f 75 |skilldecafault_u|
00000080
deobfuscate: data.zstd
00000000 28 b5 2f fd a0 50 17 09 00 e4 1c 0f 9c d8 1d 20 |(./..P......... |
00000010 00 00 00 17 00 a0 02 e0 f6 06 01 1e 35 47 5f 72 |............5G_r|
00000020 8a a3 b4 c7 e0 f3 09 02 19 29 3a 4e 61 76 88 90 |.........):Nav..|
00000030 63 6f 6e 66 69 67 2f 61 74 74 61 63 68 61 62 6c |config/attachabl|
00000040 65 2e 62 6c 6b 62 6f 74 73 63 6c 61 6e 5f 72 65 |e.blkbotsclan_re|
00000050 77 61 72 64 72 65 77 5f 73 6b 69 6c 6c 64 65 63 |wardrew_skilldec|
00000060 61 66 61 75 6c 74 5f 75 73 65 72 79 6e 61 6d 69 |afault_userynami|
00000070 63 66 6c 69 67 68 74 6d 6f 64 65 67 61 6d 65 5f |cflightmodegame_|
00000080
unzstd: data
00000000 20 00 00 00 17 00 00 00 00 00 00 00 00 00 00 00 | ...............|
00000010 a0 02 00 00 17 00 00 00 00 00 00 00 00 00 00 00 |................|
00000020 e0 00 00 00 00 00 00 00 f6 00 00 00 00 00 00 00 |................|
00000030 06 01 00 00 00 00 00 00 1e 01 00 00 00 00 00 00 |................|
00000040 35 01 00 00 00 00 00 00 47 01 00 00 00 00 00 00 |5.......G.......|
00000050 5f 01 00 00 00 00 00 00 72 01 00 00 00 00 00 00 |_.......r.......|
00000060 8a 01 00 00 00 00 00 00 a3 01 00 00 00 00 00 00 |................|
00000070 b4 01 00 00 00 00 00 00 c7 01 00 00 00 00 00 00 |................|
00000080
I see, that makes sense. Thank you!
I guess I will have to implement those transformations outside of Kaitai and just let it use it.
@kotiq Could you help me a bit more?
-
I have tried to read though the code in your blk project, but it's hard for me to understand it, since I'm not a python developer.
I'm a bit confused by the first few bytes of the nm file, since those are not regular characters. Am I missing something, or should I just ignore it?
-
I'm also having trouble understanding blk format. I tried to follow this post, but it wasn't enough for me https://www.reddit.com/r/Warthunder/comments/o5r4v1/heads_up_on_vrom_and_blk_format_changes_for_data/
Once it works - I want to publish C# implementation on Github as well.
I speak Russian as well
Feel free to contact me via discord Keksilton#6024
Используется фреймворк Construct, сам python прост.
Двоичные файлы нового формата со стороны распаковки я разделил на два класса:
Толстые: автономные файлы, которым достаточно для распаковки самих себя. Примеры в game.vromfs.bin
. Каждый такой файл несет в себе таблицу строк.
Тонкие: таблица строк для них общая для каждой FS. Примеры в aces.vromfs.bin
.
Таблица строк хранится в файле nm. Из таблицы строк собираются имена параметров и секций и значения для строковых параметров.
1
В таблице строк подряд идет пара чисел в ULEB128 число строк в таблице и размер блока строк в байтах.
2
Тонкий файл начинается с 0 байта.
Далее в ULEB128 идут количество всех секций, включая корневую секцию, и количество параметров из всех секций.
Потом размер блока параметров, каждый из которых в закодированном виде превышают 4 байта, в байтах в ULEB128 и блок параметров. В нем "длинные" значения записаны подряд в порядке упоминания в таблице описания параметров, а навигация осуществляется смещением в байтах от начала блока. Для более 2 длинных значений:
[первое длинное значение]
[второе длинное значение]
...
[последнее длинное значение]
Длинные значения в случае тонкого файла у последовательностей из 2- и 3- целых и 2-, 3-, 4-, 12- рациональных и у длинного целого как 8 байт LE.
И следом таблица описания параметров и таблица описания секций.
Таблица описания параметров состоит из 8-байтовых записей, по одной на параметр.
Каждая такая запись состоит из 3-байтового индекса LE в таблице строк, описывающего имя, байта, описывающего тип значения и 4 байтов, описывающих значение.
В случае длинных значений они содержат LE индекс. Для строк у числа отбрасывается старший бит.
Для строк значение берется по номеру строки из таблицы строк. Для остальных - значение считывается из блока параметров, индекс представляет смещение относительно начала этого блока. Считывается число байт, согласно типу значения параметра: каждое целое или рациональное занимает 4 байта, последовательности хранятся как блоки целых LE или чисел с плавающей точкой одинарной точности LE подряд.
В случае коротких значений, байты кодируют сами значения: целое LE, число с плавающей точкой одинарной точности LE, четырехкомпонентный цвет как BGRA или булево значение как целое LE: 0 - ложь, 1 - истина.
Таблица описания секций состоит из блоков переменной длины, по одному на секцию, включая корневую секцию.
Каждый блок содержит описатель имени секции в ULEB128. Для корневой секции в этом месте 0, поэтому, все остальные описатели на 1 больше номера строки по порядку в таблице строк. Для получения индекса вычитаем из описателя единицу, а корневая секция безымянная.
Далее следует число параметров в секции, и число подсекций, оба в ULEB128.
Если секция содержит подсекции, то следующее число в ULEB128 - индекс первого блока, описывающего секцию в таблице описания секций.
Секция заполняется так.
По числу параметров в ней берутся параметры из таблицы описания параметров подряд. Каждый параметр строится по описывающему блоку. Например, для числа секций больше 2, где каждый параметр строится по строке из таблицы:
Индекс Содержимое
-----------------------------
0 [параметры корневой секции]
1 [параметры 1-й секции уровня 1]
...
X [параметры последней секции уровня 1]
X + 1 [параметры 2-й секции уровня 2]
...
Y [параметры последней секции уровня 2]
...
Z [параметры первой секции последнего уровня]
...
W [параметры последней секции последнего уровня]
По числу подсекций берутся подсекции из таблицы описания подсекций подряд, начиная с указанного индекса первого блока. Аналогично параметрам, для нетривиальной корневой секции в соответствии с обходом секции в ширину:
Индекс Содержимое
-----------------------------
0 [корневая секция]
1 [1-я секция уровня 1]
...
K [последняя секция уровня 1]
K + 1 [1-я секция уровня 2]
...
L [последняя секция уровня 2]
...
M [1-я секция последнего уровня]
...
N [последняя секция последнего уровня]
В качестве иллюстрации я упакую простую секцию как тонкую и распакую ее обратно вручную.
Пусть после вычислений над секцией она имеет вид:
vec4f:p4=1.25, 2.5, 5.0, 10.0
int:i=42
long:i64=0x40
alpha{
str:t="hello"
bool:b=yes
color:c=1, 2, 3, 4
gamma{
vec2i:ip2=3, 4
vec2f:p2=1.25, 2.5
transform:m=[[1.0, 0.0, 0.0] [0.0, 1.0, 0.0] [0.0, 0.0, 1.0] [1.25, 2.5, 5.0]]
}
}
beta{
float:r=1.25
vec2i:ip2=1, 2
vec3f:p3=1.25, 2.5, 5.0
}
Ей будет соответствовать множество двоичных представлений, которые различаются из-за различных обходов секции при
построении таблицы строк.
Пусть таблица строк имеет представление:
00000000 0f 57 76 65 63 34 66 00 69 6e 74 00 6c 6f 6e 67 |.Wvec4f.int.long|
00000010 00 61 6c 70 68 61 00 73 74 72 00 62 6f 6f 6c 00 |.alpha.str.bool.|
00000020 63 6f 6c 6f 72 00 67 61 6d 6d 61 00 76 65 63 32 |color.gamma.vec2|
00000030 69 00 76 65 63 32 66 00 74 72 61 6e 73 66 6f 72 |i.vec2f.transfor|
00000040 6d 00 62 65 74 61 00 66 6c 6f 61 74 00 76 65 63 |m.beta.float.vec|
00000050 33 66 00 68 65 6c 6c 6f 00 |3f.hello.|
00000059
И сама секция имеет представление:
00000000 00 04 0c 6c 00 00 a0 3f 00 00 20 40 00 00 a0 40 |...l...?.. @...@|
00000010 00 00 20 41 40 00 00 00 00 00 00 00 01 00 00 00 |.. A@...........|
00000020 02 00 00 00 00 00 a0 3f 00 00 20 40 00 00 a0 40 |.......?.. @...@|
00000030 03 00 00 00 04 00 00 00 00 00 a0 3f 00 00 20 40 |...........?.. @|
00000040 00 00 80 3f 00 00 00 00 00 00 00 00 00 00 00 00 |...?............|
00000050 00 00 80 3f 00 00 00 00 00 00 00 00 00 00 00 00 |...?............|
00000060 00 00 80 3f 00 00 a0 3f 00 00 20 40 00 00 a0 40 |...?...?.. @...@|
00000070 00 00 00 06 00 00 00 00 01 00 00 02 2a 00 00 00 |............*...|
00000080 02 00 00 0c 10 00 00 00 04 00 00 01 0e 00 00 80 |................|
00000090 05 00 00 09 01 00 00 00 06 00 00 0a 03 02 01 04 |................|
000000a0 0c 00 00 03 00 00 a0 3f 08 00 00 07 18 00 00 00 |.......?........|
000000b0 0d 00 00 05 20 00 00 00 08 00 00 07 2c 00 00 00 |.... .......,...|
000000c0 09 00 00 04 34 00 00 00 0a 00 00 0b 3c 00 00 00 |....4.......<...|
000000d0 00 03 02 01 04 03 01 03 0c 03 00 08 03 00 |..............|
000000de
Таблица строк.
hex:0
0F - names_count = 15 имен.
57 - Они занимают names_len = 87 байт.
-------------------------------------------
hex:2 str pn_id sn_id
-------------------------------------------
766563346600 vec4f 0 1
696E7400 int 1 2
6C6F6E6700 long 2 3
616C70686100 alpha 3 4
73747200 str 4 5
626F6F6C00 bool 5 6
636F6C6F7200 color 6 7
67616D6D6100 gamma 7 8
766563326900 vec2i 8 9
766563326600 vec2f 9 10
7472616E73666F726D00 transform 10 11
6265746100 beta 11 12
666C6F617400 float 12 13
766563336600 vec3f 13 14
68656C6C6F00 hello 14 15
Где pn_id - индекс строки в таблице параметров как имени или значения строкового параметра
sn_id - индекс строки в таблице секций как имени
hex:n - смещение первого значения в соответствующем файле в шестнадцатеричном формате
Секция.
hex:0
00 - Признак тонкого файла. Имен здесь нет. Или признак для размерности в терминaх blk_unpack.py
hex:1
04 - blocks_count = 4 секции, вместе с корневой.
0C - params_count = 12 параметров во всех секциях.
Блок значений длинных параметров.
hex:3
6C - Блок длинных значений параметров занимает param_data_len = 108 байт.
---------------------------------------------------------
hex:4 value offset size
---------------------------------------------------------
0000A03F 00002040 0000A040 (1.25, 2.5, 5.0, 0 16
00002041 10.0) - -
40000000 00000000 64 16 8
01000000 02000000 (1, 2) 24 8
0000A03F 00002040 0000A040 (1.25, 2.5, 5.0) 32 12
03000000 04000000 (3, 4) 44 8
0000A03F 00002040 (1.25, 2.5) 52 8
0000803F 00000000 00000000 ((1.0, 0.0, 0.0), 60 48
00000000 0000803F 00000000 (0.0, 1.0, 0.0), - -
00000000 00000000 0000803F (0.0, 0.0, 1.0), - -
0000A03F 00002040 0000A040 (1.25, 2.5, 5.0)) - -
Таблица описания параметров.
---------------------------------------------------
hex:70 pn_id blk_type value p_id
---------------------------------------------------
000000 06 00000000 0 p4 #0@16 0
010000 02 2A000000 1 i 42 1
020000 0C 10000000 2 i64 #16@8 2
040000 01 0E000080 4 t #14 3
050000 09 01000000 5 b 1 4
060000 0A 03020104 6 c (1, 2, 3, 4) 5
0C0000 03 0000A03F 12 r 1.25 6
080000 07 18000000 8 ip2 #24@8 7
0D0000 05 20000000 13 p3 #32@12 8
080000 07 2C000000 8 ip2 #44@8 9
090000 04 34000000 9 p2 #52@8 10
0A0000 0B 3C000000 10 m #60@48 11
Где #x@y - y байтов со смещения x от начала блока длинных параметров
#z - pn_id значения строкового параметра
p_id - индекс записи параметра в таблице описания секций
Таблица описания секций.
----------------------------------------------
hex:d0 s_id sn_id p_count s_count ss_id
----------------------------------------------
00 03 02 01 0 - 3 2 1
04 03 01 03 1 4 3 1 3
0C 03 00 2 12 3 0 -
08 03 00 3 8 3 0 -
Где ss_id - индекс первой подсекции в секции
Построение таблицы параметров из таблицы строк, блока значений длинных параметров и таблицы описания параметров.
--------------------------------------------------------------------------------------------
p_id name blk_type value
--------------------------------------------------------------------------------------------
0 vec4f p4 (1.25, 2.5, 5.0, 10.0)
1 int i 42
2 long i64 64
3 str t hello
4 bool b 1
5 color c (1, 2, 3, 4)
6 float r 1.25
7 vec2i ip2 (1, 2)
8 vec3f p3 (1.25, 2.5, 5.0)
9 vec2i ip2 (3, 4)
10 vec3f p2 (1.25, 2.5)
11 transform m ((1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, 0.0, 1.0), (1.25, 2.5, 5.0))
Построение секций из таблицы строк, таблицы параметров и таблицы описания секций.
Буду обходить таблицу описания секций снизу вверх.
-----------------------------
s_id name p_ids ss_ids
-----------------------------
3 gamma (9, 10, 11) -
2 beta (6, 7, 8) -
1 alpha (3, 4, 5) (3)
0 - (0, 1, 2) (1, 2)
Подсекции
// s_id = 3
gamma {
vec2i:ip2=3, 4
vec2f:p2=1.25, 2.5
transform:m=[[1.0, 0.0, 0.0] [0.0, 1.0, 0.0] [0.0, 0.0, 1.0] [1.25, 2.5, 5.0]]
}
// s_id = 2
beta{
float:r=1.25
vec2i:ip2=1, 2
vec3f:p3=1.25, 2.5, 5.0
}
// s_id = 1
alpha{
str:t="hello"
bool:b=yes
color:c=1, 2, 3, 4
// s_id = 3
gamma{
vec2i:ip2=3, 4
vec2f:p2=1.25, 2.5
transform:m=[[1.0, 0.0, 0.0] [0.0, 1.0, 0.0] [0.0, 0.0, 1.0] [1.25, 2.5, 5.0]]
}
}
Корневая секция
//s_id = 0
vec4f:p4=1.25, 2.5, 5.0, 10.0
int:i=42
long:i64=0x40
// s_id = 1
alpha{
str:t="hello"
bool:b=yes
color:c=1, 2, 3, 4
// s_id = 3
gamma{
vec2i:ip2=3, 4
vec2f:p2=1.25, 2.5
transform:m=[[1.0, 0.0, 0.0] [0.0, 1.0, 0.0] [0.0, 0.0, 1.0] [1.25, 2.5, 5.0]]
}
}
// s_id = 2
beta{
float:r=1.25
vec2i:ip2=1, 2
vec3f:p3=1.25, 2.5, 5.0
}