NFC开发笔记

0x00 概述

在正式开始介绍项目前,我想先介绍一下背景及一些废话,方便大家能够更好理解这个项目。这个项目起初是为了能够更好地抓取手机与IC卡之间的交互数据,以及对抓取的数据进行分析。后来在抓取数据过程当中,希望能够实现某些数据的替换(中间人)、重放、中继,以此展开了一系列的功能扩展。此外,对于数据提取这一块,也做了一些专门的优化,如:在Log中发送数据到HCE模块或者到MitM模块。文章的最后还会对希望扩展的功能进一步分析。我暂且把这个项目的整体设计模式称为:VNNC模式(View Network NFC Control,可以简单理解为MVC模式),对应的包如下图。为了加速apdu数据流的数据传输,我们还增加了多线程功能,这个方式将数据的SQLite存储与apdu数据流分离在不同线程,以保证高效、有序地对数据操作。这个设计模式是鄙人自命名的,如有纰漏,请指正。以下会对整个项目进行解释,以方便接管项目者快速理解整个项目,少走一些弯路。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
.
└── tud
└── seemuh
└── nfcgate
├── gui
│   ├── AboutActivity.java
│   ├── AboutWorkaroundActivity.java
│   ├── adapter
│   │   ├── ListViewAdapter.java
│   │   ├── MitmItemAdapter.java
│   │   └── MulAdapter.java
│   ├── EditActivity.java
│   ├── fragments
│   │   ├── CloneFragment.java
│   │   ├── EnablenfcDialog.java
│   │   ├── HceFragment.java
│   │   ├── LoggingDetailFragment.java
│   │   ├── LoggingFragment.java
│   │   ├── MitmFragment.java
│   │   ├── RelayFragment.java
│   │   ├── SettingsFragment.java
│   │   ├── TokenDialog.java
│   │   └── WorkaroundDialog.java
│   ├── LogActivity.java
│   ├── LoggingDetailActivity.java
│   ├── MainActivity.java
│   ├── MitmActivity.java
│   ├── RuleActivity.java
│   ├── SettingsActivity.java
│   ├── Splash.java
│   ├── tabLayout
│   │   ├── SlidingTabLayout.java
│   │   └── SlidingTabStrip.java
│   └── tabLogic
│   └── PagerAdapter.java
├── network
│   ├── c2c
│   │   └── C2C.java
│   ├── c2s
│   │   └── C2S.java
│   ├── Callback.java
│   ├── HighLevelNetworkHandler.java
│   ├── HighLevelProtobufHandler.java
│   ├── LowLevelNetworkHandler.java
│   ├── LowLevelTCPHandler.java
│   ├── meta
│   │   └── MetaMessage.java
│   └── ProtobufCallback.java
├── nfc
│   ├── config
│   │   ├── ConfigBuilder.java
│   │   ├── ConfigOption.java
│   │   ├── OptionType.java
│   │   └── Technologies.java
│   ├── hce
│   │   ├── ApduService.java
│   │   ├── DaemonConfiguration.java
│   │   └── PaymentServiceHost.java
│   ├── NfcManager.java
│   └── reader
│   ├── DesfireWorkaround.java
│   ├── IsoDepReader.java
│   ├── NfcAReader.java
│   ├── NfcBReader.java
│   ├── NfcFReader.java
│   ├── NFCTagEmulator.java
│   ├── NFCTagReader.java
│   └── NfcVReader.java
└── util
├── CustomTextWatcher.java
├── db
│   ├── CloneListItem.java
│   ├── CloneListStorage.java
│   ├── DbInitTask.java
│   ├── RuleListItem.java
│   ├── RuleListStorage.java
│   ├── SessionLoggingContract.java
│   └── SessionLoggingDbHelper.java
├── filter
│   ├── action
│   │   ├── Action.java
│   │   ├── ActionSequence.java
│   │   ├── Append.java
│   │   ├── InsertBytes.java
│   │   ├── ReplaceBytes.java
│   │   ├── ReplaceContent.java
│   │   └── Truncate.java
│   ├── conditional
│   │   ├── All.java
│   │   ├── And.java
│   │   ├── Conditional.java
│   │   ├── EndsWith.java
│   │   ├── Equals.java
│   │   ├── Length.java
│   │   ├── Not.java
│   │   ├── Or.java
│   │   ├── StartsWith.java
│   │   └── Xor.java
│   ├── FilterInitException.java
│   ├── Filter.java
│   └── FilterManager.java
├── ItemBean.java
├── MitmComm.java
├── NfcComm.java
├── NfcSession.java
├── preference
│   └── IntEditTextPreference.java
├── ReadLoadedSo.java
├── RootManager.java
├── RuleMatching.java
├── sink
│   ├── FileSink.java
│   ├── SessionLoggingSink.java
│   ├── SinkInitException.java
│   ├── Sink.java
│   ├── SinkManager.java
│   └── TextViewSink.java
├── UpdateUI.java
└── Utils.java

0x01 视图模块之Fragment/Activity

Ⅰ. adapter

adapter包里又有三个细分的adapter,分别是:ListViewAdapterMitmItemAdapterMulAdapter

1. ListViewAdapter

这个Adapter主要是用在HCE模块中的列表的Item,每个Item包含两个EditText和一个Button, 为了能够更好管理EditText和一个Button的相关性, 而增加了这个adapter(适配器). 主要代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
  @Override
public View getView(final int position, View convertView, ViewGroup parent) {
ViewHolder holder = null;
if (convertView == null) {
convertView = LayoutInflater.from(mContext).inflate(R.layout.item_hce_edittext, null);
holder = new ViewHolder(convertView);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}

final ItemBean itemObj = mData.get(position);

//This is important. Remove TextWatcher first.
if (holder.editText1.getTag() instanceof TextWatcher) {
holder.editText1.removeTextChangedListener((TextWatcher) holder.editText1.getTag());
}
// 设置读卡器的命令
holder.editText1.setText(itemObj.getReader());
holder.bt.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
mData.remove(position);
if(Utils.RuleOrTable){
Utils.currentRule.remove(itemObj.getReader());
Utils.ruleAdapter.notifyDataSetChanged();
}else {

Utils.currentMap.remove(itemObj.getReader());
Utils.tableAdapter.notifyDataSetChanged();
}
}
});

TextWatcher watcher = new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
oldString=s.toString();
}

@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}

@Override
public void afterTextChanged(Editable s) {
if (TextUtils.isEmpty(s)) {
itemObj.setReader("");
} else {
itemObj.setReader(s.toString());
if(Utils.RuleOrTable){
Utils.currentRule=Utils.ChangeReader(Utils.currentRule,oldString,s.toString());
}else {
Utils.currentMap=Utils.ChangeReader(Utils.currentMap,oldString,s.toString());
}
}
}
};

holder.editText1.addTextChangedListener(watcher);
holder.editText1.setTag(watcher);



//This is important. Remove TextWatcher first.
if (holder.editText2.getTag() instanceof TextWatcher) {
holder.editText2.removeTextChangedListener((TextWatcher) holder.editText2.getTag());
}

holder.editText2.setText(itemObj.getCard());

TextWatcher watcher2 = new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}

@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}

@Override
public void afterTextChanged(Editable s) {
if (TextUtils.isEmpty(s)) {
itemObj.setCard("");
} else {
itemObj.setCard(s.toString());

if(Utils.RuleOrTable){
Utils.currentRule=Utils.ChangeCard(Utils.currentRule,itemObj.getReader(),s.toString());
}else {
Utils.currentMap=Utils.ChangeCard(Utils.currentMap,itemObj.getReader(),s.toString());
}
}
}
};

holder.editText2.addTextChangedListener(watcher2);
holder.editText2.setTag(watcher2);
return convertView;
}

重写getView方法可以从视图中获取当前的ListItem, 并对ListItem进行约束. 上面代码段可看到有个一TextWatcher方法, 这个方法将在CustomTextWatcher.java详细解释.

2. MitmItemAdapter

这应该是该项目中最复杂的Adapter了, 其中每个Item包含四个Button, 三个TextView, 对应每个Button都有对应的OnclickListener , 根据选中的Buttonidswitch, 对应代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
View.OnClickListener mOnClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
synchronized (itemDisable) {
synchronized (itemButtons) {
RuleListStorage db = new RuleListStorage(mContext);
List<RuleListItem> items = db.getAll();
switch (v.getId()) {
case R.id.mitm_list_btn_exec_card:
...
case R.id.mitm_list_btn_exec_reader:
...
case R.id.mitm_list_btn_edit:
...
case R.id.mitm_list_btn_delete:
...
}
}
}
}
};

如上示代码, v.getId方法对应每个Button的id, 而四个Button都有对RuleListStorage操作, 具体SQLite的介绍在后面会详细解释. 值得注意的是, 上示代码段中引用了两个sychronized, 是为了各个Item在滑动过程中, 保持状态button.setEnable(boolean). item的状态之所以会发生改变, 是因为adapter的缓存和预浏览机制. 具体原因请google: ArrayAdapter 滑动item状态发生改变.

这里想要细讲的是关于AlertDialog的使用, 这里都是围绕着etBuilder来建立的. AlertDialog的作用是设置一个弹框, 然后把xml对应的组件设置进去. 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
AlertDialog.Builder etBuilder = new AlertDialog.Builder(mContext);
final View view = LayoutInflater.from(mContext).inflate(R.layout.dialog_mitm_comm_setting, null);
final EditText etName = view.findViewById(R.id.mitm_setting_comm_name);
final EditText etReader = view.findViewById(R.id.mitm_setting_reader_comm);
final EditText etCard = view.findViewById(R.id.mitm_setting_card_comm);
etReader.addTextChangedListener(new CustomTextWatcher(etReader));
etCard.addTextChangedListener(new CustomTextWatcher(etCard));
etName.setText(mList.get(position).getName());
etReader.setText(Utils.bytesToHex(mList.get(position).getReaderComm().getData()));
etCard.setText(Utils.bytesToHex(mList.get(position).getCardComm().getData()));
etBuilder.setTitle("Edit Rule")
.setView(view)
.setCancelable(true)
.setPositiveButton("Save", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// get data from user, then storage in database
String name = etName.getText().toString();
String reader = etReader.getText().toString().replace(" ", "");
String card = etCard.getText().toString().replace(" ", "");

if (name.length() == 0) {
Toast.makeText(mContext, "Please input command Function!", Toast.LENGTH_SHORT).show();
return;
} else if (!Utils.isHex(mContext, reader) && Utils.isHex(mContext, card)) {
return;
}

// save in database
RuleListItem ruleListItem = new RuleListItem(
mList.get(position).getId(),
name,
new NfcComm(NfcComm.Source.HCE, Utils.toBytes(reader)),
new NfcComm(NfcComm.Source.CARD, Utils.toBytes(card))
);
RuleListStorage mRuleDb = new RuleListStorage(mContext);
mRuleDb.update(ruleListItem);

// refreshList for list item view
MitmFragment.getInstance().refreshList();

notifyDataSetChanged();

// show toast
Toast.makeText(mContext, "Save Successfully!", Toast.LENGTH_SHORT).show();
dialog.dismiss();
}
});
etBuilder.setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
});
etBuilder.create();
etBuilder.show();
break;

3. MulAdapter

这个Adapter比较简单, 只有CheckBoxTextView, 要注意的是, CheckBoxListView中滑动时, 被勾选的状态也会发生改变(选中之后, 下滑返回选中状态就消失, 原因是public View getView(int position, View convertView, ViewGroup parent)传进来的convertView被多次重用), 这就需要用额外的方法保持被勾选的状态. 解决办法是用HashMap来保存CheckBox的状态值:

1
private static HashMap<Integer,Boolean> isSelected = new HashMap<Integer, Boolean>();

如下方法是从Fragment中传入list之后, 根据list的状态设置CheckBox选中状态.可以视为初始化CheckBox.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// MulAdapter中设置此方法.
public void initCheck(List<NfcComm> mlist) {
for (int i = 0; i < mlist.size(); i++) {
isSelected.put(i,mlist.get(i).isCheck());
}
}

// NFCComm中设置此方法.
// checkbox getter and setter
public boolean isCheck() {
return mCheck;
}
public void setCheck(boolean mCheck) {
this.mCheck = mCheck;
}

// LoggingFragment中 ListItem 点击事件的响应, 设置CheckBox的状态
protected void onListItemClick(View v, int pos, long id) {
// get ViewHolder Object
ViewHolder holder = (ViewHolder)v.getTag();
// change checbox status
holder.cb.toggle();
// save select state in mEvenList
mEventList.get(pos).setCheck(true);
// save checkbox selected status
MulAdapter.getIsSelected().put(pos,holder.cb.isChecked());
// adjust selected item
if (holder.cb.isChecked() == true) {
mCheckNum++;
} else {
mCheckNum--;
}
Toast.makeText(v.getContext(),"Already selected " + mCheckNum
+ " item.",Toast.LENGTH_SHORT).show();
}

Ⅱ. Fragment

1. CloneFragment

这里的一些监听事件就不细讲, 挑一些重要的讲一下.

FragmentActivity是相辅相成的, 一个Activity可以有多个Fragment, 例如, 该项目中的MainActivity中调用了多个Fragment(具体调用及原理参阅<安卓编程权威指南>第10章), 而Fragment被调用的方式如下:

1
2
3
4
5
6
7
8
// CloneFragment的一个方法, 这个方法被其他class调用, 从而调用该Fragment
public static CloneFragment getInstance() {
if(mFragment == null) {
mFragment = new CloneFragment();
}
return mFragment;
}
RelayFragment.getInstance();RelayFragment.getInstance();

其中, 发现标签后, 会做如下写数据库操作:

1
2
CloneListStorage storage = new CloneListStorage(mContext);
storage.add(new CloneListItem(RelayFragment.getInstance().mNfcManager.getAnticolData(), value.toString()));

这里从RelayFragment中获取实例后, 再调用该实例里的mNfcManager实例的方法获得卡的UID. 随机UID的方法在接下来数据库操作那里解释.

2. EnablenfcDialog

这个Dialog在NFC没开启的情况下, 会跳出该Dialog, 提示去系统设置里打开NFC.

1
2
3
4
5
6
7
// 在MainActivity中, 继承了EnableNFCDialog, 因此重载了该方法, 并调用了Settings
@Override
public void onNFCDialogPositiveClick() {
// User touched the dialog's goto settings button
Intent intent = new Intent(Settings.ACTION_NFC_SETTINGS);
startActivity(intent);
}

4. HceFragment

这个Fragment是第四个Tab, 也就是Hce下的视图界面代码, 值得关注的几点有:

  • 动态申请存储权限

    1
    2
    3
    4
    5
    6
    // ActivityCompat.requestPermissions的方法能够调用申请的dialog
    if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP) {
    if (ActivityCompat.checkSelfPermission(getContext(), Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
    ActivityCompat.requestPermissions(getActivity(), PERMISSIONS_STORAGE, REQUEST_PERMISSION_CODE);
    }
    }
  • 动态申请默认支付权限

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    NfcAdapter adapter = NfcAdapter.getDefaultAdapter(getContext());
    // 通过上面实例化一个NFCAdapter之后, 成功获取了CardEmulation实例
    mCardEmulation = CardEmulation.getInstance(adapter);
    // ComponentName输入的是对应的包名和类
    ComponentName myComponent = new ComponentName("tud.seemuh.nfcgate","tud.seemuh.nfcgate.nfc.hce.ApduService");
    // 调用CardEmulation的方法
    if (!mCardEmulation.isDefaultServiceForCategory(myComponent, CardEmulation.CATEGORY_PAYMENT)) {
    Intent intent = new Intent(CardEmulation.ACTION_CHANGE_DEFAULT);
    intent.putExtra(CardEmulation.EXTRA_CATEGORY, CardEmulation.CATEGORY_PAYMENT);
    intent.putExtra(CardEmulation.EXTRA_SERVICE_COMPONENT, myComponent);
    startActivityForResult(intent, 0);
    } else {
    Log.e("MainActivityHost", "on Create: Already default!");
    }
  • 设置Spinner控件的Adapter

    1
    2
    ArrayAdapter<String> listadapter = new ArrayAdapter<String>(getContext(), android.R.layout.simple_spinner_dropdown_item,myFile);
    sp.setAdapter(listadapter);

5. LoggingDetailFragment

这个Fragment是布局Logging数据的视图, 算是一个比较复杂的Fragment了, 将会对如下特性作解释.

  • AlertDialog

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    private AlertDialog getRenameSessionDialog() {
    AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
    // Add input value 给TextView添加原本来的Session
    final EditText input = new EditText(getActivity());
    if (mSession.getName() != null) {
    input.setText(mSession.getName());
    }

    // Set up text
    builder.setTitle(getText(R.string.title_dialog_rename))
    .setMessage(getText(R.string.rename_dialog_text))
    .setView(input)
    .setIcon(R.drawable.ic_action_edit_title)
    .setPositiveButton(getString(R.string.rename_dialog_confirm), new DialogInterface.OnClickListener() {
    @Override
    public void onClick(DialogInterface dialogInterface, int i) {
    doSessionRename(input.getText().toString());
    }
    });

    builder.setNegativeButton(getString(R.string.rename_dialog_cancel), this);

    return builder.create();
    }

    这算是一个比较经典的关于弹窗的案例了, AlertDialog.Builder实例化的对象有多种设置组件的方法, 如上面代码所示, 最后调用getRenameSessionDialog.show()即可弹窗.

  • AsyncTask

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    private class AsyncSessionLoader extends AsyncTask<Long, Void, Cursor> {
    private final String TAG = "AsyncSessionLoader";

    private SQLiteDatabase mDB;

    // 重写此方法以在后台线程上执行计算
    @Override
    protected Cursor doInBackground(Long... longs) {
    Log.d(TAG, "doInBackground: Started");
    // Get a DB object
    SessionLoggingDbHelper dbHelper = new SessionLoggingDbHelper(getActivity());
    mDB = dbHelper.getReadableDatabase();

    // Construct query
    // Define Projection
    String[] projection = {
    SessionLoggingContract.SessionMeta._ID,
    SessionLoggingContract.SessionMeta.COLUMN_NAME_NAME,
    SessionLoggingContract.SessionMeta.COLUMN_NAME_DATE,
    };
    // Define Sort order
    String sortorder = SessionLoggingContract.SessionMeta.COLUMN_NAME_DATE + " DESC";
    // Define Selection
    String selection = SessionLoggingContract.SessionMeta._ID + " LIKE ?";
    // Define Selection Arguments
    String[] selectionArgs = {String.valueOf(longs[0])};

    // Perform query
    Log.d(TAG, "doInBackground: Performing query");
    Cursor c = mDB.query(
    SessionLoggingContract.SessionMeta.TABLE_NAME, // Target Table
    projection, // Which fields are we interested in?
    selection, // Selection clause
    selectionArgs, // Arguments to clause
    null, // Grouping (not desired in this case)
    null, // Filtering (not desired in this case)
    sortorder // Sort order
    );

    Log.d(TAG, "doInBackground: Query done, returning");
    return c;
    true}

    // 必须从应用程序的主线程调用此方法, 上面方法返回的cursor传入下面的方法
    @Override
    protected void onPostExecute(Cursor c) {
    // Move to the first element of the cursor
    Log.d(TAG, "onPostExecute: Beginning processing of Sessions");
    if (!c.moveToFirst()) {
    Log.i(TAG, "onPostExecute: Cursor empty, doing nothing.");
    return;
    }

    // prepare session object
    long ID = c.getLong(c.getColumnIndexOrThrow(SessionLoggingContract.SessionMeta._ID));
    String name = c.getString(c.getColumnIndexOrThrow(SessionLoggingContract.
    SessionMeta.COLUMN_NAME_NAME));
    String date = c.getString(c.getColumnIndexOrThrow(SessionLoggingContract.
    SessionMeta.COLUMN_NAME_DATE));
    NfcSession session = new NfcSession(date, ID, name);

    // Update session information
    setSessionDetails(session);
    Log.d(TAG, "onPostExecute: Closing connection and finishing");
    c.close();
    mDB.close();
    }
    }

    这里用了UI多线程AsyncTask的方式从数据库中加载Session的Apdu数据.

6. LoggingFragment

这里的特性跟loggingDetailFragment的差不多, 是Session列表的视图界面, 具体分析不再展开.

7. MitmFragment

在RelayFragment中可以跳到这个Fragment, 这里主要的操作也是数据库操作. 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// get data from user, then storage in database
// 这里需要注意的是, 因为调用了CustomTextWatcher, 每一个字节16进制字符串会有一个空格, 因此入库的时候要把空格删掉
String name = etName.getText().toString();
String reader = etReader.getText().toString().replace(" ","");
String card = etCard.getText().toString().replace(" ","");

// check input hex format
if (name.length() == 0) {
Toast.makeText(getActivity(),"Please input command Function!",Toast.LENGTH_SHORT).show();
return;
} else if (!Utils.isHex(mContext, reader) && Utils.isHex(mContext, card)) {
return;
}

// save in database
RuleListItem ruleListItem = new RuleListItem(
name,
new NfcComm(NfcComm.Source.HCE,Utils.toBytes(reader)),
new NfcComm(NfcComm.Source.CARD,Utils.toBytes(card))
);
RuleListStorage mRuleDb = new RuleListStorage(mContext);
mRuleDb.add(ruleListItem);

// add item list
refreshList();

mListAdapter.notifyDataSetChanged();
// success message show in activity
Toast.makeText(getActivity(),"Save Successfully!",Toast.LENGTH_SHORT).show();
dialog.dismiss();

8. RelayFragment

这个Fragment应该是最重要的一个了. 一开始先实例化诸多空间和诸多类, 控制Nerwork/NFC/Database等. 下面将一些方法实现.

  1. checkIpPort

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    // regex for IP checking
    private static final String regexIPpattern ="^(([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.){3}([01]?\\d\\d?|2[0-4]\\d|25[0-5])$";
    private static int maxPort = 65535;
    ...
    // 这个方法是检测IP和port的
    public boolean checkIpPort(String ip, String port) {
    boolean validPort = false;
    boolean gotException = false;
    boolean validIp = false;
    // 实例化matcher以便根据正则确定是否是合法的IP
    Pattern pattern = Pattern.compile(regexIPpattern);
    Matcher matcher = pattern.matcher(ip);

    int int_port = 0;
    try {
    int_port = Integer.parseInt(port.trim());
    } catch (NumberFormatException e) {
    gotException = true;
    }
    if (!gotException) {
    // 若在端口范围内, 则validPort置为true
    if ((int_port > 0) && (int_port <= maxPort)) validPort = true;
    }
    validIp = matcher.matches();
    if (validPort) globalPort = int_port;
    // 只有port和ip都合法时才返回true
    return validPort && validIp;
    }
  2. defineUID

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    mFilterManager.rule.setAc(RuleMatching.MitMAction.SelfDefineAnticol);
    final AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
    final EditText input = new EditText(getContext());
    input.addTextChangedListener(new CustomTextWatcher(input));
    builder.setTitle(R.string.pref_define_uid_title)
    .setCancelable(true)
    .setMessage(R.string.pref_define_uid_hex)
    .setView(input)
    .setPositiveButton("Confirm", new DialogInterface.OnClickListener() {
    @Override
    public void onClick(DialogInterface dialog, int which) {
    String UID = input.getText().toString().replace(" ","");
    // 检查UID的长度是不是4个字节
    if(!Utils.isHexAndByte(UID, getContext(),4)){
    return;
    }
    // 如果检查通过则调用Filtermanager里实例化的Rule, 这里对应的是RuleMatching
    mFilterManager.rule.setUID(UID);
    }
    })
    .setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
    @Override
    public void onClick(DialogInterface dialog, int which) {
    return;
    }
    });
    builder.create();
    builder.show();
    break;

    关于defineUID的有三种UID模式, 分别是: RandomUID SelfDefineUID DefultUID, 分别对应三个case, 上示代码是关于selfdefineUID的代码.

  3. networkConnectCommon

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    private void networkConnectCommon() {
    // Initialize SinkManager
    mSinkManager = new SinkManager(mSinkManagerQueue);

    // Initialize FilterManager
    mFilterManager = new FilterManager();

    // Pass references
    // 用来存储Apdu数据
    mNfcManager.setSinkManager(mSinkManager, mSinkManagerQueue);
    // 用来过滤Apdu数据
    mNfcManager.setFilterManager(mFilterManager);
    // 用来HighLevelProtobufHandler
    mNfcManager.setNetworkHandler(mConnectionClient);

    // FIXME For debugging purposes, hardcoded selecting of sinks happens here
    // This should be selectable by the user

    // Initialize sinks
    // Get Preference manager to determine which sinks are active
    SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getActivity());

    // Determine settings for sinks
    boolean textViewSinkActive = prefs.getBoolean(getString(R.string.pref_key_debugWindow), false);
    boolean logfileSinkActive = prefs.getBoolean(getString(R.string.pref_key_logfile), false);
    boolean logSessionSinkActive = prefs.getBoolean(getString(R.string.pref_key_sessionlogging), false);

    // try...catch...排错, 存储network中的Apdu
    try {
    if (textViewSinkActive) {
    // Debug window is active, activate the sink that collects data for it
    mSinkManager.addSink(SinkManager.SinkType.DISPLAY_TEXTVIEW, mDebuginfo, false);
    }
    if (logfileSinkActive) {
    // Logging to file is active. Generate filename from timestamp
    SimpleDateFormat sdfDate = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US);
    Date now = new Date();
    String strDate = sdfDate.format(now);

    // Initialize File Sink 保存txt log到存储空间
    mSinkManager.addSink(SinkManager.SinkType.FILE, strDate + ".txt");
    }
    if (logSessionSinkActive) {
    mSinkManager.addSink(SinkManager.SinkType.SESSION_LOG, getActivity());
    }
    } catch (SinkInitException e) {
    e.printStackTrace();
    }
    // TODO Initialize and add Filters
    // Do the actual network connection
    mConnectionClient.connect(mIP.getText().toString(), port);
    }

Ⅲ. tabLayout & tabLogic

PagerAdapter

如果还需要在MainActivity中添加Fragment视图, 可以直接在这个类里添加就好, 修改的地方有三点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 每增加一个Fragment, return的值就+1
@Override
public int getCount() {
return 4;
}

// 每增加一个Fragment, 就在对应的position返回对应的名称, 以设置名称的值
@Override
public CharSequence getPageTitle(int position) {
if (position == 0) {
return "Relay";
} else if (position == 1) {
return "Clone";
} else if (position == 2) {
return "Log";
} else if (position == 3) {
return "HCE";
} else {
return "Item " + (position + 1);
}
}

// 每增加一个Fragment, 就根据pos的位置return一个instance.
@Override
public Fragment getItem(int pos) {
switch (pos) {
case 0:
return RelayFragment.getInstance();
case 1:
return CloneFragment.getInstance();
case 2:
return LoggingFragment.getInstance();
case 3:
return HceFragment.getInstance();
default:
return RelayFragment.getInstance();
}
}

Ⅳ. Activity

1. AboutWorkaroundActivity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
mWebView = (WebView) findViewById(R.id.workaroundDescWebView);
// Returns the language code of this Locale.
String loc = Locale.getDefault().getLanguage();
// 检索这些资源的底层 AssetManager 存储.
AssetManager mg = getResources().getAssets();
String path = "html/desfire-info." + loc + ".html";
try {
mg.open(path);
Log.i(TAG, "HTML exists for locale " + loc + ", using it.");
// mWebView实例加载了该目录下的html
mWebView.loadUrl("file:///android_asset/" + path);
} catch (IOException ex) {
Log.i(TAG, "No HTML for locale " + loc + ", using default (en)");
mWebView.loadUrl("file:///android_asset/html/desfire-info.en.html");
}

2. EditActivity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 这个Activity主要是在HCE模式下, 用来编辑数据的.
// 从资源中, 获取对应txt的map并放到TextView
Map<String,String> myMap= Utils.currentMap;
for(Map.Entry<String,String> entry:myMap.entrySet()){
mData.add(new ItemBean(entry.getKey(),entry.getValue()));
}
// 给mData listview设置Adapter
mAdapter = new ListViewAdapter(this, mData);
Utils.tableAdapter = mAdapter;
mListView.setAdapter(mAdapter);

//添加控件的监听事件
mButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
mData.add(new ItemBean());
mAdapter.notifyDataSetChanged();
}
});

3. LogActivity

这个Activity用在HCE模式中查看Log的视图界面, 内容较简单, 不再细讲.

4. MainActivity

这个Activity是控制整个project的枢纽.

1
2
3
4
5
6
7
8
9
// 这里初始化hce hook action
DaemonConfiguration.Init(this);
// 这里注册接收器
registerReceiver(new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
Toast.makeText(context, intent.getStringExtra("text"), Toast.LENGTH_LONG).show();
}
}, new IntentFilter("tud.seemuh.nfcgate.toaster"));

5. SettingActivity

这个Activity是设置界面的Activity,

0x02 network模块

Ⅰ. c2c/c2s/meta

这三个是根据protobuf序列化之后的类, 不用改, 直接调用就好.

Ⅱ. HighLevelProtobufHandler

HightLevelProtobufhandler是`HighLevelNetworkHandler接口的实现。它用于控制所有的网络通信,并使用一个低级别的网络处理程序来进行实际的网络通信。在这个处理程序和各自的回调实现(在我们的例子中是ProtobufCallBack)中,协议本身被实现。HightLevelProtobufhandler保持网络连接的状态,并负责在连接断开连接时拆卸所有相关的线程,由用户请求或一般连接丢失负责。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 动态修改Button
private void reactivateButtons() {
// We need to pass a parameter, even though it isn't used. Otherwise, the app will crash.
new UpdateUI(connectButton, UpdateUI.UpdateMethod.enableButton).execute("Unfug");
new UpdateUI(joinButton, UpdateUI.UpdateMethod.enableButton).execute("Unfug");
new UpdateUI(abortButton, UpdateUI.UpdateMethod.disableButton).execute("Unfug");
}

private void setButtonTexts() {
new UpdateUI(connectButton, UpdateUI.UpdateMethod.setTextButton).execute(MainActivity.createSessionMessage);
new UpdateUI(joinButton, UpdateUI.UpdateMethod.setTextButton).execute(MainActivity.joinSessionMessage);
new UpdateUI(resetButton, UpdateUI.UpdateMethod.setTextButton).execute(MainActivity.resetMessage);
}

接下来是三个比较重要的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
// for reader
@Override
public void sendAPDUMessage(NfcComm nfcdata) {
// 检测是否是apdu数据
if (nfcdata.getType() != NfcComm.Type.NFCBytes) {
Log.e(TAG, "sendApduMessage: NfcComm object does not contain NFC bytes. Doing nothing.");
return;
}
// 若reader mode关闭了, 则返回
if (status != Status.PARTNER_READER_MODE) {
Log.e(TAG, "sendAPDUMessage: Trying to send APDU message to partner who is not in reader mode. Doing nothing.");
return;
}
byte[] apdu = nfcdata.getData();

// Prepare message
C2C.NFCData.Builder apduMessage = C2C.NFCData.newBuilder();
// 给apduMessage 设置READER属性
apduMessage.setDataSource(C2C.NFCData.DataSource.READER);
apduMessage.setDataBytes(ByteString.copyFrom(apdu));

// Send prepared message
sendMessage(apduMessage.build(), MessageCase.NFCDATA);
}

// for card
@Override
public void sendAPDUReply(NfcComm nfcdata) {
if (nfcdata.getType() != NfcComm.Type.NFCBytes) {
Log.e(TAG, "sendApduReply: NfcComm object does not contain NFC bytes. Doing nothing.");
return;
}
if (status != Status.PARTNER_APDU_MODE) {
Log.e(TAG, "sendAPDUReply: Trying to send APDU reply to partner who is not in APDU mode. Doing nothing.");
return;
}
byte[] nfcbytes = nfcdata.getData();

// Build reply Protobuf
C2C.NFCData.Builder reply = C2C.NFCData.newBuilder();
reply.setDataBytes(ByteString.copyFrom(nfcbytes));
// 设置CARD属性
reply.setDataSource(C2C.NFCData.DataSource.CARD);

// Send reply
sendMessage(reply.build(), MessageCase.NFCDATA);
}

// it's about uid
@Override
public void sendAnticol(NfcComm nfcdata) {
if (nfcdata.getType() != NfcComm.Type.AnticolBytes) {
Log.e(TAG, "sendAnticol: NfcComm object does not contain Anticol bytes. Doing nothing.");
return;
}

// Retrieve values
byte[] config = nfcdata.getConfig().build();

// Build reply protobuf
C2C.Anticol.Builder b = C2C.Anticol.newBuilder();
b.setCONFIG(ByteString.copyFrom(config));

// TODO If we aren't in a session, cache this and send it as soon as a session is established?
// (And delete it if the card is removed in the meantime)
sendMessage(b.build(), MessageCase.ANTICOL);
Log.d(TAG, "sendAnticol: Sent Anticol message");
}

Ⅲ. LowLevelTCPHandler

该类只发送和接收原始字节,所有的协议逻辑和解析分别发生在HighLevelNetworkHandler或回调实例中。原始数据按照4字节的长度发送数据, 并且通过socket把所有的数据都发送出去. BufferedInputStream为另一个输入流添加功能 - 即缓冲输入和支持mark和reset 方法的能力。当BufferedInputStream 创建时,会创建一个内部缓冲区数组。当流中的字节被读取或跳过时,内部缓冲区将根据需要从所包含的输入流中重新填充,一次处理多个字节。该mark 操作会记住输入流中的一个点,并且该reset操作会导致自从最近mark操作以来,在从所包含的输入流中获取新字节之前重新读取所有字节。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
byte[] lenbytes = new byte[4];
int rcvlen = dis.read(lenbytes);
Log.d(TAG, "Got " + rcvlen + " bytes");
int len = ByteBuffer.wrap(lenbytes).getInt();

// read the message data
if (len > 0) {
Log.i(TAG, "Reading bytes of length:" + len);
readBytes = new byte[len];
int read = 0;
do {
// read(byte[] b, int off, int len)
// 将该字节输入流中的字节读入指定的字节数组,从给定的偏移量开始。
read += dis.read(readBytes, read, len-read);
} while(read < len);

Log.d(TAG, "Read data: " + Utils.bytesToHex(readBytes));
if(mCallback != null) {
Log.d(TAG, "Delegating to Callback.");
Log.i("readBytes: ", Utils.bytesToHex(readBytes));
mCallback.onDataReceived(readBytes);
Log.d(TAG, "Callback finished execution.");
}
else {
Log.i(TAG, "No callback set, saving for later");
getSome = true;
}
} else {
Log.e(TAG, "Error no postive number of bytes: " + len);
throw new IOException("Protocol error: Length information was negative or null");
}

Ⅳ. ProtobufCallback

这个类里面包含卡和读卡器所有的数据流, 其中最为重要的是handleWrapperMessage这个函数, 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
private void handleWrapperMessage(MetaMessage.Wrapper Wrapper) {
// Determine which type of Message the MetaMessage contains
if (Wrapper.getMessageCase() == MessageCase.DATA) {
Log.i(TAG, "onDataReceived: MessageCase.DATA: Sending to handler");
handleData(Wrapper.getData());
}
else if (Wrapper.getMessageCase() == MessageCase.NFCDATA) {
Log.i(TAG, "onDataReceived: MessageCase:NFCDATA: Sending to handler");
handleNFCData(Wrapper.getNFCData());
}
else if (Wrapper.getMessageCase() == MessageCase.SESSION) {
Log.i(TAG, "onDataReceived: MessageCase.SESSION: Sending to handler");
handleSession(Wrapper.getSession());
}
else if (Wrapper.getMessageCase() == MessageCase.STATUS) {
Log.i(TAG, "onDataReceived: MessageCase.STATUS: Sending to handler");
handleStatus(Wrapper.getStatus());
}
else if (Wrapper.getMessageCase() == MessageCase.ANTICOL) {
Log.i(TAG, "onDataReceived: MessageCase.ANTICOL: Sending to handler");
handleAnticol(Wrapper.getAnticol());
}
else {
Log.e(TAG, "onDataReceived: Message fits no known case! This is fucked up");
Handler.notifyUnknownMessageType();
}
}

这个函数根据来自LowLevelNetWorkHandler的数据流, 来对该数据进一步分类, 根据特定的数据类型让特定的函数操作. 例如handleNFCDatahandleSession以及handleAnticol. 接下来拿handleNFCData举例分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private void handleNFCData(C2C.NFCData msg) {
if (msg.getDataSource() == C2C.NFCData.DataSource.READER) {
// We received a signal FROM a reader device and are required to talk TO a card.
Log.i(TAG,"hangleNFCData: " + Utils.bytesToHex(msg.getDataBytes().toByteArray()));
NfcComm nfcdata = new NfcComm(NfcComm.Source.HCE, msg.getDataBytes().toByteArray());
mNfcManager.sendToCard(nfcdata);
} else if (msg.getDataSource() == C2C.NFCData.DataSource.CARD) {
// We received a signal FROM a card and are required to talk TO a reader.
NfcComm nfcdata = new NfcComm(NfcComm.Source.CARD, msg.getDataBytes().toByteArray());
mNfcManager.sendToReader(nfcdata);
} else {
// Wait, what? This should be impossible. Are we using an old protocol version?
Log.e(TAG, "HandleNfcData: Received Nfc Data from unknown source => Not implemented");
Handler.notifyNotImplemented();
}
}

msg传入这个函数之后, 再通过if...else if...else对该数据进行分类, 分为card的数据和reader的数据. 最后再通过sendToCardsendRoReader 函数对这些数据分流, 这两个函数在NFCManager.java会介绍.

0x03 nfc模块

Ⅰ. config

  1. ConfigBuilder

    这是一个将anticol进行数据格式的转换的类. 其中包含两个重要的函数parsebuild, 如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // 这个函数会将config数据(也就是Hce手机端接受的第一条来自card的数据)根据特定的数据类型转换为可读的有意义的数据.
    public void parse(byte[] config) {
    mOptions.clear();
    int index = 0;

    while(index + 2 < config.length) {
    byte type = config[index + 0];
    byte length = config[index + 1];

    byte[] data = new byte[length];
    System.arraycopy(config, index + 2, data, 0, length);

    add(OptionType.fromType(type), data);
    index += length + 2;
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    // 这个函数是上一个函数的逆函数
    public byte[] build() {
    int length = 0;

    for (ConfigOption option : mOptions)
    length += option.len() + 2;

    byte[] data = new byte[length];
    int offset = 0;

    for (ConfigOption option : mOptions) {
    option.push(data, offset);
    offset += option.len() + 2;
    }

    return data;
    }
    // 这个函数讲格式化之后的字符串拼接起来, 返回给调用者
    @Override
    public String toString() {
    StringBuilder result = new StringBuilder();

    for (ConfigOption option : mOptions)
    result.append(option.toString());
    return result.toString();
    }
  2. ConfigOption

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // 这个函数被上示代码中的toString函数调用
    @Override
    public String toString() {
    StringBuilder result = new StringBuilder();
    true// 这里根据特定的数据, 给定特定的名称.
    result.append("Type: ");
    result.append(mID.toString());

    if (mData.length > 1) {
    result.append(" (");
    result.append(mData.length);
    result.append(")");
    }

    result.append(", Value: 0x");
    result.append(bytesToHex(mData));
    result.append("\n");

    return result.toString();
    }
  3. OptionType

    这是一个枚举类, 为上示parse函数提供解析.

Ⅱ. hce

  1. ApduService

    这个类是与底层lib交互apdu命令最终要的一个类, 其中重要的函数有processCommandApduonDeactivatedsendResponse等:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    @Override
    public byte[] processCommandApdu(byte[] apdu, Bundle extras) {
    Log.d(TAG, "APDU-IN: " + Utils.bytesToHex(apdu));
    // 这里需要留意的是, 只有当滑动到HCE界面的时候(即mCurrent==3), 才让其返回handleApdu的值
    if (SlidingTabLayout.mCurrentPos == 3) {
    return handleApdu(getApplicationContext(), apdu);
    }
    // Package the ADPU into a NfcComm object
    NfcComm nfcdata = new NfcComm(NfcComm.Source.HCE, apdu);

    // Send the object to the handler
    mNfcManager.handleHCEData(nfcdata);

    // Tell the HCE implementation to wait a moment
    return DONT_RESPOND;
    }

    这个函数传入的byte[] apdu就是来自卡的apdu命令, 而return的byte[]就是手机返回给卡的数据.

    期间, 对输入进来的数据进行实例化之后, 通过handleHCEData函数处理.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    // 这个函数当读卡器断开交易的时候被调用
    @Override
    public void onDeactivated(int reason) {
    if (SlidingTabLayout.mCurrentPos == 3) {
    Utils.tv.append("-------------------------------------End-------------------------------------\n");
    } else {
    mNfcManager.unsetApduService();
    }
    }
    // sendResponseApdu函数是返回给卡的一个函数, 其功能相当于processCommandApdu的返回值
    public void sendResponse(byte[] apdu) {
    Log.d(TAG, "APDU-OUT: " + Utils.bytesToHex(apdu));
    sendResponseApdu(apdu);
    }
    // 这个函数用在hce功能的时候, 其中处理逻辑都在Utils类中.
    static byte[] handleApdu(Context context, byte[] apdu) {
    Utils.tv.append("pos:\n"+Utils.bytesToHex(apdu)+"\n\n");
    String payload = Utils.Start(context,apdu);
    Log.i(TAG,"payload: " + payload);
    Utils.tv.append("card:\n"+payload+"\n\n");
    return Utils.toBytes(payload);
    }
  2. DaemonConfiguration

    这个类在MainActivity被调用, 这里会根据Action发送Broadcast.

Ⅲ. reader

这里根据卡的类型选择特定的类返回相应的命令, 各种卡标签的识别是建立在继承NFCTagReader这个接口上的.

Ⅳ. NfcManager

这是格式化apdu数据最重要的一个类, 定义了apdu的各种属性.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
private NfcComm handleHceDataCommon(NfcComm nfcdata) {
Log.d(TAG, "handleHceDataCommon: Pre-Filter: " +
Utils.bytesToHex(nfcdata.getData()));
if (mFilterManager != null) {
nfcdata = mFilterManager.filterHCEData(nfcdata);
}

notifySinkManager(nfcdata);

Log.d(TAG, "handleHceDataCommon: Post-Filter: " +
Utils.bytesToHex(nfcdata.getData()));
return nfcdata;
}


private NfcComm handleCardDataCommon(NfcComm nfcdata) {
Log.d(TAG, "handleCardDataCommon: Pre-Filter: " +
Utils.bytesToHex(nfcdata.getData()));
if (mFilterManager != null) {
nfcdata = mFilterManager.filterCardData(nfcdata);
}
notifySinkManager(nfcdata);

Log.d(TAG, "handleCardDataCommon: Post-Filter: " +
Utils.bytesToHex(nfcdata.getData()));
return nfcdata;
}

上面两个函数, 分辨是handle card reader 的函数, 其中最为重要的是filterCardData filterHCEData两个函数, 其中中间人数据就是在filterCardData filterHCEData两个方法里被篡改的. 函数的实现在后面RuleMatching.java会介绍.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
true/**
* Send NFC data to the card
* 这里主要将HCE手机的来自读卡器的apdu通过network发给另一部手机.
* @param nfcdata NFcComm object containing the message for the card
*/
public void sendToCard(NfcComm nfcdata) {
if (mReader.isConnected()) {
nfcdata = handleHceDataCommon(nfcdata);
Log.i(TAG,"sendToCard: " + Utils.bytesToHex(nfcdata.getData()));
// Communicate with card
byte[] reply = mReader.sendCmd(nfcdata.getData());
if (reply == null) {
mReader.closeConnection();
mNetworkHandler.disconnectCardWorkaround();
mNetworkHandler.notifyNFCNotConnected();
} else {
// Create NfcComm object and pass it through filter and sinks
NfcComm nfcreply = new NfcComm(NfcComm.Source.CARD, reply);
nfcreply = handleCardDataCommon(nfcreply);

// Send message
mNetworkHandler.sendAPDUReply(nfcreply);
}
} else {
Log.e(TAG, "HandleNFCData: No NFC connection active");
// There is no connected NFC device
mNetworkHandler.notifyNFCNotConnected();
}
}


/**
* Send NFC data to the Reader
* 这里主要将普通手机读取的来自卡的apdu发给HCE进而发给读卡器.
* @param nfcdata NfcComm object containing the message for the Reader
*/
public void sendToReader(NfcComm nfcdata) {
if (mApduService != null) {
// Pass data through sinks and filters
nfcdata = handleCardDataCommon(nfcdata);

// Send data to the Reader device
Log.i(TAG,"sendToReader: " + Utils.bytesToHex(nfcdata.getData()));
mApduService.sendResponse(nfcdata.getData());
} else {
Log.e(TAG, "HandleNFCData: Received a message for a reader, but no APDU instance active.");
mNetworkHandler.notifyNFCNotConnected();
}
}

0x04 util

Ⅰ. db

1. CloneListStorage

CloneListItem是定义数据库每个Item数据结构的一个类, 其实现并不复杂, 不再赘述. 接下来看CloneListStorage:

  1. 首先定义数据库名称及各列的列名。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // All Static variables
    // Database Version
    private static final int DATABASE_VERSION = 1;

    // Database Name
    private static final String DATABASE_NAME = "clonemode.db";

    // table name
    private static final String TABLE_NAME = "list";

    // Table Columns names
    private static final String KEY_ID = "id";
    private static final String KEY_NAME = "name";
    private static final String KEY_CONFIG = "config";
  2. 然后建立数据库:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // 如果要调用或者建立这个数据库, 传入一个context即可新建/获得这个数据库.
    public CloneListStorage(Context context) {
    super(context, DATABASE_NAME, null, DATABASE_VERSION);
    }

    // 这里重写了onCreate方法, 是产生相应的table. 注意下列字符串定义了每个table的数据类型
    @Override
    public void onCreate(SQLiteDatabase db) {
    String CREATE_CONTACTS_TABLE = "CREATE TABLE " + TABLE_NAME + "("
    + KEY_ID + " INTEGER PRIMARY KEY,"
    + KEY_NAME + " TEXT,"
    + KEY_CONFIG + " BLOB"
    + ")";
    db.execSQL(CREATE_CONTACTS_TABLE);
    }
  3. 如果要往database添加item, 如下函数可实现:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public void add(CloneListItem item) {
    // 实例化一个可写的SQLiteDatabase
    SQLiteDatabase db = this.getWritableDatabase();
    // 实例化一个ContentValues, 以给对应的table赋值
    ContentValues values = new ContentValues();
    // 首先给KEY_NAME赋值, 也就是item的名称
    values.put(KEY_NAME, item.toString());
    // 获得anticol的值之后, 再build成blob类型
    NfcComm ac = item.getAnticolData();
    values.put(KEY_CONFIG, ac.getConfig().build());

    // Inserting Row 插入数据库中
    db.insert(TABLE_NAME, null, values);
    db.close(); // Closing database connection
    }
  4. CLONE界面里的自定义UID就是在这里实现的:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    // 当数据库里的Defult没有时, 则会调用这个函数生成一个默认的item, 以后产生随机UID或者自定义UID都是在这个item里作修改.
    public void addDefultConfig(){
    SQLiteDatabase db = this.getWritableDatabase();
    // 这里用的是rawQueray, 可以直接用命令选择相应的list
    Cursor c = db.rawQuery("select * from list",null);
    if (c.getCount() <= 0) {
    String defultAnticol = "330477AD15D532012830010831010059024744";
    Log.i("***DefultAnticol: ", defultAnticol);
    ContentValues values = new ContentValues();
    values.put(KEY_NAME, "Defult");
    values.put(KEY_ID, 1);
    values.put(KEY_CONFIG, Utils.toBytes(defultAnticol));
    db.insert(TABLE_NAME, null, values);
    }
    db.close();
    }

    public void changeUID(String UID){
    if (Utils.isHex(UID)) {
    SQLiteDatabase db = this.getWritableDatabase();
    ContentValues values = new ContentValues();
    values.put(KEY_CONFIG,
    Utils.toBytes(
    "3304"+UID+"32012830010831010059024744"));
    // Default item总是在数据库的第一个.
    db.update(
    TABLE_NAME,
    values,
    KEY_ID + " = ?",
    new String[]{"1"}
    );
    db.close();
    }
    }
  5. 删除item操作如下:

    1
    2
    3
    4
    5
    6
    7
    // 可以看出, 是根据id(数据库的第几项)来删除item的
    public void delete(int id) {
    SQLiteDatabase db = this.getWritableDatabase();
    db.delete(TABLE_NAME, KEY_ID + " = ?",
    new String[]{String.valueOf(id)});
    db.close();
    }
  6. 获得数据库所有的item:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    public List<CloneListItem> getAll() {
    List<CloneListItem> contactList = new ArrayList<CloneListItem>();
    // Select All Query
    String selectQuery = "SELECT * FROM " + TABLE_NAME;

    SQLiteDatabase db = this.getWritableDatabase();
    Cursor cursor = db.rawQuery(selectQuery, null);

    // looping through all rows and adding to list
    if (cursor.moveToFirst()) {
    do {
    contactList.add(createByCursor(cursor));
    } while (cursor.moveToNext());
    }

    // return contact list
    return contactList;
    }

2. RuleListStorage

这里其实跟CloneListStorage很相似, 将一个比较不一样的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 这个函数遍历整个数据库, 然后获取每个item的state值, 可能为0,1,2, 分别表示: 未选中, 修改卡返回的apdu数据, 修改读卡器的apdu数据
public Map<Integer, Integer> getStateMap(){
Map<Integer, Integer> map = new HashMap<>();
SQLiteDatabase db = this.getReadableDatabase();
String selectQuery = "SELECT * FROM " + TABLE_NAME;
Cursor c = db.rawQuery(selectQuery,null);
if (c.moveToFirst()) {
do {
map.put(c.getInt(c.getColumnIndex(KEY_ID)),
c.getInt(c.getColumnIndex(KEY_SELECT_STATE)));
}while (c.moveToNext());
}
return map;
}

3. SessionLoggingDbHelper

这个类存储卡和读卡器的apdu数据.

Ⅱ. filter

这里是过滤一些不规范的apdu数据, 下面只分析部分FilterManager:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
true/**
* Execute filters that are registered for HCE data
* @param nfcdata The APDU that should be filtered
* @return The filtered HCE data
*/
public NfcComm filterHCEData(NfcComm nfcdata) {
// 这里根据RuleListStorage的规则识别一些来自读卡器的apdu指令,以便对其做进一步操作
rule.changeHCEData(nfcdata);
if (mHCENonEmpty) {
if (nfcdata.getType() != NfcComm.Type.NFCBytes) return nfcdata;
for (Filter f : mHCEFilters) {
nfcdata = f.filter(nfcdata);
}
}
return nfcdata;
}

true/**
* Execute filters that are registered for Card data
* @param nfcdata The APDU that should be filtered
* @return The filtered Card data
*/
public NfcComm filterCardData(NfcComm nfcdata) {
// 这里根据上面识别的来自读卡器的apdu指令,对卡返回的数据进行修改
rule.changeCardData(nfcdata);
if (mCardNonEmpty) {
if (nfcdata.getType() != NfcComm.Type.NFCBytes) return nfcdata;
for (Filter f : mCardFilters) {
nfcdata = f.filter(nfcdata);
}
}
return nfcdata;
}

true/**
* Execute filters that are registered for Anticollision data
* @param anticol NfcComm object containing anticol data
*/
public NfcComm filterAnticolData(NfcComm anticol) {
// 修改UID也是在这里开始的
anticol = rule.changeAnticolData(anticol);
if (mAnticolNonEmpty) {
if (anticol.getType() != NfcComm.Type.AnticolBytes) return anticol;
for (Filter f : mAnticolFilters) {
anticol = f.filter(anticol);
}
}
return anticol;
}

Ⅲ. 其他

1. CustomTextWatcher

TextWatcher有三个重要的方法, 顾名思义分别是TextView在改变前/改变时/改变后的动作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
// 在改变前没有做任何修改
@Override
public void beforeTextChanged(CharSequence charSequence, int start,
int count, int after) {
}

// 这里限定了输入时只能输入16进制字符, 而且每两个字符之间像个一个空格
@Override
public void onTextChanged(CharSequence charSequence, int start, int before,
int count) {
try {
String temp = charSequence.toString();
// Set selection.
if (mLastText.equals(temp)) {
if (mInvalid) {
mSelection -= 1;
} else {
if ((mSelection >= 1) && (temp.length() > mSelection - 1)
&& (temp.charAt(mSelection - 1)) == ' ') {
mSelection += 1;
}
}
int length = mLastText.length();
if (mSelection > length) {

mEditText.setSelection(length);
} else {

mEditText.setSelection(mSelection);
}
mFormat = false;
mInvalid = false;
return;
}

mFormat = true;
mSelection = start;

// Delete operation.
if (count == 0) {
if ((mSelection >= 1) && (temp.length() > mSelection - 1)
&& (temp.charAt(mSelection - 1)) == ' ') {
mSelection -= 1;
}

return;
}

// Input operation.
mSelection += count;
char[] lastChar = (temp.substring(start, start + count))
.toCharArray();
int mid = lastChar[0];
if (mid >= 48 && mid <= 57) {
/* 1-9. */
} else if (mid >= 65 && mid <= 70) {
/* A-F. */
} else if (mid >= 97 && mid <= 102) {
/* a-f. */
} else {
/* Invalid input. */
mInvalid = true;
temp = temp.substring(0, start)
+ temp.substring(start + count, temp.length());
mEditText.setText(temp);
return;
}

} catch (Exception e) {
Log.i(TAG, e.toString());
}
}

@Override
public void afterTextChanged(Editable editable) {
try {
/* Format input. */
if (mFormat) {
StringBuilder text = new StringBuilder();
// 这里将空格删去
text.append(editable.toString().replace(" ", ""));
int length = text.length();
int sum = (length % 2 == 0) ? (length / 2) - 1 : (length / 2);
for (int offset = 2, index = 0; index < sum; offset += 3, index++) {
text.insert(offset, " ");
}
mLastText = text.toString();
mEditText.setText(text);
}
} catch (Exception e) {
Log.i(TAG, e.toString());
}
}

2. NfcComm

这个类定义了nfc apdu数据的多种数据类型.

3. RuleMatching

  1. 下示代码时修改UID的一个函数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public void changeUIDRule(NfcComm anticol){
    String UID = null;
    if (ac.equals(MitMAction.RandomAnticol)) {
    // 随机生成4字节的数据作为UID
    UID = Utils.randomHexString(4);
    } else if (ac.equals(MitMAction.SelfDefineAnticol)) {
    UID = mUID;
    }
    if (UID != null) {
    mConfig = Utils.relaceBytesFromArray(anticol.getConfig().build(),
    Utils.toBytes(UID),
    new byte[]{(byte)0x33,
    (byte)0x04});
    }
    }
  2. 以下是识别来自card的指令的函数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    private void compReaderCommFromDb(NfcComm nfcdata) {
    RuleListStorage db = new RuleListStorage(RelayFragment.getInstance().getContext());
    List<RuleListItem> items = db.getAll();
    // 遍历每个位于规则数据库的item
    for (RuleListItem item: items) {
    // 如果当前的item的state为1, 则将mChangeCardComm赋值
    if (item.getSelect() == 1) {
    if (Utils.bytesToHex(nfcdata.getData()).indexOf(
    Utils.bytesToHex(item.getReaderComm().getData())) == 0) {
    mChangeCardComm = item.getCardComm().getData();
    break;
    } else {
    mChangeCardComm = null;
    }
    }else if (item.getSelect() == 2) {
    // 通过读卡器的数据来修改读卡器命令
    if (Utils.bytesToHex(nfcdata.getData()).indexOf(
    Utils.bytesToHex(item.getReaderComm().getData())) == 0) {
    mChangeHceComm = item.getCardComm().getData();
    break;
    } else {
    mChangeHceComm = null;
    }
    }
    }
    }
  3. 以下三个函数时中间人apdu数据重要函数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    // 修改card返回的apdu数据
    public NfcComm changeCardData(NfcComm nfcdata) {
    // Log.i(TAG,"Card: " + Utils.bytesToHex(nfcdata.getData()));
    if (mChangeCardComm != null) {
    // Log.i(TAG,"Exec: " + Utils.bytesToHex(mChangeCardComm));
    nfcdata.setData(mChangeCardComm);
    mChangeCardComm = null;
    }
    return nfcdata;
    }
    // 修改读卡器发送的apdu数据
    public NfcComm changeHCEData(NfcComm nfcdata) {
    // Log.i(TAG,"HCE : " + Utils.bytesToHex(nfcdata.getData()));
    compReaderCommFromDb(nfcdata);
    if (mChangeHceComm != null) {
    nfcdata.setData(mChangeHceComm);
    mChangeHceComm = null;
    }
    return nfcdata;
    }
    // 修改uid
    public NfcComm changeAnticolData(NfcComm anticol) {
    if (anticol.getType() == NfcComm.Type.AnticolBytes) {
    changeUIDRule(anticol);
    if (mConfig != null) {
    anticol = new NfcComm(new ConfigBuilder(mConfig));
    mConfig = null;
    }
    }
    return anticol;
    }

4. Utils

这个类含有全局需要的处理数据的函数. 大部分已注释了解释的函数不在列举. 举一个简单的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 这个函数读取特定目录下的所有文件, 并返回文件名列表
public static ArrayList<String> listDir(String path){
ArrayList<String> myFile = new ArrayList<String>();
String realPath=Environment.getExternalStorageDirectory().toString()+"/MTYReader/"+path;
Log.i("listDir",realPath);
File file=new File(realPath);
File[] files=file.listFiles();
if (files == null) {
return null;
}
for(int i =0;i<files.length;i++){
myFile.add(files[i].getName());
}
return myFile;
}
文章目录
  1. 1. 0x00 概述
  2. 2. 0x01 视图模块之Fragment/Activity
    1. 2.1. Ⅰ. adapter
      1. 2.1.1. 1. ListViewAdapter
      2. 2.1.2. 2. MitmItemAdapter
      3. 2.1.3. 3. MulAdapter
    2. 2.2. Ⅱ. Fragment
      1. 2.2.1. 1. CloneFragment
      2. 2.2.2. 2. EnablenfcDialog
      3. 2.2.3. 4. HceFragment
      4. 2.2.4. 5. LoggingDetailFragment
      5. 2.2.5. 6. LoggingFragment
      6. 2.2.6. 7. MitmFragment
      7. 2.2.7. 8. RelayFragment
    3. 2.3. Ⅲ. tabLayout & tabLogic
      1. 2.3.1. PagerAdapter
    4. 2.4. Ⅳ. Activity
      1. 2.4.1. 1. AboutWorkaroundActivity
      2. 2.4.2. 2. EditActivity
      3. 2.4.3. 3. LogActivity
      4. 2.4.4. 4. MainActivity
      5. 2.4.5. 5. SettingActivity
  3. 3. 0x02 network模块
    1. 3.1. Ⅰ. c2c/c2s/meta
    2. 3.2. Ⅱ. HighLevelProtobufHandler
    3. 3.3. Ⅲ. LowLevelTCPHandler
    4. 3.4. Ⅳ. ProtobufCallback
  4. 4. 0x03 nfc模块
    1. 4.1. Ⅰ. config
    2. 4.2. Ⅱ. hce
    3. 4.3. Ⅲ. reader
    4. 4.4. Ⅳ. NfcManager
  5. 5. 0x04 util
    1. 5.1. Ⅰ. db
      1. 5.1.1. 1. CloneListStorage
      2. 5.1.2. 2. RuleListStorage
      3. 5.1.3. 3. SessionLoggingDbHelper
    2. 5.2. Ⅱ. filter
    3. 5.3. Ⅲ. 其他
      1. 5.3.1. 1. CustomTextWatcher
      2. 5.3.2. 2. NfcComm
      3. 5.3.3. 3. RuleMatching
      4. 5.3.4. 4. Utils
,