From 85f19ebce976bda5d6b0231e2bde5c91e45ddcca Mon Sep 17 00:00:00 2001 From: taichunmin Date: Mon, 18 Nov 2024 11:42:19 +0800 Subject: [PATCH] mfkey32 skip records by known keys --- pages/demos.md | 4 ++-- pug/src/hf14a-scanner.pug | 2 +- pug/src/mfkey32.pug | 31 +++++++++++++++++------- pug/src/mifare-keychain.pug | 2 +- pug/src/mifare1k.pug | 2 +- src/ChameleonUltra.ts | 2 +- src/Crypto1.test.ts | 14 +++-------- src/Crypto1.ts | 47 +++++++++++++++++++++++++++++++++++-- 8 files changed, 76 insertions(+), 28 deletions(-) diff --git a/pages/demos.md b/pages/demos.md index 10870f0..7ac7eee 100644 --- a/pages/demos.md +++ b/pages/demos.md @@ -122,9 +122,9 @@ A tool for Xiaomi Watch to clone encrypted Mifare Classic tag. ## [mifare-value.html](https://taichunmin.idv.tw/chameleon-ultra.js/mifare-value.html) -在台灣有些 MIFARE Classic 卡片使用 value block 來儲存卡片的餘額,但是有些中國魔術卡不支援 value block 指令,所以無法使用這些魔術卡來複製 MIFARE Classic 卡片。這個工具可以讓你測試卡片是否支援 value block 指令。 +在台灣有些系統會使用 MIFARE Classic 卡片的 value block 來儲存餘額,value block 的 increment/decrement/restore 指令的資料是由 2 個部分所組成,在第 2 部分傳送完成後卡片不會回傳 ACK,所以讀卡機如果在 Timeout 之前沒有收到 NACK,就可以視為執行成功並繼續執行下一個指令。但有些魔術卡需要更多時間來完成指令,否則就會執行失敗,這個工具可以讓你測試卡片是否能夠成功執行 value block 的指令。 -A ChameleonUltra tool for MIFARE Classic value block commands. Some MIFARE Classic cards in Taiwan are using value block to store the balance of the card. But some chinese magic cards didn't support value block commands. So you can't use these magic cards to clone original MIFARE Classic cards. This tool can help you to test whether the card support value block commands or not. +In Taiwan, some systems use the value block of MIFARE Classic cards to store balances. The data for the value block's increment/decrement/restore commands consists of two parts. After the second part is transmitted, the card will not return ACK. Therefore, if the reader does not receive a NACK before timeout, it can be considered successful and proceed to the next command. However, some magic cards needs more time to complete the value block's command, otherwise the command will fail. This tool allows you to test whether the card can successfully execute the value block commands. ![](https://i.imgur.com/jJ3pNvn.png) diff --git a/pug/src/hf14a-scanner.pug b/pug/src/hf14a-scanner.pug index 70959f2..1fd29f2 100644 --- a/pug/src/hf14a-scanner.pug +++ b/pug/src/hf14a-scanner.pug @@ -157,7 +157,7 @@ block script async btnCopy (text, container = null) { if (!container) container = document.body const dom = document.createElement('textarea') - dom.value = text + dom.value = text = `${text}` container.appendChild(dom) dom.select() dom.setSelectionRange(0, 1e6) // For mobile devices diff --git a/pug/src/mfkey32.pug b/pug/src/mfkey32.pug index 69ba87d..154db30 100644 --- a/pug/src/mfkey32.pug +++ b/pug/src/mfkey32.pug @@ -206,7 +206,7 @@ block script await ultra.cmdHf14aSetAntiCollData(tag) await ultra.cmdMf1SetDetectionEnable(true) // enable detection const block0 = ChameleonUltra.mf1GenMagicBlock0(tag) - ultra.emitter.emit('debug', 'web', `block0 = ${block0.toString('hex')}`) + ultra.emitter.emit('debug', 'web', `block0 = ${toHex(block0)}`) await ultra.cmdMf1EmuWriteBlock(0, block0) // set block0 await ultra.cmdSlotSaveSettings() await this.swalFire({ icon: 'success', title: 'Emulate successfully!' }) @@ -234,6 +234,7 @@ block script currentProgressStep: 1, text: `Retrive logs: ${logs.length} / ${logCnt}`, })) + await this.sleep(0) // 等待 UI 更新 const tmp = await ultra.cmdMf1GetDetectionLogs(logs.length) logs.push(...tmp) } @@ -245,9 +246,8 @@ block script ultra.emitter.emit('debug', 'web', `logs = ${JSON.stringify(logs)}`) // calc how many mfkey32v2 need to be run - const bufToHex = buf => buf?.toString('hex').toUpperCase() const blockToSector = block => block < 128 ? block >>> 2 : 24 + (block >>> 4) - logs = _.values(_.groupBy(logs, log => `${bufToHex(log.uid)}-${blockToSector(log.block)}-${'AB'[+log.isKeyB]}`)) + logs = _.values(_.groupBy(logs, log => `${toHex(log.uid)}-${blockToSector(log.block)}-${'AB'[+log.isKeyB]}`)) console.log(logs) const recoverMax = _.sumBy(logs, arr => arr.length * (arr.length - 1) / 2) this.$set(this.ss, 'detects', []) @@ -258,20 +258,33 @@ block script for (let j = i + 1; j < grp.length; j++) { this.showLoading(_.merge(swalCfg, { currentProgressStep: 2, - text: `Recover key: ${recoverCnt} / ${recoverMax}`, + text: `Recover key: ${recoverCnt++} / ${recoverMax}`, })) + await this.sleep(0) // 等待 UI 更新 const r1 = grp[j] + if (r0.skip || r1.skip) continue // skip by known key try { + const key1 = this.mfkey32v2(r0.uid, r0, r1) this.$set(this.ss, 'detects', _.unionWith(this.ss.detects, [{ block: r0.block, keyType: +r0.isKeyB, - key: this.mfkey32v2(r0.uid, r0, r1), + key: toHex(key1), }], _.isEqual)) + r0.skip = r1.skip = true + for (let k = 0; k < grp.length; k++) { + if (grp[k].skip) continue + const isReaderHasKey = Crypto1.mfkey32IsReaderHasKey({ + uid: grp[k].uid, + nt: grp[k].nt, + nr: grp[k].nr, + ar: grp[k].ar, + key: key1, + }) + if (isReaderHasKey) grp[k].skip = true + } } catch (err) { ultra.emitter.emit('error', err) } - recoverCnt++ - await this.sleep(0) // 等待 UI 更新 } } } @@ -302,7 +315,7 @@ block script async btnCopy (text, container = null) { if (!container) container = document.body const dom = document.createElement('textarea') - dom.value = text + dom.value = text = `${text}` container.appendChild(dom) dom.select() dom.setSelectionRange(0, 1e6) // For mobile devices @@ -340,7 +353,7 @@ block script nt1: r1.nt, nr1: r1.nr, ar1: r1.ar, - }).toString('hex') + }) }, btnSetTestFromDetect (detect) { _.merge(this.ss, _.pick(detect, ['block', 'keyType', 'key'])) diff --git a/pug/src/mifare-keychain.pug b/pug/src/mifare-keychain.pug index 3696bbf..7fd8a26 100644 --- a/pug/src/mifare-keychain.pug +++ b/pug/src/mifare-keychain.pug @@ -592,7 +592,7 @@ block script async btnCopy (text, container = null) { if (!container) container = document.body const dom = document.createElement('textarea') - dom.value = text + dom.value = text = `${text}` container.appendChild(dom) dom.select() dom.setSelectionRange(0, 1e6) // For mobile devices diff --git a/pug/src/mifare1k.pug b/pug/src/mifare1k.pug index c0188a6..05bd26f 100644 --- a/pug/src/mifare1k.pug +++ b/pug/src/mifare1k.pug @@ -293,7 +293,7 @@ block script const sectorData = Buffer.from(dump[i], 'hex') await ultra.cmdMf1EmuWriteBlock(i << 2, sectorData) } - await this.cmdSlotSaveSettings() + await ultra.cmdSlotSaveSettings() await this.swalFire({ icon: 'success', title: 'Emulate success' }) } catch (err) { ultra.emitter.emit('error', err) diff --git a/src/ChameleonUltra.ts b/src/ChameleonUltra.ts index 1c79405..a6d8883 100644 --- a/src/ChameleonUltra.ts +++ b/src/ChameleonUltra.ts @@ -3522,7 +3522,7 @@ export class ChameleonUltra { await onChunkKeys?.({ keys: chunkKeys, mask }) const tmp = await this.cmdMf1CheckKeysOfSectors({ keys: chunkKeys, mask }) if (_.isNil(tmp)) break // all founded - for (let i = 0; i < 10; i++) mask[i] |= tmp.found[i] + mask.or(tmp.found) for (let i = 0; i < maxSectors * 2; i++) { if (_.isNil(tmp.sectorKeys[i])) continue foundKeys[i] = tmp.sectorKeys[i] diff --git a/src/Crypto1.test.ts b/src/Crypto1.test.ts index b670dc5..9b49a5b 100644 --- a/src/Crypto1.test.ts +++ b/src/Crypto1.test.ts @@ -275,15 +275,7 @@ test.each([ { uid: '65535D33', key: '974C262B9278', nt: 'BE2B7B5D', nrEnc: 'B1E1B891', arEnc: '2CF7A248' }, { uid: '65535D33', key: 'A9AC67832330', nt: '2C198BE4', nrEnc: 'FEDAC6D2', arEnc: 'CF0A3C7E' }, { uid: '65535D33', key: 'A9AC67832330', nt: 'F73E638F', nrEnc: '4F4F867A', arEnc: '18CCB40B' }, -] as const)('tag send nt and use key to verify nrEnc and arEnc', async ({ uid, key, nt, nrEnc, arEnc }) => { - const tag: any = {} - _.merge(tag, _.mapValues({ uid, nt, nrEnc, arEnc }, hex => Buffer.from(hex, 'hex').readUInt32BE(0))) - tag.state = new Crypto1() - tag.state.setLfsr(Buffer.from(key, 'hex').readUIntBE(0, 6)) - tag.ks0 = tag.state.lfsrWord(tag.uid ^ tag.nt, 0) - tag.ks1 = tag.state.lfsrWord(tag.nrEnc, 1) - tag.ks2 = tag.state.lfsrWord(0, 0) - tag.ar = (tag.ks2 ^ tag.arEnc) >>> 0 - const expected = Crypto1.prngSuccessor(tag.nt, 64) - expect(tag.ar).toEqual(expected) +] as const)('.mfkey32IsReaderHasKey()', async ({ uid, key, nt, nrEnc, arEnc }) => { + const actual = Crypto1.mfkey32IsReaderHasKey({ uid, nt, nr: nrEnc, ar: arEnc, key: Buffer.from(key, 'hex') }) + expect(actual).toEqual(true) }) diff --git a/src/Crypto1.ts b/src/Crypto1.ts index 88e8afd..3e53842 100644 --- a/src/Crypto1.ts +++ b/src/Crypto1.ts @@ -1,6 +1,8 @@ /** * @example + * ```js * import Crypto1 from 'chameleon-ultra.js/Crypto1' + * ``` */ import { Buffer } from '@taichunmin/buffer' import _ from 'lodash' @@ -53,6 +55,7 @@ const fastfwd = [ /** * JavaScript implementation of the Crypto1 cipher. + * @see [crypto1.c | RfidResearchGroup/proxmark3](https://github.com/RfidResearchGroup/proxmark3/blob/master/common/crapto1/crypto1.c) */ export default class Crypto1 { /** @@ -166,7 +169,7 @@ export default class Crypto1 { * @returns The lfsr output bit. */ lfsrBit (input: number, isEncrypted: number): number { - const { evenParity32, filter, toBool, toUint32 } = Crypto1 + const { evenParity32, filter, toBool } = Crypto1 const output = filter(this.odd) const feedin = (output & toBool(isEncrypted)) ^ @@ -175,7 +178,7 @@ export default class Crypto1 { (LF_POLY_EVEN & this.even) ;[this.odd, this.even] = [ - toUint32(this.even << 1 | evenParity32(feedin)), + (this.even << 1) & 0xFFFFFF | evenParity32(feedin), this.odd, ] @@ -766,6 +769,46 @@ export default class Crypto1 { throw new Error('failed to recover key') } + /** + * A method for Tag to validate Reader has the correct key. + * @param opts.ar - The encrypted prng successor of `opts.nt`. + * @param opts.key - The 6-bytes key to be test. + * @param opts.nr - The encrypted nonce from reader. + * @param opts.nt - The nonce from tag. + * @param opts.uid - The 4-bytes uid of tag. + * @example + * ```js + * const { Buffer } = await import('https://cdn.jsdelivr.net/npm/chameleon-ultra.js@0/+esm') + * const { default: Crypto1 } = await import('https://cdn.jsdelivr.net/npm/chameleon-ultra.js@0/dist/Crypto1.mjs/+esm') + * + * console.log(Crypto1.mfkey32IsReaderHasKey({ + * ar: 'CF0A3C7E', + * key: 'A9AC67832330', + * nr: 'FEDAC6D2', + * nt: '2C198BE4', + * uid: '65535D33', + * }).toString('hex')) // true + * ``` + */ + static mfkey32IsReaderHasKey (opts: { + ar: UInt32Like + key: Buffer + nr: UInt32Like + nt: UInt32Like + uid: UInt32Like + }): boolean { + if (!Buffer.isBuffer(opts.key) || opts.key.length !== 6) throw new TypeError('invalid opts.key') + const { castToUint32, prngSuccessor, toUint32 } = Crypto1 + const tag: Record = { state: new Crypto1() } + ;[tag.uid, tag.nt, tag.nrEnc, tag.arEnc] = _.map(['uid', 'nt', 'nr', 'ar'] as const, k => castToUint32(opts[k])) + tag.state.setLfsr(opts.key.readUIntBE(0, 6)) + tag.ks0 = tag.state.lfsrWord(tag.uid ^ tag.nt, 0) + tag.ks1 = tag.state.lfsrWord(tag.nrEnc, 1) + tag.ks2 = tag.state.lfsrWord(0, 0) + tag.ar = toUint32(tag.ks2 ^ tag.arEnc) + return tag.ar === prngSuccessor(tag.nt, 64) + } + /** * Recover the key with the successfully authentication between the reader and the tag. * @param opts -