mirror of
https://github.com/RfidResearchGroup/proxmark3.git
synced 2025-02-28 01:53:11 +08:00
CHG: all @marshmellow42 's changes to make the reveng work.
ADD: lua script test for using the reveng inside lua. *prove of concept* it implements -h help -d data in a hexstring -w width of the crc family. (ie 16 gives all CRC-16 calculations. it iterates thru all found crc presets in a crc family based on the width of crc. It calcs crc and the reverse crc.
This commit is contained in:
parent
a71ece51e4
commit
60e8657796
6 changed files with 476 additions and 9 deletions
326
client/cmdcrc.c
326
client/cmdcrc.c
|
@ -8,6 +8,15 @@
|
|||
// CRC Calculations from the software reveng commands
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
#include <stdlib.h>
|
||||
#ifdef _WIN32
|
||||
# include <io.h>
|
||||
# include <fcntl.h>
|
||||
# ifndef STDIN_FILENO
|
||||
# define STDIN_FILENO 0
|
||||
# endif /* STDIN_FILENO */
|
||||
#endif /* _WIN32 */
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
//#include <stdlib.h>
|
||||
|
@ -20,6 +29,11 @@
|
|||
|
||||
#define MAX_ARGS 20
|
||||
|
||||
int uerr(char *msg){
|
||||
PrintAndLog("%s",msg);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int split(char *str, char *arr[MAX_ARGS]){
|
||||
int beginIndex = 0;
|
||||
int endIndex;
|
||||
|
@ -66,3 +80,315 @@ int CmdCrc(const char *Cmd)
|
|||
return 0;
|
||||
}
|
||||
|
||||
int GetModels(char *Models[], int *count, uint32_t *width){
|
||||
/* default values */
|
||||
static model_t model = {
|
||||
PZERO, /* no CRC polynomial, user must specify */
|
||||
PZERO, /* Init = 0 */
|
||||
P_BE, /* RefIn = false, RefOut = false, plus P_RTJUST setting in reveng.h */
|
||||
PZERO, /* XorOut = 0 */
|
||||
PZERO, /* check value unused */
|
||||
NULL /* no model name */
|
||||
};
|
||||
int ibperhx = 8;//, obperhx = 8;
|
||||
int rflags = 0, uflags = 0; /* search and UI flags */
|
||||
|
||||
poly_t apoly, crc, qpoly = PZERO, *apolys = NULL, *pptr = NULL, *qptr = NULL;
|
||||
model_t pset = model, *candmods, *mptr;
|
||||
|
||||
/* stdin must be binary */
|
||||
#ifdef _WIN32
|
||||
_setmode(STDIN_FILENO, _O_BINARY);
|
||||
#endif /* _WIN32 */
|
||||
|
||||
SETBMP();
|
||||
|
||||
int args = 0, psets, pass;
|
||||
int Cnt = 0;
|
||||
if (*width == 0) { //reveng -D
|
||||
*count = mcount();
|
||||
|
||||
if(!*count)
|
||||
return uerr("no preset models available");
|
||||
|
||||
for(int mode = 0; mode < *count; ++mode) {
|
||||
mbynum(&model, mode);
|
||||
mcanon(&model);
|
||||
size_t size = (model.name && *model.name) ? strlen(model.name) : 6;
|
||||
|
||||
char *tmp = calloc(size+1, sizeof(char));
|
||||
if (tmp==NULL)
|
||||
return uerr("out of memory?");
|
||||
|
||||
memcpy(tmp, model.name, size);
|
||||
Models[mode] = tmp;
|
||||
}
|
||||
} else { //reveng -s
|
||||
|
||||
if(~model.flags & P_MULXN)
|
||||
return uerr("cannot search for non-Williams compliant models");
|
||||
|
||||
praloc(&model.spoly, *width);
|
||||
praloc(&model.init, *width);
|
||||
praloc(&model.xorout, *width);
|
||||
if(!plen(model.spoly))
|
||||
palloc(&model.spoly, *width);
|
||||
else
|
||||
*width = plen(model.spoly);
|
||||
|
||||
/* special case if qpoly is zero, search to end of range */
|
||||
if(!ptst(qpoly))
|
||||
rflags &= ~R_HAVEQ;
|
||||
|
||||
/* if endianness not specified, try
|
||||
* little-endian then big-endian.
|
||||
* NB: crossed-endian algorithms will not be
|
||||
* searched.
|
||||
*/
|
||||
/* scan against preset models */
|
||||
if(~uflags & C_FORCE) {
|
||||
pass = 0;
|
||||
Cnt = 0;
|
||||
do {
|
||||
psets = mcount();
|
||||
|
||||
while(psets) {
|
||||
mbynum(&pset, --psets);
|
||||
|
||||
/* skip if different width, or refin or refout don't match */
|
||||
if(plen(pset.spoly) != *width || (model.flags ^ pset.flags) & (P_REFIN | P_REFOUT))
|
||||
continue;
|
||||
/* skip if the preset doesn't match specified parameters */
|
||||
if(rflags & R_HAVEP && pcmp(&model.spoly, &pset.spoly))
|
||||
continue;
|
||||
if(rflags & R_HAVEI && psncmp(&model.init, &pset.init))
|
||||
continue;
|
||||
if(rflags & R_HAVEX && psncmp(&model.xorout, &pset.xorout))
|
||||
continue;
|
||||
|
||||
apoly = pclone(pset.xorout);
|
||||
if(pset.flags & P_REFOUT)
|
||||
prev(&apoly);
|
||||
for(qptr = apolys; qptr < pptr; ++qptr) {
|
||||
crc = pcrc(*qptr, pset.spoly, pset.init, apoly, 0);
|
||||
if(ptst(crc)) {
|
||||
pfree(&crc);
|
||||
break;
|
||||
} else
|
||||
pfree(&crc);
|
||||
}
|
||||
pfree(&apoly);
|
||||
if(qptr == pptr) {
|
||||
/* the selected model solved all arguments */
|
||||
mcanon(&pset);
|
||||
|
||||
size_t size = (pset.name && *pset.name) ? strlen(pset.name) : 6;
|
||||
//PrintAndLog("Size: %d, %s, count: %d",size,pset.name, Cnt);
|
||||
char *tmp = calloc(size+1, sizeof(char));
|
||||
|
||||
if (tmp == NULL){
|
||||
PrintAndLog("out of memory?");
|
||||
return 0;
|
||||
}
|
||||
memcpy(tmp, pset.name, size);
|
||||
Models[Cnt++] = tmp;
|
||||
*count = Cnt;
|
||||
uflags |= C_RESULT;
|
||||
}
|
||||
}
|
||||
mfree(&pset);
|
||||
|
||||
/* toggle refIn/refOut and reflect arguments */
|
||||
if(~rflags & R_HAVERI) {
|
||||
model.flags ^= P_REFIN | P_REFOUT;
|
||||
for(qptr = apolys; qptr < pptr; ++qptr)
|
||||
prevch(qptr, ibperhx);
|
||||
}
|
||||
} while(~rflags & R_HAVERI && ++pass < 2);
|
||||
}
|
||||
if(uflags & C_RESULT) {
|
||||
for(qptr = apolys; qptr < pptr; ++qptr)
|
||||
pfree(qptr);
|
||||
return 1;
|
||||
}
|
||||
if(!(model.flags & P_REFIN) != !(model.flags & P_REFOUT))
|
||||
return uerr("cannot search for crossed-endian models");
|
||||
pass = 0;
|
||||
do {
|
||||
mptr = candmods = reveng(&model, qpoly, rflags, args, apolys);
|
||||
if(mptr && plen(mptr->spoly))
|
||||
uflags |= C_RESULT;
|
||||
while(mptr && plen(mptr->spoly)) {
|
||||
/* results were printed by the callback
|
||||
* string = mtostr(mptr);
|
||||
* puts(string);
|
||||
* free(string);
|
||||
*/
|
||||
mfree(mptr++);
|
||||
}
|
||||
free(candmods);
|
||||
if(~rflags & R_HAVERI) {
|
||||
model.flags ^= P_REFIN | P_REFOUT;
|
||||
for(qptr = apolys; qptr < pptr; ++qptr)
|
||||
prevch(qptr, ibperhx);
|
||||
}
|
||||
} while(~rflags & R_HAVERI && ++pass < 2);
|
||||
for(qptr = apolys; qptr < pptr; ++qptr)
|
||||
pfree(qptr);
|
||||
free(apolys);
|
||||
if(~uflags & C_RESULT)
|
||||
return uerr("no models found");
|
||||
|
||||
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
//test call to GetModels
|
||||
int CmdrevengTest(const char *Cmd){
|
||||
char *Models[80];
|
||||
int count = 0;
|
||||
uint32_t width = 0;
|
||||
width = param_get8(Cmd, 0);
|
||||
//PrintAndLog("width: %d",width);
|
||||
if (width > 89)
|
||||
return uerr("Width cannot exceed 89");
|
||||
|
||||
int ans = GetModels(Models, &count, &width);
|
||||
if (!ans) return 0;
|
||||
|
||||
PrintAndLog("Count: %d",count);
|
||||
for (int i = 0; i < count; i++){
|
||||
PrintAndLog("Model %d: %s",i,Models[i]);
|
||||
free(Models[i]);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
//-c || -v
|
||||
//inModel = valid model name string - CRC-8
|
||||
//inHexStr = input hex string to calculate crc on
|
||||
//reverse = reverse calc option if true
|
||||
//endian = {0 = calc default endian input and output, b = big endian input and output, B = big endian output, r = right justified
|
||||
// l = little endian input and output, L = little endian output only, t = left justified}
|
||||
//result = calculated crc hex string
|
||||
int RunModel(char *inModel, char *inHexStr, bool reverse, char endian, char *result){
|
||||
/* default values */
|
||||
static model_t model = {
|
||||
PZERO, // no CRC polynomial, user must specify
|
||||
PZERO, // Init = 0
|
||||
P_BE, // RefIn = false, RefOut = false, plus P_RTJUST setting in reveng.h
|
||||
PZERO, // XorOut = 0
|
||||
PZERO, // check value unused
|
||||
NULL // no model name
|
||||
};
|
||||
int ibperhx = 8, obperhx = 8;
|
||||
int rflags = 0; // search flags
|
||||
int c;
|
||||
unsigned long width = 0UL;
|
||||
poly_t apoly, crc;
|
||||
|
||||
char *string;
|
||||
|
||||
// stdin must be binary
|
||||
#ifdef _WIN32
|
||||
_setmode(STDIN_FILENO, _O_BINARY);
|
||||
#endif /* _WIN32 */
|
||||
|
||||
SETBMP();
|
||||
//set model
|
||||
if(!(c = mbynam(&model, inModel))) {
|
||||
fprintf(stderr,"error: preset model '%s' not found. Use reveng -D to list presets.\n", inModel);
|
||||
return 0;
|
||||
}
|
||||
if(c < 0)
|
||||
return uerr("no preset models available");
|
||||
|
||||
// must set width so that parameter to -ipx is not zeroed
|
||||
width = plen(model.spoly);
|
||||
rflags |= R_HAVEP | R_HAVEI | R_HAVERI | R_HAVERO | R_HAVEX;
|
||||
|
||||
//set flags
|
||||
switch (endian) {
|
||||
case 'b': /* b big-endian (RefIn = false, RefOut = false ) */
|
||||
model.flags &= ~P_REFIN;
|
||||
rflags |= R_HAVERI;
|
||||
/* fall through: */
|
||||
case 'B': /* B big-endian output (RefOut = false) */
|
||||
model.flags &= ~P_REFOUT;
|
||||
rflags |= R_HAVERO;
|
||||
mnovel(&model);
|
||||
/* fall through: */
|
||||
case 'r': /* r right-justified */
|
||||
model.flags |= P_RTJUST;
|
||||
break;
|
||||
case 'l': /* l little-endian input and output */
|
||||
model.flags |= P_REFIN;
|
||||
rflags |= R_HAVERI;
|
||||
/* fall through: */
|
||||
case 'L': /* L little-endian output */
|
||||
model.flags |= P_REFOUT;
|
||||
rflags |= R_HAVERO;
|
||||
mnovel(&model);
|
||||
/* fall through: */
|
||||
case 't': /* t left-justified */
|
||||
model.flags &= ~P_RTJUST;
|
||||
break;
|
||||
}
|
||||
|
||||
mcanon(&model);
|
||||
|
||||
if (reverse) {
|
||||
// v calculate reversed CRC
|
||||
/* Distinct from the -V switch as this causes
|
||||
* the arguments and output to be reversed as well.
|
||||
*/
|
||||
// reciprocate Poly
|
||||
prcp(&model.spoly);
|
||||
|
||||
/* mrev() does:
|
||||
* if(refout) prev(init); else prev(xorout);
|
||||
* but here the entire argument polynomial is
|
||||
* reflected, not just the characters, so RefIn
|
||||
* and RefOut are not inverted as with -V.
|
||||
* Consequently Init is the mirror image of the
|
||||
* one resulting from -V, and so we have:
|
||||
*/
|
||||
if(~model.flags & P_REFOUT) {
|
||||
prev(&model.init);
|
||||
prev(&model.xorout);
|
||||
}
|
||||
|
||||
// swap init and xorout
|
||||
apoly = model.init;
|
||||
model.init = model.xorout;
|
||||
model.xorout = apoly;
|
||||
}
|
||||
// c calculate CRC
|
||||
|
||||
/* in the Williams model, xorout is applied after the refout stage.
|
||||
* as refout is part of ptostr(), we reverse xorout here.
|
||||
*/
|
||||
if(model.flags & P_REFOUT)
|
||||
prev(&model.xorout);
|
||||
|
||||
apoly = strtop(inHexStr, model.flags, ibperhx);
|
||||
|
||||
if(reverse)
|
||||
prev(&apoly);
|
||||
|
||||
crc = pcrc(apoly, model.spoly, model.init, model.xorout, model.flags);
|
||||
|
||||
if(reverse)
|
||||
prev(&crc);
|
||||
|
||||
string = ptostr(crc, model.flags, obperhx);
|
||||
for (int i = 0; i < 50; i++){
|
||||
result[i] = string[i];
|
||||
if (result[i]==0) break;
|
||||
}
|
||||
free(string);
|
||||
pfree(&crc);
|
||||
pfree(&apoly);
|
||||
return 1;
|
||||
}
|
||||
|
|
|
@ -12,4 +12,7 @@
|
|||
#define CMDCRC_H__
|
||||
|
||||
int CmdCrc(const char *Cmd);
|
||||
|
||||
int GetModels(char *Models[], int *count, uint32_t *width);
|
||||
int RunModel(char *inModel, char *inHexStr, bool reverse, char endian, char *result);
|
||||
#endif
|
||||
|
|
|
@ -183,8 +183,10 @@ int reveng_main(int argc, char *argv[]) {
|
|||
return 0;
|
||||
//exit(EXIT_FAILURE);
|
||||
}
|
||||
if(c < 0)
|
||||
if(c < 0){
|
||||
uerror("no preset models available");
|
||||
return 0;
|
||||
}
|
||||
/* must set width so that parameter to -ipx is not zeroed */
|
||||
width = plen(model.spoly);
|
||||
rflags |= R_HAVEP | R_HAVEI | R_HAVERI | R_HAVERO | R_HAVEX;
|
||||
|
@ -314,8 +316,10 @@ ippx:
|
|||
break;
|
||||
case 'D': /* D dump all models */
|
||||
args = mcount();
|
||||
if(!args)
|
||||
if(!args){
|
||||
uerror("no preset models available");
|
||||
return 0;
|
||||
}
|
||||
for(mode = 0; mode < args; ++mode) {
|
||||
mbynum(&model, mode);
|
||||
mcanon(&model);
|
||||
|
@ -327,8 +331,10 @@ ippx:
|
|||
* either attaching names to arbitrary models or forcing to a preset
|
||||
* mmatch(&model, M_OVERWR);
|
||||
*/
|
||||
if(~model.flags & P_MULXN)
|
||||
if(~model.flags & P_MULXN){
|
||||
uerror("not a Williams model compliant algorithm");
|
||||
return 0;
|
||||
}
|
||||
string = mtostr(&model);
|
||||
puts(string);
|
||||
free(string);
|
||||
|
@ -348,10 +354,14 @@ ippx:
|
|||
}
|
||||
break;
|
||||
case 's': /* s search for algorithm */
|
||||
if(!width)
|
||||
if(!width){
|
||||
uerror("must specify positive -k or -w before -s");
|
||||
if(~model.flags & P_MULXN)
|
||||
return 0;
|
||||
}
|
||||
if(~model.flags & P_MULXN){
|
||||
uerror("cannot search for non-Williams compliant models");
|
||||
return 0;
|
||||
}
|
||||
praloc(&model.spoly, width);
|
||||
praloc(&model.init, width);
|
||||
praloc(&model.xorout, width);
|
||||
|
@ -366,8 +376,10 @@ ippx:
|
|||
|
||||
/* allocate argument array */
|
||||
args = argc - optind;
|
||||
if(!(apolys = malloc(args * sizeof(poly_t))))
|
||||
if(!(apolys = malloc(args * sizeof(poly_t)))){
|
||||
uerror("cannot allocate memory for argument list");
|
||||
return 0;
|
||||
}
|
||||
|
||||
for(pptr = apolys; optind < argc; ++optind) {
|
||||
if(uflags & C_INFILE)
|
||||
|
@ -435,8 +447,10 @@ ippx:
|
|||
return 1;
|
||||
//exit(EXIT_SUCCESS);
|
||||
}
|
||||
if(!(model.flags & P_REFIN) != !(model.flags & P_REFOUT))
|
||||
if(!(model.flags & P_REFIN) != !(model.flags & P_REFOUT)){
|
||||
uerror("cannot search for crossed-endian models");
|
||||
return 0;
|
||||
}
|
||||
pass = 0;
|
||||
do {
|
||||
mptr = candmods = reveng(&model, qpoly, rflags, args, apolys);
|
||||
|
@ -465,7 +479,6 @@ ippx:
|
|||
break;
|
||||
default: /* no mode specified */
|
||||
fprintf(stderr, "%s: no mode switch specified. Use %s -h for help.\n", myname, myname);
|
||||
return 0;
|
||||
//exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
#include "../common/crc64.h"
|
||||
#include "../common/sha1.h"
|
||||
#include "aes.h"
|
||||
#include "cmdcrc.h"
|
||||
/**
|
||||
* The following params expected:
|
||||
* UsbCommand c
|
||||
|
@ -388,6 +389,59 @@ static int l_sha1(lua_State *L)
|
|||
return 1;
|
||||
}
|
||||
|
||||
static int l_reveng_models(lua_State *L){
|
||||
|
||||
char *models[80];
|
||||
int count = 0;
|
||||
lua_Integer in_width = luaL_checkinteger(L, 1);
|
||||
|
||||
if( in_width > 89 ) return returnToLuaWithError(L,"Width cannot exceed 89, got %d", in_width);
|
||||
|
||||
uint32_t width = (uint32_t)in_width;
|
||||
int ans = GetModels(models, &count, &width);
|
||||
if (!ans) return 0;
|
||||
|
||||
lua_newtable(L);
|
||||
|
||||
for (int i = 0; i < count; i++){
|
||||
lua_pushstring(L, (const char*)models[i]);
|
||||
lua_rawseti(L,-2,i+1);
|
||||
free(models[i]);
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int l_reveng_RunModel(lua_State *L){
|
||||
//-c || -v
|
||||
//inModel = valid model name string - CRC-8
|
||||
//inHexStr = input hex string to calculate crc on
|
||||
//reverse = reverse calc option if true
|
||||
//endian = {0 = calc default endian input and output, b = big endian input and output, B = big endian output, r = right justified
|
||||
// l = little endian input and output, L = little endian output only, t = left justified}
|
||||
//result = calculated crc hex string
|
||||
char result[50];
|
||||
|
||||
size_t dataLen;
|
||||
const char *inModel = luaL_checklstring(L, 1, &dataLen);
|
||||
if ( dataLen < 4 ) return returnToLuaWithError(L,"Can't find model, got %s", inModel);
|
||||
|
||||
const char *inHexStr = luaL_checklstring(L, 2, &dataLen);
|
||||
if ( dataLen < 4 ) return returnToLuaWithError(L,"Hex string too short, got %d", dataLen);
|
||||
|
||||
int reverse = luaL_checkinteger(L, 3);
|
||||
const char *endian = luaL_checklstring(L, 4, &dataLen);
|
||||
|
||||
//PrintAndLog("mod: %s, hex: %s, rev %d", inModel, inHexStr, reverse);
|
||||
//int RunModel(char *inModel, char *inHexStr, bool reverse, char endian, char *result)
|
||||
int ans = RunModel( (char*)inModel, (char*)inHexStr, (bool)reverse, (char*)endian, result);
|
||||
if (!ans)
|
||||
return returnToLuaWithError(L,"Reveng failed");
|
||||
|
||||
lua_pushstring(L, (const char*)result);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Sets the lua path to include "./lualibs/?.lua", in order for a script to be
|
||||
* able to do "require('foobar')" if foobar.lua is within lualibs folder.
|
||||
|
@ -433,6 +487,8 @@ int set_pm3_libraries(lua_State *L)
|
|||
{"crc16", l_crc16},
|
||||
{"crc64", l_crc64},
|
||||
{"sha1", l_sha1},
|
||||
{"reveng_models", l_reveng_models},
|
||||
{"reveng_runmodel", l_reveng_RunModel},
|
||||
{NULL, NULL}
|
||||
};
|
||||
|
||||
|
|
69
client/scripts/e.lua
Normal file
69
client/scripts/e.lua
Normal file
|
@ -0,0 +1,69 @@
|
|||
local getopt = require('getopt')
|
||||
local utils = require('utils')
|
||||
|
||||
example = "script calculates many checksums (CRC) over the provided hex input"
|
||||
author = "Iceman"
|
||||
desc =
|
||||
[[
|
||||
This script calculates many checksums (CRS) over the provided hex input.
|
||||
|
||||
Arguments:
|
||||
-b data in hex
|
||||
-w width of the CRC algorithm. <optional> defaults to all known CRC presets.
|
||||
Examples :
|
||||
script run e -b 010203040506070809
|
||||
script run e -b 010203040506070809 -w 16
|
||||
]]
|
||||
|
||||
---
|
||||
-- A debug printout-function
|
||||
function dbg(args)
|
||||
if DEBUG then
|
||||
print("###", args)
|
||||
end
|
||||
end
|
||||
---
|
||||
-- This is only meant to be used when errors occur
|
||||
function oops(err)
|
||||
print("ERROR: ",err)
|
||||
return nil,err
|
||||
end
|
||||
---
|
||||
-- Usage help
|
||||
function help()
|
||||
print(desc)
|
||||
print("Example usage")
|
||||
print(example)
|
||||
end
|
||||
---
|
||||
-- The main entry point
|
||||
function main(args)
|
||||
|
||||
local data = '01020304'
|
||||
local width = 0
|
||||
|
||||
-- Read the parameters
|
||||
for o, a in getopt.getopt(args, 'hb:w:') do
|
||||
if o == "h" then return help() end
|
||||
if o == "b" then data = utils.ConvertHexToa end
|
||||
if o == "w" then width = a end
|
||||
end
|
||||
|
||||
print('Width of CRC: '..width..' bytes: '..data)
|
||||
print('')
|
||||
print('Model','CRC', 'CRC_Reverse')
|
||||
|
||||
local lists = core.reveng_models(width)
|
||||
for _,i in pairs(lists) do
|
||||
local one = core.reveng_runmodel(i, data, 0,0)
|
||||
local two = core.reveng_runmodel(i, data, 1,0)
|
||||
|
||||
print(i, one, two)
|
||||
end
|
||||
|
||||
if 1 == 1 then
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
main(args)
|
|
@ -248,7 +248,7 @@ char param_getchar(const char *line, int paramnum)
|
|||
|
||||
uint8_t param_get8(const char *line, int paramnum)
|
||||
{
|
||||
return param_get8ex(line, paramnum, 10, 0);
|
||||
return param_get8ex(line, paramnum, 0, 10);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
Loading…
Reference in a new issue