mirror of
https://github.com/RfidResearchGroup/proxmark3.git
synced 2025-07-16 10:03:04 -07:00
hf 15 restore - now uses cliparser
This commit is contained in:
parent
c04f526c3f
commit
7a04b06f52
2 changed files with 148 additions and 127 deletions
|
@ -210,21 +210,6 @@ const productName_t uidmapping[] = {
|
||||||
};
|
};
|
||||||
|
|
||||||
static int CmdHF15Help(const char *Cmd);
|
static int CmdHF15Help(const char *Cmd);
|
||||||
|
|
||||||
static int usage_15_restore(void) {
|
|
||||||
const char *options[][2] = {
|
|
||||||
{"h", "this help"},
|
|
||||||
{"-2", "use slower '1 out of 256' mode"},
|
|
||||||
{"-o", "set OPTION Flag (needed for TI)"},
|
|
||||||
{"a", "use addressed mode"},
|
|
||||||
{"r <NUM>", "numbers of retries on error, default is 3"},
|
|
||||||
{"f <filename>", "load <filename>"},
|
|
||||||
{"b <block size>", "block size, default is 4"}
|
|
||||||
};
|
|
||||||
PrintAndLogEx(NORMAL, "Usage: hf 15 restore [-2] [-o] [h] [r <NUM>] [u <UID>] [f <filename>] [b <block size>]");
|
|
||||||
PrintAndLogOptions(options, 7, 3);
|
|
||||||
return PM3_SUCCESS;
|
|
||||||
}
|
|
||||||
static int usage_15_raw(void) {
|
static int usage_15_raw(void) {
|
||||||
const char *options[][2] = {
|
const char *options[][2] = {
|
||||||
{"-r", "do not read response" },
|
{"-r", "do not read response" },
|
||||||
|
@ -1133,7 +1118,7 @@ static int CmdHF15WriteAfi(const char *Cmd) {
|
||||||
|
|
||||||
// request to be sent to device/card
|
// request to be sent to device/card
|
||||||
uint16_t flags = arg_get_raw_flag(uidlen, unaddressed, scan, add_option);
|
uint16_t flags = arg_get_raw_flag(uidlen, unaddressed, scan, add_option);
|
||||||
uint8_t req[PM3_CMD_DATA_SIZE] = {flags, ISO15_CMD_WRITEAFI};
|
uint8_t req[16] = {flags, ISO15_CMD_WRITEAFI};
|
||||||
uint16_t reqlen = 2;
|
uint16_t reqlen = 2;
|
||||||
|
|
||||||
if (unaddressed == false) {
|
if (unaddressed == false) {
|
||||||
|
@ -1230,7 +1215,9 @@ static int CmdHF15WriteDsfid(const char *Cmd) {
|
||||||
|
|
||||||
// request to be sent to device/card
|
// request to be sent to device/card
|
||||||
uint16_t flags = arg_get_raw_flag(uidlen, unaddressed, scan, add_option);
|
uint16_t flags = arg_get_raw_flag(uidlen, unaddressed, scan, add_option);
|
||||||
uint8_t req[PM3_CMD_DATA_SIZE] = {flags, ISO15_CMD_WRITEDSFID};
|
uint8_t req[16] = {flags, ISO15_CMD_WRITEDSFID};
|
||||||
|
// enforce, since we are writing
|
||||||
|
req[0] |= ISO15_REQ_OPTION;
|
||||||
uint16_t reqlen = 2;
|
uint16_t reqlen = 2;
|
||||||
|
|
||||||
if (unaddressed == false) {
|
if (unaddressed == false) {
|
||||||
|
@ -1250,9 +1237,6 @@ static int CmdHF15WriteDsfid(const char *Cmd) {
|
||||||
PrintAndLogEx(SUCCESS, "Using UID... " _GREEN_("%s"), iso15693_sprintUID(NULL, uid));
|
PrintAndLogEx(SUCCESS, "Using UID... " _GREEN_("%s"), iso15693_sprintUID(NULL, uid));
|
||||||
}
|
}
|
||||||
|
|
||||||
// enforce, since we are writing
|
|
||||||
req[0] |= ISO15_REQ_OPTION;
|
|
||||||
|
|
||||||
// dsfid
|
// dsfid
|
||||||
req[reqlen++] = (uint8_t)dsfid;
|
req[reqlen++] = (uint8_t)dsfid;
|
||||||
|
|
||||||
|
@ -1813,6 +1797,48 @@ static int CmdHF15Readblock(const char *Cmd) {
|
||||||
return PM3_SUCCESS;
|
return PM3_SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static int hf_15_write_blk(bool verbose, bool fast, uint8_t *req, uint8_t reqlen) {
|
||||||
|
|
||||||
|
uint8_t read_response = 1;
|
||||||
|
clearCommandBuffer();
|
||||||
|
SendCommandMIX(CMD_HF_ISO15693_COMMAND, reqlen, fast, read_response, req, reqlen);
|
||||||
|
PacketResponseNG resp;
|
||||||
|
if (WaitForResponseTimeout(CMD_ACK, &resp, 2000) == false) {
|
||||||
|
PrintAndLogEx(FAILED, "iso15693 card timeout, data may be written anyway");
|
||||||
|
DropField();
|
||||||
|
return PM3_ETIMEOUT;
|
||||||
|
}
|
||||||
|
|
||||||
|
DropField();
|
||||||
|
int status = resp.oldarg[0];
|
||||||
|
if (status == PM3_ETEAROFF) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status < 2) {
|
||||||
|
if (verbose) {
|
||||||
|
PrintAndLogEx(FAILED, "iso15693 command failed");
|
||||||
|
}
|
||||||
|
return PM3_EWRONGANSWER;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t *recv = resp.data.asBytes;
|
||||||
|
if (CheckCrc15(recv, status) == false) {
|
||||||
|
if (verbose) {
|
||||||
|
PrintAndLogEx(FAILED, "crc (" _RED_("fail") ")");
|
||||||
|
}
|
||||||
|
return PM3_ESOFT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((recv[0] & ISO15_RES_ERROR) == ISO15_RES_ERROR) {
|
||||||
|
if (verbose) {
|
||||||
|
PrintAndLogEx(ERR, "iso15693 card returned error %i: %s", recv[0], TagErrorStr(recv[0]));
|
||||||
|
}
|
||||||
|
return PM3_EWRONGANSWER;
|
||||||
|
}
|
||||||
|
return PM3_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Commandline handling: HF15 CMD WRITE
|
* Commandline handling: HF15 CMD WRITE
|
||||||
* Writes a single Block - might run into timeout, even when successful
|
* Writes a single Block - might run into timeout, even when successful
|
||||||
|
@ -1825,10 +1851,11 @@ static int CmdHF15Write(const char *Cmd) {
|
||||||
"hf 15 wrbl -u E011223344556677 -b 12 -d AABBCCDD"
|
"hf 15 wrbl -u E011223344556677 -b 12 -d AABBCCDD"
|
||||||
);
|
);
|
||||||
|
|
||||||
void *argtable[6+3] = {};
|
void *argtable[6+4] = {};
|
||||||
uint8_t arglen = arg_add_default(argtable);
|
uint8_t arglen = arg_add_default(argtable);
|
||||||
argtable[arglen++] = arg_int1("b", "blk", "<dec>", "page number (0-255)");
|
argtable[arglen++] = arg_int1("b", "blk", "<dec>", "page number (0-255)");
|
||||||
argtable[arglen++] = arg_str1("d", "data", "<hex>", "data, 4 bytes");
|
argtable[arglen++] = arg_str1("d", "data", "<hex>", "data, 4 bytes");
|
||||||
|
argtable[arglen++] = arg_lit0("v", "verbose", "verbose output");
|
||||||
argtable[arglen++] = arg_param_end;
|
argtable[arglen++] = arg_param_end;
|
||||||
CLIExecWithReturn(ctx, Cmd, argtable, false);
|
CLIExecWithReturn(ctx, Cmd, argtable, false);
|
||||||
|
|
||||||
|
@ -1845,6 +1872,7 @@ static int CmdHF15Write(const char *Cmd) {
|
||||||
int dlen = 0;
|
int dlen = 0;
|
||||||
CLIGetHexWithReturn(ctx, 7, d, &dlen);
|
CLIGetHexWithReturn(ctx, 7, d, &dlen);
|
||||||
|
|
||||||
|
bool verbose = arg_get_lit(ctx, 8);
|
||||||
CLIParserFree(ctx);
|
CLIParserFree(ctx);
|
||||||
|
|
||||||
// sanity checks
|
// sanity checks
|
||||||
|
@ -1858,9 +1886,18 @@ static int CmdHF15Write(const char *Cmd) {
|
||||||
return PM3_EINVARG;
|
return PM3_EINVARG;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// default fallback to scan for tag.
|
||||||
|
// overriding unaddress parameter :)
|
||||||
|
if (uidlen != 8) {
|
||||||
|
scan = true;
|
||||||
|
}
|
||||||
|
|
||||||
// request to be sent to device/card
|
// request to be sent to device/card
|
||||||
uint16_t flags = arg_get_raw_flag(uidlen, unaddressed, scan, add_option);
|
uint16_t flags = arg_get_raw_flag(uidlen, unaddressed, scan, add_option);
|
||||||
uint8_t req[PM3_CMD_DATA_SIZE] = {flags, ISO15_CMD_WRITE};
|
uint8_t req[17] = {flags, ISO15_CMD_WRITE};
|
||||||
|
|
||||||
|
// enforce, since we are writing
|
||||||
|
req[0] |= ISO15_REQ_OPTION;
|
||||||
uint16_t reqlen = 2;
|
uint16_t reqlen = 2;
|
||||||
|
|
||||||
if (unaddressed == false) {
|
if (unaddressed == false) {
|
||||||
|
@ -1880,6 +1917,7 @@ static int CmdHF15Write(const char *Cmd) {
|
||||||
PrintAndLogEx(SUCCESS, "Using UID... " _GREEN_("%s"), iso15693_sprintUID(NULL, uid));
|
PrintAndLogEx(SUCCESS, "Using UID... " _GREEN_("%s"), iso15693_sprintUID(NULL, uid));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
req[reqlen++] = (uint8_t)block;
|
req[reqlen++] = (uint8_t)block;
|
||||||
memcpy(req + reqlen, d, sizeof(d));
|
memcpy(req + reqlen, d, sizeof(d));
|
||||||
reqlen += sizeof(d);
|
reqlen += sizeof(d);
|
||||||
|
@ -1889,106 +1927,92 @@ static int CmdHF15Write(const char *Cmd) {
|
||||||
|
|
||||||
PrintAndLogEx(INFO, "iso15693 writing to page %02d (0x%02X) | data [ %s ] ", block, block, sprint_hex(req, reqlen));
|
PrintAndLogEx(INFO, "iso15693 writing to page %02d (0x%02X) | data [ %s ] ", block, block, sprint_hex(req, reqlen));
|
||||||
|
|
||||||
uint8_t read_response = 1;
|
int res = hf_15_write_blk(verbose, fast, req, reqlen);
|
||||||
PacketResponseNG resp;
|
if (res == PM3_SUCCESS)
|
||||||
clearCommandBuffer();
|
PrintAndLogEx(SUCCESS, "Write ( " _GREEN_("ok") " )");
|
||||||
SendCommandMIX(CMD_HF_ISO15693_COMMAND, reqlen, fast, read_response, req, reqlen);
|
else
|
||||||
|
PrintAndLogEx(FAILED, "Write ( " _RED_("fail") " )");
|
||||||
|
|
||||||
if (WaitForResponseTimeout(CMD_ACK, &resp, 2000) == false) {
|
|
||||||
PrintAndLogEx(FAILED, "iso15693 card timeout, data may be written anyway");
|
|
||||||
DropField();
|
|
||||||
return PM3_ETIMEOUT;
|
|
||||||
}
|
|
||||||
|
|
||||||
DropField();
|
|
||||||
|
|
||||||
int status = resp.oldarg[0];
|
|
||||||
if (status == PM3_ETEAROFF) {
|
|
||||||
return status;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status < 2) {
|
|
||||||
PrintAndLogEx(FAILED, "iso15693 command failed");
|
|
||||||
return PM3_EWRONGANSWER;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint8_t *data = resp.data.asBytes;
|
|
||||||
|
|
||||||
if (CheckCrc15(data, status) == false) {
|
|
||||||
PrintAndLogEx(FAILED, "crc (" _RED_("fail") ")");
|
|
||||||
return PM3_ESOFT;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((data[0] & ISO15_RES_ERROR) == ISO15_RES_ERROR) {
|
|
||||||
PrintAndLogEx(ERR, "iso15693 card returned error %i: %s", data[0], TagErrorStr(data[0]));
|
|
||||||
return PM3_EWRONGANSWER;
|
|
||||||
}
|
|
||||||
|
|
||||||
PrintAndLogEx(SUCCESS, "Write ( " _GREEN_("ok") " )");
|
|
||||||
return PM3_SUCCESS;
|
return PM3_SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int CmdHF15Restore(const char *Cmd) {
|
static int CmdHF15Restore(const char *Cmd) {
|
||||||
|
CLIParserContext *ctx;
|
||||||
|
CLIParserInit(&ctx, "hf 15 restore",
|
||||||
|
"This command restore the contents of a dump file onto a ISO-15693 tag",
|
||||||
|
"hf 15 restore\n"
|
||||||
|
"hf 15 restore -*\n"
|
||||||
|
"hf 15 restore -u E011223344556677 -f hf-15-my-dump.bin"
|
||||||
|
);
|
||||||
|
|
||||||
char newPrefix[60] = {0x00};
|
void *argtable[6+5] = {};
|
||||||
char filename[FILE_PATH_SIZE] = {0x00};
|
uint8_t arglen = arg_add_default(argtable);
|
||||||
size_t blocksize = 4;
|
argtable[arglen++] = arg_str0("f", "file", "<fn>", "filename of dump"),
|
||||||
uint8_t cmdp = 0, retries = 3;
|
argtable[arglen++] = arg_int0("r", "retry", "<dec>", "number of retries (def 3)"),
|
||||||
bool addressed_mode = false;
|
argtable[arglen++] = arg_int0(NULL, "bs", "<dec>", "block size (def 4)"),
|
||||||
|
argtable[arglen++] = arg_lit0("v", "verbose", "verbose output");
|
||||||
|
argtable[arglen++] = arg_param_end;
|
||||||
|
CLIExecWithReturn(ctx, Cmd, argtable, true);
|
||||||
|
|
||||||
while (param_getchar(Cmd, cmdp) != 0x00) {
|
uint8_t uid[8];
|
||||||
switch (tolower(param_getchar(Cmd, cmdp))) {
|
int uidlen = 0;
|
||||||
case '-': {
|
CLIGetHexWithReturn(ctx, 1, uid, &uidlen);
|
||||||
char param[3] = "";
|
bool unaddressed = arg_get_lit(ctx, 2);
|
||||||
param_getstr(Cmd, cmdp, param, sizeof(param));
|
bool scan = arg_get_lit(ctx, 3);
|
||||||
switch (param[1]) {
|
int fast = (arg_get_lit(ctx, 4) == false);
|
||||||
case '2':
|
bool add_option = arg_get_lit(ctx, 5);
|
||||||
case 'o':
|
|
||||||
sprintf(newPrefix, " %s", param);
|
int fnlen = 0;
|
||||||
break;
|
char filename[FILE_PATH_SIZE] = {0};
|
||||||
default:
|
CLIParamStrToBuf(arg_get_str(ctx, 6), (uint8_t *)filename, FILE_PATH_SIZE, &fnlen);
|
||||||
PrintAndLogEx(WARNING, "11 unknown parameter " _YELLOW_("'%s'"), param);
|
int retries = arg_get_int_def(ctx, 7, 3);
|
||||||
return usage_15_restore();
|
int blocksize = arg_get_int_def(ctx, 8, 4);
|
||||||
}
|
bool verbose = arg_get_lit(ctx, 9);
|
||||||
break;
|
CLIParserFree(ctx);
|
||||||
}
|
|
||||||
case 'f':
|
// sanity checks
|
||||||
param_getstr(Cmd, cmdp + 1, filename, FILE_PATH_SIZE);
|
if ((scan + unaddressed + uidlen) > 1) {
|
||||||
cmdp++;
|
PrintAndLogEx(WARNING, "Select only one option /scan/unaddress/uid");
|
||||||
break;
|
return PM3_EINVARG;
|
||||||
case 'r':
|
|
||||||
retries = param_get8ex(Cmd, cmdp + 1, 3, 10);
|
|
||||||
cmdp++;
|
|
||||||
break;
|
|
||||||
case 'b':
|
|
||||||
blocksize = param_get8ex(Cmd, cmdp + 1, 4, 10);
|
|
||||||
cmdp++;
|
|
||||||
break;
|
|
||||||
case 'a':
|
|
||||||
addressed_mode = true;
|
|
||||||
break;
|
|
||||||
case 'h':
|
|
||||||
return usage_15_restore();
|
|
||||||
default:
|
|
||||||
PrintAndLogEx(WARNING, "unknown parameter " _YELLOW_("'%c'"), param_getchar(Cmd, cmdp));
|
|
||||||
return usage_15_restore();
|
|
||||||
}
|
|
||||||
cmdp++;
|
|
||||||
}
|
}
|
||||||
|
if (fnlen == 0) {
|
||||||
PrintAndLogEx(INFO, "blocksize: %zu", blocksize);
|
|
||||||
|
|
||||||
if (!strlen(filename)) {
|
|
||||||
PrintAndLogEx(WARNING, "please provide a filename");
|
PrintAndLogEx(WARNING, "please provide a filename");
|
||||||
return usage_15_restore();
|
return PM3_EINVARG;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t uid[8] = {0x00};
|
// default fallback to scan for tag.
|
||||||
if (getUID(false, uid) != PM3_SUCCESS) {
|
// overriding unaddress parameter :)
|
||||||
PrintAndLogEx(WARNING, "no tag found");
|
if (uidlen != 8) {
|
||||||
return PM3_ESOFT;
|
scan = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// request to be sent to device/card
|
||||||
|
uint16_t flags = arg_get_raw_flag(uidlen, unaddressed, scan, add_option);
|
||||||
|
uint8_t req[17] = {flags, ISO15_CMD_WRITE};
|
||||||
|
// enforce, since we are writing
|
||||||
|
req[0] |= ISO15_REQ_OPTION;
|
||||||
|
uint16_t reqlen = 2;
|
||||||
|
|
||||||
|
if (unaddressed == false) {
|
||||||
|
if (scan) {
|
||||||
|
if (getUID(false, uid) != PM3_SUCCESS) {
|
||||||
|
PrintAndLogEx(WARNING, "no tag found");
|
||||||
|
return PM3_EINVARG;
|
||||||
|
}
|
||||||
|
uidlen = 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uidlen == 8) {
|
||||||
|
// add UID (scan, uid)
|
||||||
|
memcpy(req + reqlen, uid, sizeof(uid));
|
||||||
|
reqlen += sizeof(uid);
|
||||||
|
}
|
||||||
|
PrintAndLogEx(SUCCESS, "Using UID... " _GREEN_("%s"), iso15693_sprintUID(NULL, uid));
|
||||||
|
} else {
|
||||||
|
PrintAndLogEx(SUCCESS, "Using unaddressed mode");
|
||||||
|
}
|
||||||
|
PrintAndLogEx(INFO, "Using block size... %zu", blocksize);
|
||||||
|
|
||||||
size_t datalen = 0;
|
size_t datalen = 0;
|
||||||
uint8_t *data = NULL;
|
uint8_t *data = NULL;
|
||||||
if (loadFile_safe(filename, ".bin", (void **)&data, &datalen) != PM3_SUCCESS) {
|
if (loadFile_safe(filename, ".bin", (void **)&data, &datalen) != PM3_SUCCESS) {
|
||||||
|
@ -2003,37 +2027,33 @@ static int CmdHF15Restore(const char *Cmd) {
|
||||||
}
|
}
|
||||||
|
|
||||||
PrintAndLogEx(INFO, "restoring data blocks");
|
PrintAndLogEx(INFO, "restoring data blocks");
|
||||||
|
PrintAndLogEx(INFO, "." NOLF);
|
||||||
|
fflush(stdout);
|
||||||
|
|
||||||
int retval = PM3_SUCCESS;
|
int retval = PM3_SUCCESS;
|
||||||
size_t bytes = 0;
|
size_t bytes = 0;
|
||||||
uint16_t i = 0;
|
uint16_t i = 0;
|
||||||
while (bytes < datalen) {
|
while (bytes < datalen) {
|
||||||
|
|
||||||
|
req[reqlen + 1] = i;
|
||||||
|
// copy over the data to the request
|
||||||
|
memcpy(req + reqlen + 1, data + bytes, blocksize);
|
||||||
|
AddCrc15(req, reqlen + 1 + blocksize);
|
||||||
|
|
||||||
uint8_t tried = 0;
|
uint8_t tried = 0;
|
||||||
char hex[40] = {0x00};
|
|
||||||
char tmpCmd[200] = {0x00};
|
|
||||||
|
|
||||||
if (addressed_mode) {
|
|
||||||
char uidhex[17] = {0x00};
|
|
||||||
hex_to_buffer((uint8_t *)uidhex, uid, sizeof(uid), sizeof(uidhex) - 1, 0, false, true);
|
|
||||||
hex_to_buffer((uint8_t *)hex, data + bytes, blocksize, sizeof(hex) - 1, 0, false, true);
|
|
||||||
snprintf(tmpCmd, sizeof(tmpCmd), "%s %s %u %s", newPrefix, uidhex, i, hex);
|
|
||||||
} else {
|
|
||||||
hex_to_buffer((uint8_t *)hex, data + bytes, blocksize, sizeof(hex) - 1, 0, false, true);
|
|
||||||
snprintf(tmpCmd, sizeof(tmpCmd), "%s u %u %s", newPrefix, i, hex);
|
|
||||||
}
|
|
||||||
|
|
||||||
PrintAndLogEx(DEBUG, "hf 15 write %s", tmpCmd);
|
|
||||||
|
|
||||||
for (tried = 0; tried < retries; tried++) {
|
for (tried = 0; tried < retries; tried++) {
|
||||||
retval = CmdHF15Write(tmpCmd);
|
|
||||||
if (retval == false) {
|
retval = hf_15_write_blk(verbose, fast, req, (reqlen + 1 + blocksize + 2));
|
||||||
|
if (retval == PM3_SUCCESS) {
|
||||||
|
PrintAndLogEx(NORMAL, "." NOLF);
|
||||||
|
fflush(stdout);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tried >= retries) {
|
if (tried >= retries) {
|
||||||
free(data);
|
free(data);
|
||||||
|
PrintAndLogEx(NORMAL, "");
|
||||||
PrintAndLogEx(FAILED, "restore failed. Too many retries.");
|
PrintAndLogEx(FAILED, "restore failed. Too many retries.");
|
||||||
return retval;
|
return retval;
|
||||||
}
|
}
|
||||||
|
@ -2041,6 +2061,8 @@ static int CmdHF15Restore(const char *Cmd) {
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
free(data);
|
free(data);
|
||||||
|
|
||||||
|
PrintAndLogEx(NORMAL, "");
|
||||||
PrintAndLogEx(INFO, "done");
|
PrintAndLogEx(INFO, "done");
|
||||||
PrintAndLogEx(HINT, "try `" _YELLOW_("hf 15 dump") "` to read your card to verify");
|
PrintAndLogEx(HINT, "try `" _YELLOW_("hf 15 dump") "` to read your card to verify");
|
||||||
return PM3_SUCCESS;
|
return PM3_SUCCESS;
|
||||||
|
|
|
@ -1,2 +1 @@
|
||||||
hf 15 raw
|
hf 15 raw
|
||||||
hf 15 restore
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue