├── LICENSE ├── README.md ├── nv_complete.txt ├── nv_print.rb └── repack.rb /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2022, fenrir 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mbn_utils 2 | Utilities for Qualcomm mbn file are provided. 3 | A mbn file is mainly used to regulate comminucation methods and frequency bands in a cell phone running on a Qualcomm modem. 4 | The goal of this tool is to control behaviour of phone's modem by changing the mbn file and flashing into your phone by android `fastboot` command at your own risk. 5 | It is noted that the method to install the modified mbn file into an unlocked device (**without root**) will be an alternative to get **root**, open **diag** port and modify NV items by using **QPST**. 6 | In addition, this tool is inspired by [EfsTools](https://github.com/JohnBel/EfsTools). 7 | 8 | ## This tool is under development! 9 | Currently, to change the behaviour by using this tool fails due to mismatch of the digest, which may be related to [Qualcomm secure boot](https://github.com/TalAloni/QualcommSecureBoot-SecondaryExecutableVerifier/). 10 | The digest is included in the last item of the main segment, which corresponds to 32 bytes data ranging from index 79 (counts satrting from zero) in the last line of _items.txt_ described below. 11 | When a successful match is acheieved in the near future, we can check the digest as _/nv/item_files/mcfg/mcfg_sw_config_digest_version_ in modem EFS by using tools such as QPST. 12 | In addition, other digests may be important to make this tool available according to [link](https://www.jianshu.com/p/ca84184877a1), and the below table is the summary. 13 | 14 | | file localtion under _image/modem_pr/mcfg/configs_ of NON-HLOS.bin | EFS location | 15 | ----|---- 16 | | mcfg_sw/generic/(area)/(operator)/Commercial/mcfg_sw.mbn | /nv/item_files/mcfg/mcfg_sw_config_digest_version | 17 | | mcfg_sw/mbn_sw.dig | /nv/item_files/mcfg/mcfg_rfs_sw_digest_version | 18 | | mcfg_hw/mbn_hw.dig | /nv/item_files/mcfg/mcfg_rfs_hw_digest_version | 19 | 20 | 21 | ## How to use 22 | 1. Download this repository and install Ruby 23 | 1. Parse a mbn file to be modified with [repack.rb](repack.rb); for instance, the target file named _mcfg_sw.mbn_ is parsed with the following command: 24 | ``` 25 | ./repack mcfg_sw.mbn 26 | ``` 27 | 1. The content of the parsed file is extracted in a directory like _mcfg_sw.mbn.extracted_. 28 | 1. Edit the content. The most important file is _mcfg_sw.mbn.extracted/items.txt_, whose each line corresponds to each item of the content with its properties are described in fields separated by commas. 29 | The leading field in a line indicates the item type; 1, 2, 4, and 10 are NV, NV_FILE, FILE, and trailer, respectively. 30 | The other fields are summarized in the following table. 31 | You can not only modify the parsed items but also add or delete the items. 32 | If you add or delte files in the extracted directory, _items.txt_ should be updated manually. 33 | Especially, do not forget to update the length located at the 7th filed of NV_FILE or FILE after a file item is modified. 34 | | 1st field | type | 2nd | 3rd | 4th | 5th | 6th | 7th | 8th | 35 | ----|----|----|----|----|----|----|----|---- 36 | | 1 | NV | attribute | index | magic | hex byte data separated by spaces | | | | 37 | | 2 or 4 | NV_FILE or FILE | attribute | file_name stored in the extratced directory | magic1 | magic2 | magic3 | length in file | offset in file | 38 | | 10 | trailer | attribute | (N.A.) | magic | length | hex byte data separated by spaces | | | 39 | 1. (Optional) to understand the NV items in _items.txt_, you can use [nv_print.rb](nv_print.rb) to generate human freindly item list: 40 | ``` 41 | ./nv_print.rb mcfg_sw.mbn.extracted/items.txt 42 | ``` 43 | 1. After _items.txt_ and files are modified, rerun `repack.rb` to get the modified mbn file named as _mcfg_sw.mbn.repacked_. 44 | ``` 45 | ./repack mcfg_sw.mbn 46 | ``` 47 | 48 | ## Where to get a mbn file 49 | The mbn file is included in a ROM image; for example, `image/modem_pr/mcfg/configs/mcfg_sw/generic/(area)/(operator)/Commercial/mcfg_sw.mbn` in EFS image named `NON-HLOS.bin` of a Xiaomi phone fastboot ROM. 50 | The content of EFS image is extracted with a LINUX machine by usiang a command like `mount -o loop NON-HLOS.bin some_dir`. 51 | -------------------------------------------------------------------------------- /nv_print.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ruby 2 | 3 | nv_table = proc{ 4 | res = {} 5 | open(File::join(File::dirname(__FILE__), 'nv_complete.txt')).each{|line| 6 | next if line =~ /^\s*(?:$|#)/ # accept empty line 7 | next unless line.chomp =~ /(\d+)\^(\"[^\"]+\")\^(\"[^\"]+\")/ 8 | res[$1.to_i] = [$2, $3] 9 | } 10 | res 11 | }.call 12 | 13 | open(ARGV.shift).each{|line| 14 | next if line =~ /^\s*(?:$|#)/ # accept empty line 15 | type, attri, location, *other = line.chomp.split(',') 16 | next if type.to_i != 1 17 | idx = location.to_i 18 | puts ([idx] + (nv_table[idx] || ([nil] * 2)) + other).join(',') 19 | } 20 | -------------------------------------------------------------------------------- /repack.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ruby 2 | 3 | ELF32_HEADER = [ 4 | [:magic, "C16", "\x7FELF".unpack("C*") + [1,1]], # 0x00- 5 | 6 | [:type, "v", 2], # 0x10- 7 | [:machine, "v", 0], # 0x12- 8 | [:version, "V", 1], # 0x14- 9 | 10 | [:entry_point_offset, "V"], # 0x18- 11 | [:program_header_offset, "V"], # 0x1C- 12 | [:section_header_offset, "V"], # 0x20- 13 | 14 | [:flags, "V"], # 0x24- 15 | [:header_size, "v", 52], # 0x28- 16 | [:program_header_size, "v", 32], # 0x2A- 17 | [:program_headers, "v", 3], # 0x2C- 18 | [:section_header_size, "v"], # 0x2E- 19 | [:section_headers, "v"], # 0x30- 20 | [:section_header_index, "v"], # 0x32- 21 | ] 22 | 23 | ELF32_PROGRAM_HEADER = [ 24 | [:type, "V"], 25 | [:offset, "V"], 26 | [:virtual_address, "V"], 27 | [:physical_address, "V"], 28 | [:file_bytes, "V"], 29 | [:mem_bytes, "V"], 30 | [:flags, "V"], 31 | [:align, "V"], 32 | ] 33 | 34 | MCFG_HEADER = [ 35 | [:magic, "C4", "MCFG".unpack("C*")], # 0x00- 36 | [:format_type, "v"], # 0x04- 37 | [:configuration_type, "v"], # 0x06- 38 | [:items, "V"], # 0x08- 39 | [:carrier_index, "v"], # 0x0C- 40 | [:reserved, "v"], # 0x0E- 41 | [:version_id, "v", 4995], # 0x10- 42 | [:version_size, "v", 4], # 0x12- 43 | [:version, "V"], # 0x14- 44 | ] 45 | 46 | ITEM_HEADER = [ 47 | [:length, "V"], 48 | [:type, "C"], 49 | [:attributes, "C"], 50 | [:reserved, "v"], 51 | ] 52 | 53 | [MCFG_HEADER, ITEM_HEADER].each{|table| 54 | res = table.inject(0){|result, item| 55 | result + ([0] * 16).pack(item[1]).length 56 | } 57 | table.define_singleton_method(:min_bytes){res} 58 | } 59 | 60 | def parse(table, input) 61 | require 'stringio' 62 | io = case input 63 | when IO, StringIO 64 | input 65 | when String 66 | StringIO::new(input) 67 | else 68 | raise 69 | end 70 | Hash[*(table.collect{|k, fmt, expected| 71 | len = ([0] * 0x400).pack(fmt).size 72 | str = io.read(len) 73 | data = str.unpack(fmt) 74 | data = data[0] if data.size == 1 75 | case expected 76 | when Array 77 | raise unless data.slice(0, expected.length) == expected 78 | when nil 79 | else 80 | raise unless data == expected 81 | end 82 | [k, data] 83 | }.flatten(1))] 84 | end 85 | 86 | def deparse(table, hash) 87 | table.collect{|k, fmt| 88 | [hash[k]].flatten.pack(fmt) 89 | }.join 90 | end 91 | 92 | fname = ARGV.shift 93 | 94 | # Parse phase 95 | buf = open(fname, 'rb').read 96 | $stderr.puts "Input: #{fname} (size: #{buf.size})" 97 | elf_header = parse(ELF32_HEADER, buf) 98 | $stderr.puts "ELF: #{elf_header}" 99 | program_headers = elf_header[:program_headers].times.collect{|i| 100 | offset = elf_header[:program_header_offset] \ 101 | + elf_header[:program_header_size] * i 102 | parse(ELF32_PROGRAM_HEADER, buf[offset..-1]) 103 | } 104 | mcfg_seg = proc{|seg| 105 | $stderr.puts "MCFG_SEG: #{seg}" 106 | require 'stringio' 107 | StringIO::new(buf[seg[:offset], seg[:file_bytes]]) 108 | }.call(program_headers[2]) 109 | mcfg_header = parse(MCFG_HEADER, mcfg_seg) 110 | $stderr.puts "MCFG: #{mcfg_header}" 111 | 112 | items = mcfg_header[:items].times.collect{|i| 113 | header = parse(ITEM_HEADER, mcfg_seg) 114 | prop, content = [nil, nil] 115 | case header[:type] 116 | when 1 # Nv 117 | prop = parse([ 118 | [:id, "v"], 119 | [:length_1, "v"], # length + 1 120 | [:magic, "C"], 121 | ], mcfg_seg) 122 | #$stderr.puts "NV(%05d, %d)"%[prop[:id], prop[:length_1] - 1] 123 | content = mcfg_seg.read(prop[:length_1] - 1) 124 | when 2, 4 # NvFile(2), File(4) 125 | is_nv = (header[:type] == 2) 126 | prop = parse([ 127 | [:magic, "v", 1], 128 | [:fname_length, "v"], # with end '\0' 129 | ], mcfg_seg) 130 | prop[:fname] = mcfg_seg.read(prop[:fname_length])[0..-2] 131 | prop.merge!(parse([ 132 | [:size_magic, "v", 2], 133 | [:length_1, "v"], # length + 1 134 | ], mcfg_seg)) 135 | prop.merge!(parse([[:data_magic, "C"]], mcfg_seg)) if prop[:length_1] > 0 136 | #$stderr.puts "#{is_nv ? "ItemFile" : "File"}(%s, %d)"%[prop[:fname], prop[:length_1] - 1] 137 | content = (prop[:length_1] > 1) ? mcfg_seg.read(prop[:length_1] - 1) : "" 138 | when 10 # trailer 139 | raise unless (i + 1 == mcfg_header[:items]) 140 | prop = parse([ 141 | [:magic, "v", 0xA1], 142 | [:length_4, "v"], # length - 4 ? 143 | ], mcfg_seg) 144 | content = mcfg_seg.read(prop[:length_4] + 4) 145 | raise unless content[0, 8] == "MCFG_TRL" 146 | #$stderr.puts "trailer: #{prop}" 147 | $stderr.puts "digest: #{content[79, 32].unpack('C*').collect{|i| "%02X"%[i]}.join(' ')}" 148 | else 149 | raise "Unknown item: #{header}" 150 | end 151 | [header, prop, content] 152 | } 153 | 154 | # Extraction phase 155 | dir_extract = "#{fname}.extracted" 156 | fname_list = File::join(dir_extract, "items.txt") 157 | proc{ 158 | require 'fileutils' 159 | FileUtils.mkdir_p(dir_extract) unless Dir::exist?(dir_extract) 160 | files = {} 161 | open(fname_list, 'w'){|io| 162 | items.each{|header, prop, content| 163 | case header[:type] 164 | when 1 # Nv 165 | io.puts([header[:type], header[:attributes], prop[:id], 166 | prop[:magic], 167 | content.unpack("C*").collect{|v| "%02X"%[v]}.join(' ')].join(',')) 168 | when 2, 4 # NvFile(2), File(4) 169 | files[prop[:fname]] ||= 0 170 | io.puts([header[:type], header[:attributes], prop[:fname], 171 | prop[:magic], prop[:size_magic], (prop[:data_magic] || -1), 172 | content.size, files[prop[:fname]]].join(',')) 173 | fname_dst = File::join(dir_extract, prop[:fname]) 174 | FileUtils.mkdir_p(File::dirname(fname_dst)) 175 | File::open(fname_dst, 'ab'){|io2| io2 << content} 176 | files[prop[:fname]] += content.size 177 | when 10 #trailer 178 | io.puts([header[:type], header[:attributes], nil, 179 | prop[:magic], prop[:length_4], 180 | content.unpack("C*").collect{|v| "%02X"%[v]}.join(' ')].join(',')) 181 | end 182 | } 183 | } unless File::exist?(fname_list) 184 | }.call 185 | 186 | # Combine phase 187 | items_new = open(fname_list, 'r').collect.with_index{|line, i| 188 | next nil if line =~ /^\s*(?:$|#)/ # accept empty line 189 | type, attri, location, *other = line.chomp.split(',') 190 | type = Integer(type) 191 | prop, content = case type 192 | when 1 # Nv 193 | content = (other[1] || "").split(/\s+/).collect{|str| str.to_i(16)}.pack("C*") 194 | prop = [Integer(location), content.length + 1, Integer(other[0])].pack("vvC") 195 | [prop, content] 196 | when 2, 4 # NvFile(2), File(4) 197 | len_content = Integer(other[3]) rescue nil # check dummy item entry 198 | src_offset = Integer(other[4]) 199 | content = (0 == len_content) \ 200 | ? "" \ 201 | : open(File::join(dir_extract, location), 'rb').read[src_offset, len_content] 202 | data_magic = Integer(other[2]) 203 | prop = [ 204 | Integer(other[0]), # magic 205 | location.length + 1, # fname_length 206 | ].pack("vv") + location + "\0" + [ 207 | Integer(other[1]), # size_magic 208 | content.length + (data_magic < 0 ? 0 : 1), # length_1 209 | ].pack("vv") + ((data_magic < 0) ? "" : [data_magic].pack("C")) 210 | [prop, content] 211 | when 10 # trailer 212 | content = other[2].split(/\s+/).collect{|str| str.to_i(16)}.pack("C*") 213 | prop = other[0..1].collect{|v| Integer(v)}.pack("vv") 214 | [prop, content] 215 | else 216 | raise 217 | end 218 | deparse(ITEM_HEADER, { 219 | :length => ITEM_HEADER.min_bytes + prop.length + content.length, 220 | :type => type, 221 | :attributes => (Integer(attri) rescue 25), # may require monkey patch 222 | :reserved => 0, 223 | }) + prop + content 224 | }.compact 225 | 226 | mcfg_seg_new = deparse( 227 | MCFG_HEADER, mcfg_header.merge({:items => items_new.length})) + items_new.join 228 | 229 | proc{ 230 | len_old = MCFG_HEADER.min_bytes \ 231 | + items.collect.with_index.to_a.inject(0){|res, v| 232 | item_old, item_new = [v[0], items_new[v[1]]] 233 | len = item_old[0][:length] 234 | len2 = item_new ? item_new.length : nil 235 | if len != len2 then 236 | #$stderr.puts "different item size @ #{v[1]}:#{item_old}: old(#{len}) != new(#{len2})" 237 | end 238 | res + len 239 | } #+ trailer[:length] 240 | $stderr.puts "main segment size: #{len_old} => #{mcfg_seg_new.length}" 241 | }.call 242 | proc{|rem_bytes| # alignment 243 | next if rem_bytes == 0 244 | mcfg_seg_new += "\0" * (program_headers[2][:align] - rem_bytes) 245 | }.call(mcfg_seg_new.length % program_headers[2][:align]) 246 | 247 | buf[program_headers[2][:offset], program_headers[2][:file_bytes]] \ 248 | = mcfg_seg_new 249 | program_headers[2][:file_bytes] = mcfg_seg_new.length 250 | buf[ 251 | elf_header[:program_header_offset] + elf_header[:program_header_size] * 2, 252 | elf_header[:program_header_size]] \ 253 | = deparse(ELF32_PROGRAM_HEADER, program_headers[2]) 254 | 255 | fname_repacked = "#{fname}.repacked" 256 | $stderr.puts "Rapacking: #{fname} => #{fname_repacked}" 257 | open(fname_repacked, 'wb').write(buf) 258 | --------------------------------------------------------------------------------