mirror of
https://github.com/Proxmark/proxmark3.git
synced 2025-08-19 04:49:38 -07:00
Improve 'Magic' Mifare tags generation detection & hf mf c* commands magic 4k compatibility (#349)
* Improve 'Magic' Mifare tags detection * Magic Mifare tags detection and version printing * Magic s50/1k tag halt error correction for cload and csave * hf mf c* commands for gen1b * Use |= for bitwise operation * gen1b: don't issue wipe command and don't expect response from WUPC1 magic command after a SELECT_UID: old UID display works in hf mf csetuid * hf mf cgetsc compatibility for 4k * hf mf csave compatibility for 4k * hf mf cload compatibility for 4k, suppress halt errors messages for debug level 2 * Revert to MF_DBG_ERROR level in mifare_classic_halt() and don't issue the halt command for gen1b * Improve 'Magic' Mifare tags generation detection & hf mf c* commands magic 4k compatibility
This commit is contained in:
parent
0e2ddb4196
commit
7906cb41ff
6 changed files with 644 additions and 479 deletions
|
@ -6,10 +6,13 @@ This project uses the changelog in accordance with [keepchangelog](http://keepac
|
||||||
## [unreleased][unreleased]
|
## [unreleased][unreleased]
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
- Improved backdoor detection missbehaving magic s50/1k tag (Fl0-0)
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
- Added hf mf c* commands compatibity for 4k and gen1b backdoor (Fl0-0)
|
||||||
|
- Added backdoor detection for gen1b magic s70/4k tag (Fl0-0)
|
||||||
- Added data fsktonrz, a fsk cleaning/demodulating routine for weak fsk signal. Note: follow this up with a `data rawdemod nr` to finish demoding your signal.
|
- Added data fsktonrz, a fsk cleaning/demodulating routine for weak fsk signal. Note: follow this up with a `data rawdemod nr` to finish demoding your signal.
|
||||||
- Added lf em 410xbrute, LF EM410x reader bruteforce attack by simulating UIDs from a file (Fl0-0)
|
- Added lf em 410xbrute, LF EM410x reader bruteforce attack by simulating UIDs from a file (Fl0-0)
|
||||||
|
|
||||||
|
|
|
@ -1179,6 +1179,7 @@ void MifareCSetBlock(uint32_t arg0, uint32_t arg1, uint32_t arg2, uint8_t *datai
|
||||||
// bit 2 - need HALT after sequence
|
// bit 2 - need HALT after sequence
|
||||||
// bit 3 - need init FPGA and field before sequence
|
// bit 3 - need init FPGA and field before sequence
|
||||||
// bit 4 - need reset FPGA and LED
|
// bit 4 - need reset FPGA and LED
|
||||||
|
// bit 6 - gen1b backdoor type
|
||||||
uint8_t workFlags = arg1;
|
uint8_t workFlags = arg1;
|
||||||
uint8_t blockNo = arg2;
|
uint8_t blockNo = arg2;
|
||||||
|
|
||||||
|
@ -1217,13 +1218,15 @@ void MifareCSetBlock(uint32_t arg0, uint32_t arg1, uint32_t arg2, uint8_t *datai
|
||||||
};
|
};
|
||||||
|
|
||||||
if(mifare_classic_halt(NULL, cuid)) {
|
if(mifare_classic_halt(NULL, cuid)) {
|
||||||
if (MF_DBGLEVEL >= 1) Dbprintf("Halt error");
|
if (MF_DBGLEVEL > 2) Dbprintf("Halt error");
|
||||||
break;
|
// Continue, some magic tags misbehavies and send an answer to it.
|
||||||
|
// break;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// reset chip
|
// reset chip
|
||||||
if (needWipe){
|
// Wipe command don't work with gen1b
|
||||||
|
if (needWipe && !(workFlags & 0x40)){
|
||||||
ReaderTransmitBitsPar(wupC1,7,0, NULL);
|
ReaderTransmitBitsPar(wupC1,7,0, NULL);
|
||||||
if(!ReaderReceive(receivedAnswer, receivedAnswerPar) || (receivedAnswer[0] != 0x0a)) {
|
if(!ReaderReceive(receivedAnswer, receivedAnswerPar) || (receivedAnswer[0] != 0x0a)) {
|
||||||
if (MF_DBGLEVEL >= 1) Dbprintf("wupC1 error");
|
if (MF_DBGLEVEL >= 1) Dbprintf("wupC1 error");
|
||||||
|
@ -1237,14 +1240,19 @@ void MifareCSetBlock(uint32_t arg0, uint32_t arg1, uint32_t arg2, uint8_t *datai
|
||||||
};
|
};
|
||||||
|
|
||||||
if(mifare_classic_halt(NULL, cuid)) {
|
if(mifare_classic_halt(NULL, cuid)) {
|
||||||
if (MF_DBGLEVEL >= 1) Dbprintf("Halt error");
|
if (MF_DBGLEVEL > 2) Dbprintf("Halt error");
|
||||||
break;
|
// Continue, some magic tags misbehavies and send an answer to it.
|
||||||
|
// break;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// write block
|
// write block
|
||||||
if (workFlags & 0x02) {
|
if (workFlags & 0x02) {
|
||||||
ReaderTransmitBitsPar(wupC1,7,0, NULL);
|
ReaderTransmitBitsPar(wupC1,7,0, NULL);
|
||||||
|
|
||||||
|
// gen1b magic tag : do no issue wupC2 and don't expect 0x0a response after SELECT_UID (after getting UID from chip in 'hf mf csetuid' command)
|
||||||
|
if (!(workFlags & 0x40)) {
|
||||||
|
|
||||||
if(!ReaderReceive(receivedAnswer, receivedAnswerPar) || (receivedAnswer[0] != 0x0a)) {
|
if(!ReaderReceive(receivedAnswer, receivedAnswerPar) || (receivedAnswer[0] != 0x0a)) {
|
||||||
if (MF_DBGLEVEL >= 1) Dbprintf("wupC1 error");
|
if (MF_DBGLEVEL >= 1) Dbprintf("wupC1 error");
|
||||||
break;
|
break;
|
||||||
|
@ -1256,6 +1264,7 @@ void MifareCSetBlock(uint32_t arg0, uint32_t arg1, uint32_t arg2, uint8_t *datai
|
||||||
break;
|
break;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ((mifare_sendcmd_short(NULL, 0, 0xA0, blockNo, receivedAnswer, receivedAnswerPar, NULL) != 1) || (receivedAnswer[0] != 0x0a)) {
|
if ((mifare_sendcmd_short(NULL, 0, 0xA0, blockNo, receivedAnswer, receivedAnswerPar, NULL) != 1) || (receivedAnswer[0] != 0x0a)) {
|
||||||
if (MF_DBGLEVEL >= 1) Dbprintf("write block send command error");
|
if (MF_DBGLEVEL >= 1) Dbprintf("write block send command error");
|
||||||
|
@ -1272,10 +1281,14 @@ void MifareCSetBlock(uint32_t arg0, uint32_t arg1, uint32_t arg2, uint8_t *datai
|
||||||
};
|
};
|
||||||
|
|
||||||
if (workFlags & 0x04) {
|
if (workFlags & 0x04) {
|
||||||
|
// do no issue halt command for gen1b magic tag (#db# halt error. response len: 1)
|
||||||
|
if (!(workFlags & 0x40)) {
|
||||||
if (mifare_classic_halt(NULL, cuid)) {
|
if (mifare_classic_halt(NULL, cuid)) {
|
||||||
if (MF_DBGLEVEL >= 1) Dbprintf("Halt error");
|
if (MF_DBGLEVEL > 2) Dbprintf("Halt error");
|
||||||
break;
|
// Continue, some magic tags misbehavies and send an answer to it.
|
||||||
};
|
// break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
isOK = 1;
|
isOK = 1;
|
||||||
|
@ -1301,6 +1314,7 @@ void MifareCGetBlock(uint32_t arg0, uint32_t arg1, uint32_t arg2, uint8_t *datai
|
||||||
// bit 3 - need init FPGA and field before sequence
|
// bit 3 - need init FPGA and field before sequence
|
||||||
// bit 4 - need reset FPGA and LED
|
// bit 4 - need reset FPGA and LED
|
||||||
// bit 5 - need to set datain instead of issuing USB reply (called via ARM for StandAloneMode14a)
|
// bit 5 - need to set datain instead of issuing USB reply (called via ARM for StandAloneMode14a)
|
||||||
|
// bit 6 - gen1b backdoor type
|
||||||
uint8_t workFlags = arg0;
|
uint8_t workFlags = arg0;
|
||||||
uint8_t blockNo = arg2;
|
uint8_t blockNo = arg2;
|
||||||
|
|
||||||
|
@ -1333,13 +1347,15 @@ void MifareCGetBlock(uint32_t arg0, uint32_t arg1, uint32_t arg2, uint8_t *datai
|
||||||
if (MF_DBGLEVEL >= 1) Dbprintf("wupC1 error");
|
if (MF_DBGLEVEL >= 1) Dbprintf("wupC1 error");
|
||||||
break;
|
break;
|
||||||
};
|
};
|
||||||
|
// do no issue for gen1b magic tag
|
||||||
|
if (!(workFlags & 0x40)) {
|
||||||
ReaderTransmit(wupC2, sizeof(wupC2), NULL);
|
ReaderTransmit(wupC2, sizeof(wupC2), NULL);
|
||||||
if(!ReaderReceive(receivedAnswer, receivedAnswerPar) || (receivedAnswer[0] != 0x0a)) {
|
if(!ReaderReceive(receivedAnswer, receivedAnswerPar) || (receivedAnswer[0] != 0x0a)) {
|
||||||
if (MF_DBGLEVEL >= 1) Dbprintf("wupC2 error");
|
if (MF_DBGLEVEL >= 1) Dbprintf("wupC2 error");
|
||||||
break;
|
break;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// read block
|
// read block
|
||||||
if ((mifare_sendcmd_short(NULL, 0, 0x30, blockNo, receivedAnswer, receivedAnswerPar, NULL) != 18)) {
|
if ((mifare_sendcmd_short(NULL, 0, 0x30, blockNo, receivedAnswer, receivedAnswerPar, NULL) != 18)) {
|
||||||
|
@ -1349,10 +1365,14 @@ void MifareCGetBlock(uint32_t arg0, uint32_t arg1, uint32_t arg2, uint8_t *datai
|
||||||
memcpy(data, receivedAnswer, 18);
|
memcpy(data, receivedAnswer, 18);
|
||||||
|
|
||||||
if (workFlags & 0x04) {
|
if (workFlags & 0x04) {
|
||||||
|
// do no issue halt command for gen1b magic tag (#db# halt error. response len: 1)
|
||||||
|
if (!(workFlags & 0x40)) {
|
||||||
if (mifare_classic_halt(NULL, cuid)) {
|
if (mifare_classic_halt(NULL, cuid)) {
|
||||||
if (MF_DBGLEVEL >= 1) Dbprintf("Halt error");
|
if (MF_DBGLEVEL > 1) Dbprintf("Halt error");
|
||||||
break;
|
// Continue, some magic tags misbehavies and send an answer to it.
|
||||||
};
|
// break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
isOK = 1;
|
isOK = 1;
|
||||||
|
@ -1381,24 +1401,23 @@ void MifareCIdent(){
|
||||||
uint8_t wupC2[] = { 0x43 };
|
uint8_t wupC2[] = { 0x43 };
|
||||||
|
|
||||||
// variables
|
// variables
|
||||||
byte_t isOK = 1;
|
byte_t isOK = 0;
|
||||||
|
|
||||||
uint8_t receivedAnswer[MAX_MIFARE_FRAME_SIZE];
|
uint8_t receivedAnswer[MAX_MIFARE_FRAME_SIZE];
|
||||||
uint8_t receivedAnswerPar[MAX_MIFARE_PARITY_SIZE];
|
uint8_t receivedAnswerPar[MAX_MIFARE_PARITY_SIZE];
|
||||||
|
|
||||||
ReaderTransmitBitsPar(wupC1,7,0, NULL);
|
ReaderTransmitBitsPar(wupC1,7,0, NULL);
|
||||||
if(!ReaderReceive(receivedAnswer, receivedAnswerPar) || (receivedAnswer[0] != 0x0a)) {
|
if(ReaderReceive(receivedAnswer, receivedAnswerPar) && (receivedAnswer[0] == 0x0a)) {
|
||||||
isOK = 0;
|
isOK = 2;
|
||||||
};
|
|
||||||
|
|
||||||
ReaderTransmit(wupC2, sizeof(wupC2), NULL);
|
ReaderTransmit(wupC2, sizeof(wupC2), NULL);
|
||||||
if(!ReaderReceive(receivedAnswer, receivedAnswerPar) || (receivedAnswer[0] != 0x0a)) {
|
if(ReaderReceive(receivedAnswer, receivedAnswerPar) && (receivedAnswer[0] == 0x0a)) {
|
||||||
isOK = 0;
|
isOK = 1;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
if (mifare_classic_halt(NULL, 0)) {
|
// From iceman1001: removed the if, since some magic tags misbehavies and send an answer to it.
|
||||||
isOK = 0;
|
mifare_classic_halt(NULL, 0);
|
||||||
};
|
|
||||||
|
|
||||||
cmd_send(CMD_ACK,isOK,0,0,0,0);
|
cmd_send(CMD_ACK,isOK,0,0,0,0);
|
||||||
}
|
}
|
||||||
|
|
|
@ -411,8 +411,13 @@ int CmdHF14AReader(const char *Cmd)
|
||||||
c.arg[2] = 0;
|
c.arg[2] = 0;
|
||||||
SendCommand(&c);
|
SendCommand(&c);
|
||||||
WaitForResponse(CMD_ACK,&resp);
|
WaitForResponse(CMD_ACK,&resp);
|
||||||
uint8_t isOK = resp.arg[0] & 0xff;
|
|
||||||
PrintAndLog("Answers to chinese magic backdoor commands: %s", (isOK ? "YES" : "NO") );
|
uint8_t isGeneration = resp.arg[0] & 0xff;
|
||||||
|
switch( isGeneration ){
|
||||||
|
case 1: PrintAndLog("Answers to chinese magic backdoor commands (GEN 1a): YES"); break;
|
||||||
|
case 2: PrintAndLog("Answers to chinese magic backdoor commands (GEN 1b): YES"); break;
|
||||||
|
default: PrintAndLog("Answers to chinese magic backdoor commands: NO"); break;
|
||||||
|
}
|
||||||
|
|
||||||
// disconnect
|
// disconnect
|
||||||
c.cmd = CMD_READER_ISO_14443a;
|
c.cmd = CMD_READER_ISO_14443a;
|
||||||
|
|
159
client/cmdhfmf.c
159
client/cmdhfmf.c
|
@ -28,7 +28,6 @@
|
||||||
|
|
||||||
#define NESTED_SECTOR_RETRY 10 // how often we try mfested() until we give up
|
#define NESTED_SECTOR_RETRY 10 // how often we try mfested() until we give up
|
||||||
|
|
||||||
|
|
||||||
static int CmdHelp(const char *Cmd);
|
static int CmdHelp(const char *Cmd);
|
||||||
|
|
||||||
int CmdHF14AMifare(const char *Cmd)
|
int CmdHF14AMifare(const char *Cmd)
|
||||||
|
@ -1738,7 +1737,6 @@ int CmdHF14AMfECFill(const char *Cmd)
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
int CmdHF14AMfEKeyPrn(const char *Cmd)
|
int CmdHF14AMfEKeyPrn(const char *Cmd)
|
||||||
{
|
{
|
||||||
int i;
|
int i;
|
||||||
|
@ -1783,7 +1781,6 @@ int CmdHF14AMfEKeyPrn(const char *Cmd)
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
int CmdHF14AMfCSetUID(const char *Cmd)
|
int CmdHF14AMfCSetUID(const char *Cmd)
|
||||||
{
|
{
|
||||||
uint8_t wipeCard = 0;
|
uint8_t wipeCard = 0;
|
||||||
|
@ -1858,7 +1855,7 @@ int CmdHF14AMfCSetBlk(const char *Cmd)
|
||||||
uint8_t memBlock[16] = {0x00};
|
uint8_t memBlock[16] = {0x00};
|
||||||
uint8_t blockNo = 0;
|
uint8_t blockNo = 0;
|
||||||
bool wipeCard = false;
|
bool wipeCard = false;
|
||||||
int res;
|
int res, gen = 0;
|
||||||
|
|
||||||
if (strlen(Cmd) < 1 || param_getchar(Cmd, 0) == 'h') {
|
if (strlen(Cmd) < 1 || param_getchar(Cmd, 0) == 'h') {
|
||||||
PrintAndLog("Usage: hf mf csetblk <block number> <block data (32 hex symbols)> [w]");
|
PrintAndLog("Usage: hf mf csetblk <block number> <block data (32 hex symbols)> [w]");
|
||||||
|
@ -1868,6 +1865,8 @@ int CmdHF14AMfCSetBlk(const char *Cmd)
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
gen = mfCIdentify();
|
||||||
|
|
||||||
blockNo = param_get8(Cmd, 0);
|
blockNo = param_get8(Cmd, 0);
|
||||||
|
|
||||||
if (param_gethex(Cmd, 1, memBlock, 32)) {
|
if (param_gethex(Cmd, 1, memBlock, 32)) {
|
||||||
|
@ -1879,7 +1878,14 @@ int CmdHF14AMfCSetBlk(const char *Cmd)
|
||||||
wipeCard = (ctmp == 'w' || ctmp == 'W');
|
wipeCard = (ctmp == 'w' || ctmp == 'W');
|
||||||
PrintAndLog("--block number:%2d data:%s", blockNo, sprint_hex(memBlock, 16));
|
PrintAndLog("--block number:%2d data:%s", blockNo, sprint_hex(memBlock, 16));
|
||||||
|
|
||||||
|
if (gen == 2) {
|
||||||
|
/* generation 1b magic card */
|
||||||
|
res = mfCSetBlock(blockNo, memBlock, NULL, wipeCard, CSETBLOCK_SINGLE_OPER | CSETBLOCK_MAGIC_1B);
|
||||||
|
} else {
|
||||||
|
/* generation 1a magic card by default */
|
||||||
res = mfCSetBlock(blockNo, memBlock, NULL, wipeCard, CSETBLOCK_SINGLE_OPER);
|
res = mfCSetBlock(blockNo, memBlock, NULL, wipeCard, CSETBLOCK_SINGLE_OPER);
|
||||||
|
}
|
||||||
|
|
||||||
if (res) {
|
if (res) {
|
||||||
PrintAndLog("Can't write block. error=%d", res);
|
PrintAndLog("Can't write block. error=%d", res);
|
||||||
return 1;
|
return 1;
|
||||||
|
@ -1893,33 +1899,44 @@ int CmdHF14AMfCLoad(const char *Cmd)
|
||||||
FILE * f;
|
FILE * f;
|
||||||
char filename[FILE_PATH_SIZE] = {0x00};
|
char filename[FILE_PATH_SIZE] = {0x00};
|
||||||
char * fnameptr = filename;
|
char * fnameptr = filename;
|
||||||
char buf[64] = {0x00};
|
char buf[256] = {0x00};
|
||||||
uint8_t buf8[64] = {0x00};
|
uint8_t buf8[256] = {0x00};
|
||||||
uint8_t fillFromEmulator = 0;
|
uint8_t fillFromEmulator = 0;
|
||||||
int i, len, blockNum, flags=0;
|
int i, len, blockNum, flags = 0, gen = 0, numblock = 64;
|
||||||
|
|
||||||
if (param_getchar(Cmd, 0) == 'h' || param_getchar(Cmd, 0)== 0x00) {
|
if (param_getchar(Cmd, 0) == 'h' || param_getchar(Cmd, 0)== 0x00) {
|
||||||
PrintAndLog("It loads magic Chinese card from the file `filename.eml`");
|
PrintAndLog("It loads magic Chinese card from the file `filename.eml`");
|
||||||
PrintAndLog("or from emulator memory (option `e`)");
|
PrintAndLog("or from emulator memory (option `e`). 4K card: (option `4`)");
|
||||||
PrintAndLog("Usage: hf mf cload <file name w/o `.eml`>");
|
PrintAndLog("Usage: hf mf cload [file name w/o `.eml`][e][4]");
|
||||||
PrintAndLog(" or: hf mf cload e ");
|
PrintAndLog(" or: hf mf cload e [4]");
|
||||||
PrintAndLog(" sample: hf mf cload filename");
|
PrintAndLog("Sample: hf mf cload filename");
|
||||||
|
PrintAndLog(" hf mf cload filname 4");
|
||||||
|
PrintAndLog(" hf mf cload e");
|
||||||
|
PrintAndLog(" hf mf cload e 4");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
char ctmp = param_getchar(Cmd, 0);
|
char ctmp = param_getchar(Cmd, 0);
|
||||||
if (ctmp == 'e' || ctmp == 'E') fillFromEmulator = 1;
|
if (ctmp == 'e' || ctmp == 'E') fillFromEmulator = 1;
|
||||||
|
ctmp = param_getchar(Cmd, 1);
|
||||||
|
if (ctmp == '4') numblock = 256;
|
||||||
|
|
||||||
|
gen = mfCIdentify();
|
||||||
|
PrintAndLog("Loading magic mifare %dK", numblock == 256 ? 4:1);
|
||||||
|
|
||||||
if (fillFromEmulator) {
|
if (fillFromEmulator) {
|
||||||
for (blockNum = 0; blockNum < 16 * 4; blockNum += 1) {
|
for (blockNum = 0; blockNum < numblock; blockNum += 1) {
|
||||||
if (mfEmlGetMem(buf8, blockNum, 1)) {
|
if (mfEmlGetMem(buf8, blockNum, 1)) {
|
||||||
PrintAndLog("Cant get block: %d", blockNum);
|
PrintAndLog("Cant get block: %d", blockNum);
|
||||||
return 2;
|
return 2;
|
||||||
}
|
}
|
||||||
if (blockNum == 0) flags = CSETBLOCK_INIT_FIELD + CSETBLOCK_WUPC; // switch on field and send magic sequence
|
if (blockNum == 0) flags = CSETBLOCK_INIT_FIELD + CSETBLOCK_WUPC; // switch on field and send magic sequence
|
||||||
if (blockNum == 1) flags = 0; // just write
|
if (blockNum == 1) flags = 0; // just write
|
||||||
if (blockNum == 16 * 4 - 1) flags = CSETBLOCK_HALT + CSETBLOCK_RESET_FIELD; // Done. Magic Halt and switch off field.
|
if (blockNum == numblock - 1) flags = CSETBLOCK_HALT + CSETBLOCK_RESET_FIELD; // Done. Magic Halt and switch off field.
|
||||||
|
|
||||||
|
if (gen == 2)
|
||||||
|
/* generation 1b magic card */
|
||||||
|
flags |= CSETBLOCK_MAGIC_1B;
|
||||||
if (mfCSetBlock(blockNum, buf8, NULL, 0, flags)) {
|
if (mfCSetBlock(blockNum, buf8, NULL, 0, flags)) {
|
||||||
PrintAndLog("Cant set magic card block: %d", blockNum);
|
PrintAndLog("Cant set magic card block: %d", blockNum);
|
||||||
return 3;
|
return 3;
|
||||||
|
@ -1927,10 +1944,12 @@ int CmdHF14AMfCLoad(const char *Cmd)
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
} else {
|
} else {
|
||||||
len = strlen(Cmd);
|
param_getstr(Cmd, 0, filename);
|
||||||
|
|
||||||
|
len = strlen(filename);
|
||||||
if (len > FILE_PATH_SIZE - 5) len = FILE_PATH_SIZE - 5;
|
if (len > FILE_PATH_SIZE - 5) len = FILE_PATH_SIZE - 5;
|
||||||
|
|
||||||
memcpy(filename, Cmd, len);
|
//memcpy(filename, Cmd, len);
|
||||||
fnameptr += len;
|
fnameptr += len;
|
||||||
|
|
||||||
sprintf(fnameptr, ".eml");
|
sprintf(fnameptr, ".eml");
|
||||||
|
@ -1965,8 +1984,11 @@ int CmdHF14AMfCLoad(const char *Cmd)
|
||||||
|
|
||||||
if (blockNum == 0) flags = CSETBLOCK_INIT_FIELD + CSETBLOCK_WUPC; // switch on field and send magic sequence
|
if (blockNum == 0) flags = CSETBLOCK_INIT_FIELD + CSETBLOCK_WUPC; // switch on field and send magic sequence
|
||||||
if (blockNum == 1) flags = 0; // just write
|
if (blockNum == 1) flags = 0; // just write
|
||||||
if (blockNum == 16 * 4 - 1) flags = CSETBLOCK_HALT + CSETBLOCK_RESET_FIELD; // Done. Switch off field.
|
if (blockNum == numblock - 1) flags = CSETBLOCK_HALT + CSETBLOCK_RESET_FIELD; // Done. Switch off field.
|
||||||
|
|
||||||
|
if (gen == 2)
|
||||||
|
/* generation 1b magic card */
|
||||||
|
flags |= CSETBLOCK_MAGIC_1B;
|
||||||
if (mfCSetBlock(blockNum, buf8, NULL, 0, flags)) {
|
if (mfCSetBlock(blockNum, buf8, NULL, 0, flags)) {
|
||||||
PrintAndLog("Can't set magic card block: %d", blockNum);
|
PrintAndLog("Can't set magic card block: %d", blockNum);
|
||||||
fclose(f);
|
fclose(f);
|
||||||
|
@ -1974,12 +1996,13 @@ int CmdHF14AMfCLoad(const char *Cmd)
|
||||||
}
|
}
|
||||||
blockNum++;
|
blockNum++;
|
||||||
|
|
||||||
if (blockNum >= 16 * 4) break; // magic card type - mifare 1K
|
if (blockNum >= numblock) break; // magic card type - mifare 1K 64 blocks, mifare 4k 256 blocks
|
||||||
}
|
}
|
||||||
fclose(f);
|
fclose(f);
|
||||||
|
|
||||||
if (blockNum != 16 * 4 && blockNum != 32 * 4 + 8 * 16){
|
//if (blockNum != 16 * 4 && blockNum != 32 * 4 + 8 * 16){
|
||||||
PrintAndLog("File content error. There must be 64 blocks");
|
if (blockNum != numblock){
|
||||||
|
PrintAndLog("File content error. There must be %d blocks", numblock);
|
||||||
return 4;
|
return 4;
|
||||||
}
|
}
|
||||||
PrintAndLog("Loaded from file: %s", filename);
|
PrintAndLog("Loaded from file: %s", filename);
|
||||||
|
@ -1991,7 +2014,7 @@ int CmdHF14AMfCLoad(const char *Cmd)
|
||||||
int CmdHF14AMfCGetBlk(const char *Cmd) {
|
int CmdHF14AMfCGetBlk(const char *Cmd) {
|
||||||
uint8_t memBlock[16];
|
uint8_t memBlock[16];
|
||||||
uint8_t blockNo = 0;
|
uint8_t blockNo = 0;
|
||||||
int res;
|
int res, gen = 0;
|
||||||
memset(memBlock, 0x00, sizeof(memBlock));
|
memset(memBlock, 0x00, sizeof(memBlock));
|
||||||
|
|
||||||
if (strlen(Cmd) < 1 || param_getchar(Cmd, 0) == 'h') {
|
if (strlen(Cmd) < 1 || param_getchar(Cmd, 0) == 'h') {
|
||||||
|
@ -2001,11 +2024,19 @@ int CmdHF14AMfCGetBlk(const char *Cmd) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
gen = mfCIdentify();
|
||||||
|
|
||||||
blockNo = param_get8(Cmd, 0);
|
blockNo = param_get8(Cmd, 0);
|
||||||
|
|
||||||
PrintAndLog("--block number:%2d ", blockNo);
|
PrintAndLog("--block number:%2d ", blockNo);
|
||||||
|
|
||||||
|
if (gen == 2) {
|
||||||
|
/* generation 1b magic card */
|
||||||
|
res = mfCGetBlock(blockNo, memBlock, CSETBLOCK_SINGLE_OPER | CSETBLOCK_MAGIC_1B);
|
||||||
|
} else {
|
||||||
|
/* generation 1a magic card by default */
|
||||||
res = mfCGetBlock(blockNo, memBlock, CSETBLOCK_SINGLE_OPER);
|
res = mfCGetBlock(blockNo, memBlock, CSETBLOCK_SINGLE_OPER);
|
||||||
|
}
|
||||||
if (res) {
|
if (res) {
|
||||||
PrintAndLog("Can't read block. error=%d", res);
|
PrintAndLog("Can't read block. error=%d", res);
|
||||||
return 1;
|
return 1;
|
||||||
|
@ -2015,11 +2046,10 @@ int CmdHF14AMfCGetBlk(const char *Cmd) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
int CmdHF14AMfCGetSc(const char *Cmd) {
|
int CmdHF14AMfCGetSc(const char *Cmd) {
|
||||||
uint8_t memBlock[16] = {0x00};
|
uint8_t memBlock[16] = {0x00};
|
||||||
uint8_t sectorNo = 0;
|
uint8_t sectorNo = 0;
|
||||||
int i, res, flags;
|
int i, res, flags, gen = 0, baseblock = 0, sect_size = 4;
|
||||||
|
|
||||||
if (strlen(Cmd) < 1 || param_getchar(Cmd, 0) == 'h') {
|
if (strlen(Cmd) < 1 || param_getchar(Cmd, 0) == 'h') {
|
||||||
PrintAndLog("Usage: hf mf cgetsc <sector number>");
|
PrintAndLog("Usage: hf mf cgetsc <sector number>");
|
||||||
|
@ -2029,25 +2059,40 @@ int CmdHF14AMfCGetSc(const char *Cmd) {
|
||||||
}
|
}
|
||||||
|
|
||||||
sectorNo = param_get8(Cmd, 0);
|
sectorNo = param_get8(Cmd, 0);
|
||||||
if (sectorNo > 15) {
|
|
||||||
PrintAndLog("Sector number must be in [0..15] as in MIFARE classic.");
|
if (sectorNo > 39) {
|
||||||
|
PrintAndLog("Sector number must be in [0..15] in MIFARE classic 1k and [0..39] in MIFARE classic 4k.");
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
PrintAndLog("--sector number:%d ", sectorNo);
|
PrintAndLog("--sector number:%d ", sectorNo);
|
||||||
|
|
||||||
flags = CSETBLOCK_INIT_FIELD + CSETBLOCK_WUPC;
|
gen = mfCIdentify();
|
||||||
for (i = 0; i < 4; i++) {
|
|
||||||
if (i == 1) flags = 0;
|
|
||||||
if (i == 3) flags = CSETBLOCK_HALT + CSETBLOCK_RESET_FIELD;
|
|
||||||
|
|
||||||
res = mfCGetBlock(sectorNo * 4 + i, memBlock, flags);
|
flags = CSETBLOCK_INIT_FIELD + CSETBLOCK_WUPC;
|
||||||
|
if (sectorNo < 32 ) {
|
||||||
|
baseblock = sectorNo * 4;
|
||||||
|
} else {
|
||||||
|
baseblock = 128 + 16 * (sectorNo - 32);
|
||||||
|
|
||||||
|
}
|
||||||
|
if (sectorNo > 31) sect_size = 16;
|
||||||
|
|
||||||
|
for (i = 0; i < sect_size; i++) {
|
||||||
|
if (i == 1) flags = 0;
|
||||||
|
if (i == sect_size - 1) flags = CSETBLOCK_HALT + CSETBLOCK_RESET_FIELD;
|
||||||
|
|
||||||
|
if (gen == 2)
|
||||||
|
/* generation 1b magic card */
|
||||||
|
flags |= CSETBLOCK_MAGIC_1B;
|
||||||
|
|
||||||
|
res = mfCGetBlock(baseblock + i, memBlock, flags);
|
||||||
if (res) {
|
if (res) {
|
||||||
PrintAndLog("Can't read block. %d error=%d", sectorNo * 4 + i, res);
|
PrintAndLog("Can't read block. %d error=%d", baseblock + i, res);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
PrintAndLog("block %3d data:%s", sectorNo * 4 + i, sprint_hex(memBlock, 16));
|
PrintAndLog("block %3d data:%s", baseblock + i, sprint_hex(memBlock, 16));
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
@ -2059,31 +2104,44 @@ int CmdHF14AMfCSave(const char *Cmd) {
|
||||||
char filename[FILE_PATH_SIZE] = {0x00};
|
char filename[FILE_PATH_SIZE] = {0x00};
|
||||||
char * fnameptr = filename;
|
char * fnameptr = filename;
|
||||||
uint8_t fillFromEmulator = 0;
|
uint8_t fillFromEmulator = 0;
|
||||||
uint8_t buf[64] = {0x00};
|
uint8_t buf[256] = {0x00};
|
||||||
int i, j, len, flags;
|
int i, j, len, flags, gen = 0, numblock = 64;
|
||||||
|
|
||||||
// memset(filename, 0, sizeof(filename));
|
// memset(filename, 0, sizeof(filename));
|
||||||
// memset(buf, 0, sizeof(buf));
|
// memset(buf, 0, sizeof(buf));
|
||||||
|
|
||||||
if (param_getchar(Cmd, 0) == 'h') {
|
if (param_getchar(Cmd, 0) == 'h') {
|
||||||
PrintAndLog("It saves `magic Chinese` card dump into the file `filename.eml` or `cardID.eml`");
|
PrintAndLog("It saves `magic Chinese` card dump into the file `filename.eml` or `cardID.eml`");
|
||||||
PrintAndLog("or into emulator memory (option `e`)");
|
PrintAndLog("or into emulator memory (option `e`). 4K card: (option `4`)");
|
||||||
PrintAndLog("Usage: hf mf esave [file name w/o `.eml`][e]");
|
PrintAndLog("Usage: hf mf esave [file name w/o `.eml`][e][4]");
|
||||||
PrintAndLog(" sample: hf mf esave ");
|
PrintAndLog("Sample: hf mf esave ");
|
||||||
PrintAndLog(" hf mf esave filename");
|
PrintAndLog(" hf mf esave filename");
|
||||||
PrintAndLog(" hf mf esave e \n");
|
PrintAndLog(" hf mf esave e");
|
||||||
|
PrintAndLog(" hf mf esave 4");
|
||||||
|
PrintAndLog(" hf mf esave filename 4");
|
||||||
|
PrintAndLog(" hf mf esave e 4");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
char ctmp = param_getchar(Cmd, 0);
|
char ctmp = param_getchar(Cmd, 0);
|
||||||
if (ctmp == 'e' || ctmp == 'E') fillFromEmulator = 1;
|
if (ctmp == 'e' || ctmp == 'E') fillFromEmulator = 1;
|
||||||
|
if (ctmp == '4') numblock = 256;
|
||||||
|
ctmp = param_getchar(Cmd, 1);
|
||||||
|
if (ctmp == '4') numblock = 256;
|
||||||
|
|
||||||
|
gen = mfCIdentify();
|
||||||
|
PrintAndLog("Saving magic mifare %dK", numblock == 256 ? 4:1);
|
||||||
|
|
||||||
if (fillFromEmulator) {
|
if (fillFromEmulator) {
|
||||||
// put into emulator
|
// put into emulator
|
||||||
flags = CSETBLOCK_INIT_FIELD + CSETBLOCK_WUPC;
|
flags = CSETBLOCK_INIT_FIELD + CSETBLOCK_WUPC;
|
||||||
for (i = 0; i < 16 * 4; i++) {
|
for (i = 0; i < numblock; i++) {
|
||||||
if (i == 1) flags = 0;
|
if (i == 1) flags = 0;
|
||||||
if (i == 16 * 4 - 1) flags = CSETBLOCK_HALT + CSETBLOCK_RESET_FIELD;
|
if (i == numblock - 1) flags = CSETBLOCK_HALT + CSETBLOCK_RESET_FIELD;
|
||||||
|
|
||||||
|
if (gen == 2)
|
||||||
|
/* generation 1b magic card */
|
||||||
|
flags |= CSETBLOCK_MAGIC_1B;
|
||||||
|
|
||||||
if (mfCGetBlock(i, buf, flags)) {
|
if (mfCGetBlock(i, buf, flags)) {
|
||||||
PrintAndLog("Cant get block: %d", i);
|
PrintAndLog("Cant get block: %d", i);
|
||||||
|
@ -2097,12 +2155,20 @@ int CmdHF14AMfCSave(const char *Cmd) {
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
} else {
|
} else {
|
||||||
len = strlen(Cmd);
|
param_getstr(Cmd, 0, filename);
|
||||||
|
|
||||||
|
len = strlen(filename);
|
||||||
if (len > FILE_PATH_SIZE - 5) len = FILE_PATH_SIZE - 5;
|
if (len > FILE_PATH_SIZE - 5) len = FILE_PATH_SIZE - 5;
|
||||||
|
|
||||||
if (len < 1) {
|
ctmp = param_getchar(Cmd, 0);
|
||||||
|
if (len < 1 || (ctmp == '4')) {
|
||||||
// get filename
|
// get filename
|
||||||
if (mfCGetBlock(0, buf, CSETBLOCK_SINGLE_OPER)) {
|
|
||||||
|
flags = CSETBLOCK_SINGLE_OPER;
|
||||||
|
if (gen == 2)
|
||||||
|
/* generation 1b magic card */
|
||||||
|
flags |= CSETBLOCK_MAGIC_1B;
|
||||||
|
if (mfCGetBlock(0, buf, flags)) {
|
||||||
PrintAndLog("Cant get block: %d", 0);
|
PrintAndLog("Cant get block: %d", 0);
|
||||||
len = sprintf(fnameptr, "dump");
|
len = sprintf(fnameptr, "dump");
|
||||||
fnameptr += len;
|
fnameptr += len;
|
||||||
|
@ -2112,7 +2178,7 @@ int CmdHF14AMfCSave(const char *Cmd) {
|
||||||
sprintf(fnameptr, "%02x", buf[j]);
|
sprintf(fnameptr, "%02x", buf[j]);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
memcpy(filename, Cmd, len);
|
//memcpy(filename, Cmd, len);
|
||||||
fnameptr += len;
|
fnameptr += len;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2128,10 +2194,13 @@ int CmdHF14AMfCSave(const char *Cmd) {
|
||||||
|
|
||||||
// put hex
|
// put hex
|
||||||
flags = CSETBLOCK_INIT_FIELD + CSETBLOCK_WUPC;
|
flags = CSETBLOCK_INIT_FIELD + CSETBLOCK_WUPC;
|
||||||
for (i = 0; i < 16 * 4; i++) {
|
for (i = 0; i < numblock; i++) {
|
||||||
if (i == 1) flags = 0;
|
if (i == 1) flags = 0;
|
||||||
if (i == 16 * 4 - 1) flags = CSETBLOCK_HALT + CSETBLOCK_RESET_FIELD;
|
if (i == numblock - 1) flags = CSETBLOCK_HALT + CSETBLOCK_RESET_FIELD;
|
||||||
|
|
||||||
|
if (gen == 2)
|
||||||
|
/* generation 1b magic card */
|
||||||
|
flags |= CSETBLOCK_MAGIC_1B;
|
||||||
if (mfCGetBlock(i, buf, flags)) {
|
if (mfCGetBlock(i, buf, flags)) {
|
||||||
PrintAndLog("Cant get block: %d", i);
|
PrintAndLog("Cant get block: %d", i);
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -23,6 +23,8 @@
|
||||||
#include "util.h"
|
#include "util.h"
|
||||||
#include "iso14443crc.h"
|
#include "iso14443crc.h"
|
||||||
|
|
||||||
|
#include "mifare.h"
|
||||||
|
|
||||||
// mifare tracer flags used in mfTraceDecode()
|
// mifare tracer flags used in mfTraceDecode()
|
||||||
#define TRACE_IDLE 0x00
|
#define TRACE_IDLE 0x00
|
||||||
#define TRACE_AUTH1 0x01
|
#define TRACE_AUTH1 0x01
|
||||||
|
@ -452,8 +454,18 @@ int mfCSetBlock(uint8_t blockNo, uint8_t *data, uint8_t *uid, bool wantWipe, uin
|
||||||
int mfCSetUID(uint8_t *uid, uint8_t *atqa, uint8_t *sak, uint8_t *oldUID, bool wantWipe) {
|
int mfCSetUID(uint8_t *uid, uint8_t *atqa, uint8_t *sak, uint8_t *oldUID, bool wantWipe) {
|
||||||
uint8_t oldblock0[16] = {0x00};
|
uint8_t oldblock0[16] = {0x00};
|
||||||
uint8_t block0[16] = {0x00};
|
uint8_t block0[16] = {0x00};
|
||||||
|
int old, gen = 0;
|
||||||
|
|
||||||
|
gen = mfCIdentify();
|
||||||
|
|
||||||
|
if (gen == 2) {
|
||||||
|
/* generation 1b magic card */
|
||||||
|
old = mfCGetBlock(0, oldblock0, CSETBLOCK_SINGLE_OPER | CSETBLOCK_MAGIC_1B);
|
||||||
|
} else {
|
||||||
|
/* generation 1a magic card by default */
|
||||||
|
old = mfCGetBlock(0, oldblock0, CSETBLOCK_SINGLE_OPER);
|
||||||
|
}
|
||||||
|
|
||||||
int old = mfCGetBlock(0, oldblock0, CSETBLOCK_SINGLE_OPER);
|
|
||||||
if (old == 0) {
|
if (old == 0) {
|
||||||
memcpy(block0, oldblock0, 16);
|
memcpy(block0, oldblock0, 16);
|
||||||
PrintAndLog("old block 0: %s", sprint_hex(block0,16));
|
PrintAndLog("old block 0: %s", sprint_hex(block0,16));
|
||||||
|
@ -474,7 +486,14 @@ int mfCSetUID(uint8_t *uid, uint8_t *atqa, uint8_t *sak, uint8_t *oldUID, bool w
|
||||||
block0[7]=atqa[0];
|
block0[7]=atqa[0];
|
||||||
}
|
}
|
||||||
PrintAndLog("new block 0: %s", sprint_hex(block0,16));
|
PrintAndLog("new block 0: %s", sprint_hex(block0,16));
|
||||||
|
|
||||||
|
if (gen == 2) {
|
||||||
|
/* generation 1b magic card */
|
||||||
|
return mfCSetBlock(0, block0, oldUID, wantWipe, CSETBLOCK_SINGLE_OPER | CSETBLOCK_MAGIC_1B);
|
||||||
|
} else {
|
||||||
|
/* generation 1a magic card by default */
|
||||||
return mfCSetBlock(0, block0, oldUID, wantWipe, CSETBLOCK_SINGLE_OPER);
|
return mfCSetBlock(0, block0, oldUID, wantWipe, CSETBLOCK_SINGLE_OPER);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SNIFFER
|
// SNIFFER
|
||||||
|
@ -820,3 +839,50 @@ int tryDecryptWord(uint32_t nt, uint32_t ar_enc, uint32_t at_enc, uint8_t *data,
|
||||||
crypto1_destroy(traceCrypto1);
|
crypto1_destroy(traceCrypto1);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int mfCIdentify()
|
||||||
|
{
|
||||||
|
UsbCommand c = {CMD_READER_ISO_14443a, {ISO14A_CONNECT | ISO14A_NO_DISCONNECT, 0, 0}};
|
||||||
|
SendCommand(&c);
|
||||||
|
|
||||||
|
UsbCommand resp;
|
||||||
|
WaitForResponse(CMD_ACK,&resp);
|
||||||
|
|
||||||
|
iso14a_card_select_t card;
|
||||||
|
memcpy(&card, (iso14a_card_select_t *)resp.d.asBytes, sizeof(iso14a_card_select_t));
|
||||||
|
|
||||||
|
uint64_t select_status = resp.arg[0]; // 0: couldn't read, 1: OK, with ATS, 2: OK, no ATS, 3: proprietary Anticollision
|
||||||
|
|
||||||
|
if(select_status != 0) {
|
||||||
|
uint8_t rats[] = { 0xE0, 0x80 }; // FSDI=8 (FSD=256), CID=0
|
||||||
|
c.arg[0] = ISO14A_RAW | ISO14A_APPEND_CRC | ISO14A_NO_DISCONNECT;
|
||||||
|
c.arg[1] = 2;
|
||||||
|
c.arg[2] = 0;
|
||||||
|
memcpy(c.d.asBytes, rats, 2);
|
||||||
|
SendCommand(&c);
|
||||||
|
WaitForResponse(CMD_ACK,&resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
c.cmd = CMD_MIFARE_CIDENT;
|
||||||
|
c.arg[0] = 0;
|
||||||
|
c.arg[1] = 0;
|
||||||
|
c.arg[2] = 0;
|
||||||
|
SendCommand(&c);
|
||||||
|
WaitForResponse(CMD_ACK,&resp);
|
||||||
|
|
||||||
|
uint8_t isGeneration = resp.arg[0] & 0xff;
|
||||||
|
switch( isGeneration ){
|
||||||
|
case 1: PrintAndLog("Chinese magic backdoor commands (GEN 1a) detected"); break;
|
||||||
|
case 2: PrintAndLog("Chinese magic backdoor command (GEN 1b) detected"); break;
|
||||||
|
default: PrintAndLog("No chinese magic backdoor command detected"); break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// disconnect
|
||||||
|
c.cmd = CMD_READER_ISO_14443a;
|
||||||
|
c.arg[0] = 0;
|
||||||
|
c.arg[1] = 0;
|
||||||
|
c.arg[2] = 0;
|
||||||
|
SendCommand(&c);
|
||||||
|
|
||||||
|
return (int) isGeneration;
|
||||||
|
}
|
||||||
|
|
|
@ -22,6 +22,7 @@
|
||||||
#define CSETBLOCK_INIT_FIELD 0x08
|
#define CSETBLOCK_INIT_FIELD 0x08
|
||||||
#define CSETBLOCK_RESET_FIELD 0x10
|
#define CSETBLOCK_RESET_FIELD 0x10
|
||||||
#define CSETBLOCK_SINGLE_OPER 0x1F
|
#define CSETBLOCK_SINGLE_OPER 0x1F
|
||||||
|
#define CSETBLOCK_MAGIC_1B 0x40
|
||||||
|
|
||||||
extern char logHexFileName[FILE_PATH_SIZE];
|
extern char logHexFileName[FILE_PATH_SIZE];
|
||||||
|
|
||||||
|
@ -46,4 +47,6 @@ extern int loadTraceCard(uint8_t *tuid);
|
||||||
extern int saveTraceCard(void);
|
extern int saveTraceCard(void);
|
||||||
extern int tryDecryptWord(uint32_t nt, uint32_t ar_enc, uint32_t at_enc, uint8_t *data, int len);
|
extern int tryDecryptWord(uint32_t nt, uint32_t ar_enc, uint32_t at_enc, uint8_t *data, int len);
|
||||||
|
|
||||||
|
extern int mfCIdentify();
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue