0x00 概述
在正式开始介绍项目前,我想先介绍一下背景及一些废话,方便大家能够更好理解这个项目。这个项目起初是为了能够更好地抓取手机与IC卡之间的交互数据,以及对抓取的数据进行分析。后来在抓取数据过程当中,希望能够实现某些数据的替换(中间人)、重放、中继,以此展开了一系列的功能扩展。此外,对于数据提取这一块,也做了一些专门的优化,如:在Log中发送数据到HCE模块或者到MitM模块。文章的最后还会对希望扩展的功能进一步分析。我暂且把这个项目的整体设计模式称为:VNNC模式(View Network NFC Control,可以简单理解为MVC模式),对应的包如下图。为了加速apdu数据流的数据传输,我们还增加了多线程功能,这个方式将数据的SQLite存储与apdu数据流分离在不同线程,以保证高效、有序地对数据操作。这个设计模式是鄙人自命名的,如有纰漏,请指正。以下会对整个项目进行解释,以方便接管项目者快速理解整个项目,少走一些弯路。
1 | . |
0x01 视图模块之Fragment/Activity
Ⅰ. adapter
adapter包里又有三个细分的adapter,分别是:ListViewAdapter
、MitmItemAdapter
、MulAdapter
。
1. ListViewAdapter
这个Adapter主要是用在HCE
模块中的列表的Item,每个Item包含两个EditText
和一个Button
, 为了能够更好管理EditText
和一个Button
的相关性, 而增加了这个adapter(适配器). 主要代码如下:
1 |
|
重写getView
方法可以从视图中获取当前的ListItem, 并对ListItem进行约束. 上面代码段可看到有个一TextWatcher
方法, 这个方法将在CustomTextWatcher.java
详细解释.
2. MitmItemAdapter
这应该是该项目中最复杂的Adapter
了, 其中每个Item包含四个Button
, 三个TextView
, 对应每个Button
都有对应的OnclickListener
, 根据选中的Button
的id
来switch
, 对应代码如下:
1 | View.OnClickListener mOnClickListener = new View.OnClickListener() { |
如上示代码, v.getId
方法对应每个Button
的id, 而四个Button
都有对RuleListStorage
操作, 具体SQLite的介绍在后面会详细解释. 值得注意的是, 上示代码段中引用了两个sychronized
, 是为了各个Item在滑动过程中, 保持状态button.setEnable(boolean)
. item的状态之所以会发生改变, 是因为adapter的缓存和预浏览机制. 具体原因请google: ArrayAdapter 滑动item状态发生改变.
这里想要细讲的是关于AlertDialog
的使用, 这里都是围绕着etBuilder
来建立的. AlertDialog
的作用是设置一个弹框, 然后把xml对应的组件设置进去. 如下:
1 | AlertDialog.Builder etBuilder = new AlertDialog.Builder(mContext); |
3. MulAdapter
这个Adapter
比较简单, 只有CheckBox
和TextView
, 要注意的是, CheckBox
在ListView
中滑动时, 被勾选的状态也会发生改变(选中之后, 下滑返回选中状态就消失, 原因是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 | // MulAdapter中设置此方法. |
Ⅱ. Fragment
1. CloneFragment
这里的一些监听事件就不细讲, 挑一些重要的讲一下.
Fragment
与Activity
是相辅相成的, 一个Activity
可以有多个Fragment
, 例如, 该项目中的MainActivity
中调用了多个Fragment
(具体调用及原理参阅<安卓编程权威指南>第10章), 而Fragment
被调用的方式如下:安卓编程权威指南>
1 | // CloneFragment的一个方法, 这个方法被其他class调用, 从而调用该Fragment |
其中, 发现标签后, 会做如下写数据库操作:
1 | CloneListStorage storage = new CloneListStorage(mContext); |
这里从RelayFragment
中获取实例后, 再调用该实例里的mNfcManager
实例的方法获得卡的UID. 随机UID的方法在接下来数据库操作那里解释.
2. EnablenfcDialog
这个Dialog
在NFC没开启的情况下, 会跳出该Dialog
, 提示去系统设置里打开NFC.
1 | // 在MainActivity中, 继承了EnableNFCDialog, 因此重载了该方法, 并调用了Settings |
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
14NfcAdapter 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
2ArrayAdapter<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
24private 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() {
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
68private class AsyncSessionLoader extends AsyncTask<Long, Void, Cursor> {
private final String TAG = "AsyncSessionLoader";
private SQLiteDatabase mDB;
// 重写此方法以在后台线程上执行计算
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传入下面的方法
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 | // get data from user, then storage in database |
8. RelayFragment
这个Fragment应该是最重要的一个了. 一开始先实例化诸多空间和诸多类, 控制Nerwork/NFC/Database等. 下面将一些方法实现.
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;
}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
29mFilterManager.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() {
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() {
public void onClick(DialogInterface dialog, int which) {
return;
}
});
builder.create();
builder.show();
break;关于defineUID的有三种UID模式, 分别是:
RandomUID
SelfDefineUID
DefultUID
, 分别对应三个case, 上示代码是关于selfdefineUID
的代码.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
52private 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 | // 每增加一个Fragment, return的值就+1 |
Ⅳ. Activity
1. AboutWorkaroundActivity
1 | mWebView = (WebView) findViewById(R.id.workaroundDescWebView); |
2. EditActivity
1 | // 这个Activity主要是在HCE模式下, 用来编辑数据的. |
3. LogActivity
这个Activity用在HCE模式中查看Log的视图界面, 内容较简单, 不再细讲.
4. MainActivity
这个Activity是控制整个project的枢纽.
1 | // 这里初始化hce hook action |
5. SettingActivity
这个Activity是设置
界面的Activity,
0x02 network模块
Ⅰ. c2c/c2s/meta
这三个是根据protobuf序列化之后的类, 不用改, 直接调用就好.
Ⅱ. HighLevelProtobufHandler
HightLevelProtobufhandler
是`HighLevelNetworkHandler
接口的实现。它用于控制所有的网络通信,并使用一个低级别的网络处理程序来进行实际的网络通信。在这个处理程序和各自的回调实现(在我们的例子中是ProtobufCallBack
)中,协议本身被实现。HightLevelProtobufhandler
保持网络连接的状态,并负责在连接断开连接时拆卸所有相关的线程,由用户请求或一般连接丢失负责。
1 | // 动态修改Button |
接下来是三个比较重要的函数:
1 | // for reader |
Ⅲ. LowLevelTCPHandler
该类只发送和接收原始字节,所有的协议逻辑和解析分别发生在HighLevelNetworkHandler或回调实例中。原始数据按照4字节的长度发送数据, 并且通过socket把所有的数据都发送出去. BufferedInputStream
为另一个输入流添加功能 - 即缓冲输入和支持mark和reset 方法的能力。当BufferedInputStream
创建时,会创建一个内部缓冲区数组。当流中的字节被读取或跳过时,内部缓冲区将根据需要从所包含的输入流中重新填充,一次处理多个字节。该mark 操作会记住输入流中的一个点,并且该reset操作会导致自从最近mark操作以来,在从所包含的输入流中获取新字节之前重新读取所有字节。
1 | byte[] lenbytes = new byte[4]; |
Ⅳ. ProtobufCallback
这个类里面包含卡和读卡器所有的数据流, 其中最为重要的是handleWrapperMessage
这个函数, 如下:
1 | private void handleWrapperMessage(MetaMessage.Wrapper Wrapper) { |
这个函数根据来自LowLevelNetWorkHandler
的数据流, 来对该数据进一步分类, 根据特定的数据类型让特定的函数操作. 例如handleNFCData
和handleSession
以及handleAnticol
. 接下来拿handleNFCData
举例分析:
1 | private void handleNFCData(C2C.NFCData msg) { |
msg
传入这个函数之后, 再通过if...else if...else
对该数据进行分类, 分为card
的数据和reader
的数据. 最后再通过sendToCard
和 sendRoReader
函数对这些数据分流, 这两个函数在NFCManager.java
会介绍.
0x03 nfc模块
Ⅰ. config
ConfigBuilder
这是一个将
anticol
进行数据格式的转换的类. 其中包含两个重要的函数parse
和build
, 如下所示: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;
}
// 这个函数讲格式化之后的字符串拼接起来, 返回给调用者
public String toString() {
StringBuilder result = new StringBuilder();
for (ConfigOption option : mOptions)
result.append(option.toString());
return result.toString();
}ConfigOption
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20// 这个函数被上示代码中的toString函数调用
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();
}OptionType
这是一个枚举类, 为上示
parse
函数提供解析.
Ⅱ. hce
ApduService
这个类是与底层lib交互apdu命令最终要的一个类, 其中重要的函数有
processCommandApdu
、onDeactivated
、sendResponse
等:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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// 这个函数当读卡器断开交易的时候被调用
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);
}DaemonConfiguration
这个类在
MainActivity
被调用, 这里会根据Action
发送Broadcast
.
Ⅲ. reader
这里根据卡的类型选择特定的类返回相应的命令, 各种卡标签的识别是建立在继承NFCTagReader
这个接口上的.
Ⅳ. NfcManager
这是格式化apdu数据最重要的一个类, 定义了apdu的各种属性.
1 | private NfcComm handleHceDataCommon(NfcComm nfcdata) { |
上面两个函数, 分辨是handle card
reader
的函数, 其中最为重要的是filterCardData
filterHCEData
两个函数, 其中中间人数据就是在filterCardData
filterHCEData
两个方法里被篡改的. 函数的实现在后面RuleMatching.java
会介绍.
1 | true/** |
0x04 util
Ⅰ. db
1. CloneListStorage
CloneListItem
是定义数据库每个Item
数据结构的一个类, 其实现并不复杂, 不再赘述. 接下来看CloneListStorage
:
首先定义数据库名称及各列的列名。
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";然后建立数据库:
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的数据类型
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);
}如果要往database添加item, 如下函数可实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15public 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
}在
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();
}
}删除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();
}获得数据库所有的item:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18public 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 | // 这个函数遍历整个数据库, 然后获取每个item的state值, 可能为0,1,2, 分别表示: 未选中, 修改卡返回的apdu数据, 修改读卡器的apdu数据 |
3. SessionLoggingDbHelper
这个类存储卡和读卡器的apdu数据.
Ⅱ. filter
这里是过滤一些不规范的apdu数据, 下面只分析部分FilterManager
:
1 | true/** |
Ⅲ. 其他
1. CustomTextWatcher
TextWatcher
有三个重要的方法, 顾名思义分别是TextView在改变前/改变时/改变后的动作:
1 | // 在改变前没有做任何修改 |
2. NfcComm
这个类定义了nfc apdu数据的多种数据类型.
3. RuleMatching
下示代码时修改UID的一个函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15public 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});
}
}以下是识别来自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
26private 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;
}
}
}
}以下三个函数时中间人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 | // 这个函数读取特定目录下的所有文件, 并返回文件名列表 |