Okay, Nokia, boot me up!

So, after all the preparations in the previous part, we can actually look at the partitions. Well, not quite outright…

Attempt 1

Dumping and extraction of the initial boot image was honestly the hardest part for me, largely because I had to do all this via Wine. I had to use a special version of SP Flash Tool with a special version of readback configuration XML which didn’t even work as expected but at least it had read something from the boot partition. If the image is not corrupted, I’m fine with this: as long as I can boot the image via fastboot boot (remember that the bootloader was already unlocked in the very first part), everything else can be read with busybox dd from it. I’ll call this hard-obtained boot.img file the genesis image.

So, I decided to extract the genesis image structure the same way I did it for GerdaOS tree (luckily, this partition format is platform-agnostic), which involves abootimg tool. It definitely turned out that this image is incomplete. But my first idea was to actually fix the TWRP image with the genesis-extracted kernel and try booting it. So I actually needed nothing but zImage, which was unaffected and fully read. So I extracted the zImage with this Perl script and then tried putting it into TWRP tree. Still didn’t boot, although it’s clear that it tried.

Attempt 2

At this point, I almost gave up. I mean, I admitted that I’m fine with sacrificing one of the partitions. And I ran fastboot flash recovery twrp323.img from the snapshot from one of the forums (which also contained the non-working stock recovery image if anything). And it worked. So, the point is fastboot boot doesn’t have enough working memory in this device to be actually functional. This really sucks. As well as the fact that you can’t just boot into recovery with Volume Up + Power or Volume Down + Power, you have to run adb reboot recovery for that. Keeping both Volume buttons pressed certainly does something different, but it’s not booting into the recovery for sure. Bottom line: in order to boot into the recovery, you have to have a working system or Faildows-only tools. Nice, isn’t it?

Anyway, at least I managed to make boot and nvdata partition backups. That’s all I needed. TWRP 3.2.3 is fine for most purposes, and it also gives you full root ADB access. That’s what I really appreciate there. Of course, you can’t explore any modem-related stuff on a non-fully-booted system. But this is expected and also fine. Heck, it even has an internal terminal emulator - you can practically live there!

Now, this is where the pain ends and the fun comes.

First, I was able to look at the init structure of boot.img and… to find out that we don’t have many places to hook into anyway. But we’ll get to that in the next chapter.

Second, I mounted the nvdata partition right from the TWRP and found out that the file we need is called /nvdata/md/NVRAM/NVD_IMEI/MP0B_001 indeed. So, I pulled it for the analysis, and I really don’t know why it’s 120 bytes long and why are the other 12-byte blocks (except the first 2 ones) non-zero and have this repeating pattern: e9 1a 0b 2e 17 95 bd 0f 8b 86 53 72. Well, guess what? They changed the algo or at least the key mask since then! All my research is irrelevant now. Or is it?..

Well, 12-block pattern is still the case but whatever was true for the older MediaTeks is not true now. Now I have to figure it out myself. After all, I have some computer security education. Time to apply it for once, lol.

Pure algorithmic joy

So, here’s what we know for now:

  1. The IMEI file is called /nvdata/md/NVRAM/NVD_IMEI/MP0B_001.
  2. It contains 10 12-byte blocks, so the algo is probably not much different from the previously known one.
  3. Whenever IMEI is zero - or empty - of just absent - whatever, the resulting 12-byte block is set to e9 1a 0b 2e 17 95 bd 0f 8b 86 53 72.
  4. Whenever IMEI is non-zero, bytes 8 and 9 are always 0x74 and 0x79 respectively. So the algo is definitely not so different from the previous one, just uses a different mask value.

At least that’s true for my device. I also know the values which my own device IMEIs translate into. So, we know the plaintext, we know the ciphertext, we know (with very high probability) how the data is arranged and that a simple XOR is used for the first 8 values, now we just need to find the key. And it turned out that the key can be derived by just inverting (XORring with 0xff) first 10 bytes of an “empty” 12-byte block. Really. The 0x74 and 0x79 values also come from this. So, in my case, inversion result was:

1
16 e5 f4 d1 e8 6a 42 f0 74 79

Which perfectly corresponded to what I had calculated with known plaintext. And the 0x74 and 0x79 values are not so magic anymore.

So now, we have a perfect IMEI file modification algorithm for really all MediaTeks (where such a kind of NVRAM access is enabled):

  1. Take the last 12-byte block of the existing 120-byte MP0B_001 file. Invert all its bytes by XORring with 255 (0xff) and then set the last two bytes (10 and 11) to zero. This becomes the master key block.
  2. Shape the first 8 bytes of an operating block (byte 0 to 7) from 15 IMEI1 digits with 0 added to the end and each pair swapped (i.e. 357369035621901 becomes [0x53, 0x37, 0x96, 0x30, 0x65, 0x12, 0x09, 0x01]).
  3. XOR the operating block with the master key block. Result should be 12 bytes long.
  4. Set byte 10 of the resulting block to modulo 256 sum of odd positions of first 10 bytes.
  5. Set byte 11 of the resulting block to modulo 256 sum of even positions of first 10 bytes.
  6. For IMEI1, write the resulting block to the position 0 of MP0B_001 file, overwriting existing bytes.
  7. For IMEI2, repeat the steps 2 to 5 and write the resulting block to the position 12 of MP0B_001 file, overwriting existing bytes.

Here’s a function I came up with which implements this algo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//MTK IMEI file manipulation routine
//imeiFile should be a Uint8Array(120) read by FileReader or something (depending on the environment)
//imei1 and imei2 should be just strings with decimal IMEIs

function mtkImei(imeiFile, imei1, imei2 = null) {
let masterkey = imeiFile.slice(-12).map(x=>x^255)
masterkey[10] = masterkey[11] = 0
function imei2blk(imeistr, mk) {
let out = new Uint8Array(mk),
nibbles = imeistr.split('').map(Number).concat([0]), i
for(i=0;i<8;i++)
out[i] ^= nibbles[(i<<1)+1] << 4 | nibbles[i<<1]
for(i=0;i<10;i++)
out[i&1 ? 11 : 10] += out[i]
return out
}
imeiFile.set(imei2blk(imei1, masterkey), 0)
if(imei2)
imeiFile.set(imei2blk(imei2, masterkey), 12)
return imeiFile
}

And now I have to translate all this, including IMEI randomizer, into Lua 5.3. Why? Because I already have a static Lua binary for ARM! I can’t stand languages where indexing starts from 1 instead of 0 but it’s a one-time task anyway.

So, here’s the entire module recreated in Lua 5.3, very suboptimal but working:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
-- MTPhreak IMEI changer and randomizer for MediaTek NVRAM

-- Usage: lua mtphreak.lua NVRAMfile [imei1 [imei2]]
-- If no IMEIs are passed, they are randomized (with respect to Luhn checksum)

function parseImei(imeistr) -- imei string to 12-byte table
local imeiTbl = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
for i = 1, 15 do
local digit = tonumber(imeistr:sub(i,i))
local x = ((i - 1) >> 1) + 1
if i&1 == 1 then
imeiTbl[x] = imeiTbl[x] | digit
else
imeiTbl[x] = imeiTbl[x] | (digit << 4)
end
end
return imeiTbl
end

function tblToBytestring(tbl)
local s = ""
for i=1, #tbl do
s = s .. string.char(tbl[i])
end
return s
end

function encodeImei(imeistr, mk)
local opBlk = parseImei(imeistr)
for index, value in ipairs(opBlk) do
opBlk[index] = value ~ mk[index]
end
for i = 1, 10 do
local targetIndex = (i&1 == 0) and 12 or 11
opBlk[targetIndex] = (opBlk[targetIndex] + opBlk[i]) % 256
end
return tblToBytestring(opBlk)
end

function getRandomImei()
local imeiRndStr = string.format("%014d", math.random(0,10000000) .. math.random(0,10000000))
local imei = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
local revmap = {0, 2, 4, 6, 8, 1, 3, 5, 7, 9}
for i = 1, 14 do
imei[i] = tonumber(imeiRndStr:sub(i,i))
end
local oddsum = imei[1] + imei[3] + imei[5] + imei[7] + imei[9] + imei[11] + imei[13]
local evensum = revmap[imei[2] + 1] + revmap[imei[4] + 1] + revmap[imei[6] + 1]
+ revmap[imei[8] + 1] + revmap[imei[10] + 1] + revmap[imei[12] + 1] + revmap[imei[14] + 1]
local luhn = 10 - (oddsum + evensum) % 10
return imeiRndStr .. ((luhn > 9) and 0 or luhn)
end

-- main command part

local imeiFileName = arg[1]
local imeiFile = assert(io.open(imeiFileName, "rb"))
local imei1 = arg[2]
local imei2 = arg[3]

local imeiFull = imeiFile:read("*all")

imeiFile:seek("end", -12)
local masterKey = { string.byte(imeiFile:read("*all"), 1, -1) }

for index, value in ipairs(masterKey) do
masterKey[index] = value ~ 255
end

masterKey[11] = 0
masterKey[12] = 0

-- now we have the master key table ready

assert(imeiFile:close())

if imei1 == nil and imei2 == nil then -- randomize
math.randomseed(os.clock()*100000000000)
imei1 = getRandomImei()
imei2 = getRandomImei()
end

imeiFile = assert(io.open(imeiFileName, "wb"))
imeiFile:write(imeiFull)

if imei1 ~= nil then -- write the first IMEI
print('Writing IMEI 1: ' .. imei1)
local encodedImei1 = encodeImei(imei1, masterKey)
imeiFile:seek("set")
imeiFile:write(encodedImei1)
end
if imei2 ~= nil then -- write the second IMEI
print('Writing IMEI 2: ' .. imei2)
local encodedImei2 = encodeImei(imei2, masterKey)
imeiFile:seek("set", 12)
imeiFile:write(encodedImei2)
end

assert(imeiFile:close())
print('IMEIs written to ' .. imeiFileName)

Now, this is what I call open-source. Just copy, paste and run.

In the next chapter, we’ll get to the actual boot image modification. Stay tuned!

_