Just wow

You can’t get bored with Nokias. That’s what I know for sure now. Because after all seemingly successful steps, the strategy suddenly has to be completely redefined once again…

The thing is, userdata partition is encrypted anyway, even if you didn’t set up any sort of password protection. And IMEIs seem to play an important role in key generation for this partition. So, after you change them on boot (which I succeeded to achieve), you see a… password entering prompt. And after you enter 0000 (which is the default if you hadn’t set any of yours), you see this:

Decryption unsuccessful

The password you entered is correct, but unfortunately your data is corrupt.

To resume using your phone, you need to perform a factory reset.
When you set up your phone after the reset, you’ll have an opportunity to restore any data that was backed up to your Google Account.

And a single button: “Reset phone”.

But that’s only half a problem. Another half is that you HAVE TO have stock recovery to do this. TWRP 3.2.3 just doesn’t understand the necessary commands (or, more exactly, doesn’t reset their state), so you’re thrown back into it everytime on boot even if you performed the reset.

Luckily, I had the stock recovery image and rolled back successfully. But this whole thing is a clusterfuck. So, what do we do now?

The original idea was to change IMEIs on every boot. Now, it seems like we can only change them on every factory reset. And the only thing to correctly perform it is the stock recovery. That’s why we need to modify stock recovery instead of stock boot image. Which is also fine for me, however, having no TWRP around makes things much scarier. Anyway, here we go…

By the way, I found out how to enter fastboot mode from the turned-off state. This is quite trivial: press and hold Volume Down + Power AND plug in the USB cable within the following second. After the phone vibrates, don’t release the buttons until you see FASTBOOT mode on the screen. This is quite tricky to perform but actually works. For the recovery, the same story goes for Volume Up + Power. So, TWRP could be accessed in an autonomous fashion but that’s not our plan…

The recovery.img structure is much similar to boot.img structure. After all, they’re of the same nature, just serving different purposes. What it means for us is that we can use absolutely the same tooling (abootimg, for instance) to manipulate them.

So, what new things do we need in the image? Obviously, a Lua 5.3 runtime binary, the mtphreak.lua script created in the previous part, and a shell script to launch it on the file retrieved from the nvdata partition. I decided to put all this into the /sbin directory. And also, we need to reference the MTPhreak launcher in the /init.rc script to be able to launch it somehow. So, to ensure that we don’t screw up, just add the permission declarations and the explicit script launch to the on boot hook:

1
2
3
4
5
6
on boot
ifup lo
...
chmod 0775 /sbin/lua
chmod 0775 /sbin/mtphreak.sh
exec -- /system/bin/sh /sbin/mtphreak.sh

Rebuilding is like this:

1
2
(cd ramdisk && chmod -R 775 sbin && find . -print0 | cpio -o0a -H newc -R +0:+0 | gzip -9 > ../initrd.img)
../abootimg/abootimg --create ../new-recovery.img -f bootimg.cfg -k zImage -r initrd.img

Then we just reboot into fastboot and flash it:

1
2
3
adb reboot bootloader
fastboot flash recovery ../new-recovery.img
fastboot reboot

Then we boot into the system, check our IMEIs, go to Settings - System - Reset options - Erase all data (factory reset), tap “Reset phone” and “Erase everything”, wait until the process completes, wait up to 5 minutes for the first boot, go through the entire first-time setup again, check *#06# and…

And the IMEI numbers are the same, just like randomization never happened. It’s clear to me now that /nvdata/md/NVRAM/NVD_IMEI/MP0B_001 file gets rewritten from somewhere on each factory reset. We just need to find out where it’s rewritten from.

And it’s as simple as it gets: aside nvdata, there’s another partition called just nvram. The difference is the filesystem is totally non-standard, so we have to manipulate it on the binary level.

Nevertheless, I found the necessary partition offset for the same kind of 120-byte block as we got used to. I don’t exactly know whether this offset is different for every device. But it can be calculated as follows:

  1. Find the second occurrence of the MP0B_001 byte sequence in the binary dump and record its offset. In my case, it’s 0x219BE (137662).
  2. Subtract 120 from the offset. What I got is 0x21946, or 137542. This is the block start.
  3. The following 120 bytes is your IMEI block of the NVRAM.

So, the strategy is like this:

  1. Find the primary offset (that).
  2. Retrieve the 120-byte block with dd if=/dev/block/platform/mtk-msdc.0/11230000.msdc0/by-name/nvram of=/tmp/MP0B_001 bs=1 count=120 skip=[offset].
  3. Apply the mtphreak.lua script to the block in the /tmp/MP0B_001 file.
  4. Rewrite the 120-byte block with dd if=/tmp/MP0B_001 of=/dev/block/platform/mtk-msdc.0/11230000.msdc0/by-name/nvram bs=1 seek=[offset] conv=notrunc.

And keep in mind that we’ve got limited choices of finding the primary offset. Of course, busybox can also be introduced but what can it offer?

Well, we have some rudimentary versions of hexdump, printf and grep, as well as the knowledge that the MP0B_001 sequence always starts at the even offset. Which gives us the following starting point: hexdump -c /dev/block/platform/mtk-msdc.11230000/by-name/nvram | grep 'M P'. Not bad but still not perfect. We have to refine the result.

To do this, let’s first cut off all the offsets lower than 0x10000 and higher than 0xfffff, since actual IMEI file shouldn’t reside there. Then, we cut off all the calibration references.

1
hexdump -C /dev/block/platform/mtk-msdc.0/11230000.msdc0/by-name/nvram | grep '4d 50' | grep 000 | grep -v 0000 | grep -v CAL

Now I just see 3 entries, which is already fine. But we need to cut off the other two. This is quite difficult, so the only surefire way would be to remove all the non-MP0B strings, which gives us the following refined command to get the basic hex offset:

1
hexdump -C /dev/block/platform/mtk-msdc.0/11230000.msdc0/by-name/nvram | grep '4d 50' | grep 000 | grep -Ev 'CAL|0000|MP1|MP2|MP3|MP4|MP5|MP6|MP7|MP8|MP9|MPA|MPB|MPC|MPD|MPE|MPF|MP01|MP02|MP03|MP04|MP05|MP06|MP07|MP08|MP09|MP0A|MP0C|MP0D|MP0E|MP0F'

And this is the basic offset (which can be retrieved in hexadecimal form by piping the result into | cut -d ). We don’t even know the exact position of MP signature here. But it can be calculated with the shell builtins easily (yes, Nokia 1 busybox sh supports some bashisms). So, here’s the final version of /sbin/mtphreak.sh:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/system/bin/sh

DEVPREFIX=/dev/block/platform/mtk-msdc.0/11230000.msdc0/by-name

raw_offset_str=$(hexdump -C ${DEVPREFIX}/nvram | grep '4d 50' | grep 000 | grep -Ev 'CAL|0000|MP1|MP2|MP3|MP4|MP5|MP6|MP7|MP8|MP9|MPA|MPB|MPC|MPD|MPE|MPF|MP01|MP02|MP03|MP04|MP05|MP06|MP07|MP08|MP09|MP0A|MP0C|MP0D|MP0E|MP0F')

raw_offset_hex=$(echo -n $raw_offset_str | cut -f 1 -d ' ')
char_str=$(echo -n $raw_offset_str|cut -f 2 -d '|' | cut -d 'P' -f 1 | cut -d 'M' -f 1)
local_offset=${#char_str}
raw_offset_dec=$(printf "%d" 0x${raw_offset_hex})
real_offset=$(( raw_offset_dec + local_offset - 120 ))

dd if=${DEVPREFIX}/nvram of=/tmp/MP0B_001 bs=1 count=120 skip=$real_offset
/sbin/lua /sbin/mtphreak.lua /tmp/MP0B_001
dd if=/tmp/MP0B_001 of=${DEVPREFIX}/nvram bs=1 count=120 seek=$real_offset conv=notrunc

But something was still blocking it. And the worst part is that it’s not possible to debug this on a live recovery because adbd totally refuses to start. So, I decided to go full-Lua, ditching both /sbin/busybox and /sbin/mtphreak.sh.

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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
-- MTPhreak IMEI changer and randomizer for MediaTek NVRAM

-- Usage: lua mtphreak.lua NVRAMpartition [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

function findSigOffset(rawString) -- find the SECOND occurrence of the MP0B_001 string
local sig = 'MP0B_001'
local firstS, firstE = rawString:find(sig, 1, true)
local secondS, secondE = rawString:find(sig, firstE, true)
return secondS - 1
end

function ranval(frandom)
local s = frandom:read(4)
assert(s:len() == 4)
local v = 0
for i = 1, 4 do
v = 256 * v + s:byte(i)
end
return v
end

-- main command part

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

local imeiFull = imeiFile:read(0xfffff) -- we don't need upper part of the NVRAM dump

local signatureOffset = findSigOffset(imeiFull)
local masterOffset = signatureOffset - 120

print('Master offset: ' .. masterOffset)

imeiFile:seek("set", signatureOffset - 12)
local masterKey = { string.byte(imeiFile:read(12), 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

if imei1 == nil and imei2 == nil then -- randomize
local randFile = assert(io.open("/dev/urandom", "rb"))
math.randomseed(ranval(randFile))
imei1 = getRandomImei()
imei2 = getRandomImei()
assert(randFile:close())
end

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

assert(imeiFile:close())

Also, it turned out that the correct name to use from this recovery is /dev/block/platform/bootdevice/by-name/nvram. Effing great, right?

Well, it also turned out that this recovery somehow runs only the /sbin/recovery binary correctly. And I have to choose from 2 paths: either convert this entire effort to C, or modify TWRP so that the necessary recovery actions are performed in addition to the usual factory reset. I decided to go with the second one. And you’ll be able to read about the outcome in my next post.

_