分类目录归档:Android

Android 蓝牙连接 ESC/POS 热敏打印机打印(ESC/POS指令篇)

上一篇 主要介绍了如何通过蓝牙连接到打印机。这一篇,我们就介绍如何向打印机发送打印指令,来打印字符和图片。

=====================2017.05.09 更新====================

终于抽时间整了一个可以运行的demo出来,实现了以下功能:

  • 检测蓝牙开启状态
  • 显示已配对设备
  • 连接打印机
  • 打印测试,包括打印标题,打印两列三列文字,打印图片等

最终demo及打印的小票示例:

Demo界面
打印小票示例
========================以下是原文=======================

1. 构造输出流

首先要明确一点,就是蓝牙连接打印机这种场景下,手机是 Client 端,打印机是 Server 端。

在上一篇的最后,我们从 BluetoothSocket 得到了一个OutputStream。这里我们做一层包装,得到一个OutputStreamWriter 对象:

OutputStreamWriter writer = new OutputStreamWriter(outputStream, "GBK");

这样做主要是为了后面可以直接输出字符串,不然只能输出 int 或 byte 数据;

2. 常用打印指令

手机通过蓝牙向打印机发送的都是纯字节流,那么打印机如何知道该打印的是一个文本,还是条形码,还是图片数据呢?这里就要介绍 ESC/POS 打印控制命令

  • 初始化打印机
    初始化打印机指令

在每次打印开始之前要调用该指令对打印机进行初始化。向打印机发送这条指令对应的代码就是:

  protected void initPrinter() throws IOException {  
        writer.write(0x1B);  
        writer.write(0x40);  
        writer.flush();  
  }
  • 打印文本
    没有对应指令,直接输出
protected void printText(String text) throws IOException {  
        writer.write(text);
        writer.flush();
    } 
  • 设置文本对齐方式
文本对齐方式指令

对应的发送指令的代码:

    /* 设置文本对齐方式
     * @param align 打印位置  0:居左(默认) 1:居中 2:居右 
     * @throws IOException 
     */  
    protected void setAlignPosition(int align) throws IOException {  
        writer.write(0x1B);  
        writer.write(0x61);  
        writer.write(align);  
        writer.flush();  
    }

与初始化指令不同的是,这条指令带有一个参数n。

  • 换行和制表符
    直接输出对应的字符:
    protected void nextLine() throws IOException {  
        writer.write("\n");  
        writer.flush();  
    }

    protected void printTab(int length) throws IOException {  
        for (int i = 0; i < length; i++) {  
            writer.write("\t");  
        }  
        writer.flush();  
    }  

这两个指令在打印订单详情的时候使用最多。尤其是制表符,可以让每一列的文字对齐。

  • 设置行间距
设置行间距指令

n表示行间距为n个像素点,最大值256

protected void setLineGap(int gap) throws IOException {  
        writer.write(0x1B);  
        writer.write(0x33);  
        writer.write(gap);  
        writer.flush();  
}

这个指令在后面打印图片的时候会用到。

3. 打印图片

很多小票上面都会附上一个二维码,用户扫描之后,可以获得更多的信息。因为热敏打印机只能打印黑白两色,所以首先把图片转成纯黑白的,再调用图片打印指令进行打印。

3.1 打印图片指令

打印图片指令

这个指令的参数很多,一个一个来说:

  • m:取值十进制 0、1、32、33。设置打印精度,0、1对应每行8个点,32、33对应每行24个点,对应最高的打印精度(其实这里也没太搞清楚取值0、1或者取值32、33的区别,只要记住取值33,对应每行24个点,后面还有用)
  • n1, n2 : 表示图片的宽度,为什么有两个?其实只是分成了高位和低位两部分,因为每部分只有8bit,最大表示256。所以 n1 = 图片宽度 % 256,n2 = 图片宽度 / 256。假设图片宽300,那么n1=1,n2=44
  • d1 d2 ... dk 这部分就是转换成字节流的图像数据了

3.2 图片分辨率调整

如果分辨率过大,超过了打印机可打印的最大宽度,那么超出的部分将无法打印。我试验的这台最大宽度是 384 个像素点,超过这个宽度的数据无法被打印出来。所以在开始打印之前,我们需要调整图片的分辨率。代码如下:

    /**
     * 对图片进行压缩(去除透明度)
     *
     * @param bitmapOrg
     */
    public static Bitmap compressPic(Bitmap bitmap) {
        // 获取这个图片的宽和高
        int width = bitmap.getWidth();
        int height = bitmap.getHeight();
        // 指定调整后的宽度和高度
        int newWidth = 240;
        int newHeight = 240;
        Bitmap targetBmp = Bitmap.createBitmap(newWidth, newHeight, Bitmap.Config.ARGB_8888);
        Canvas targetCanvas = new Canvas(targetBmp);
        targetCanvas.drawColor(0xffffffff);
        targetCanvas.drawBitmap(bitmap, new Rect(0, 0, width, height), new Rect(0, 0, newWidth, newHeight), null);
        return targetBmp;
    }

3.2 图片黑白化处理

因为能够打印的图像只有黑白两色,所以需要先做黑白化的处理。这一部分其实又细分为彩色图片->灰度图片,灰度图片->黑白图片两步。直接上代码:

    /**
     * 灰度图片黑白化,黑色是1,白色是0
     *
     * @param x   横坐标
     * @param y   纵坐标
     * @param bit 位图
     * @return
     */
    public static byte px2Byte(int x, int y, Bitmap bit) {
        if (x < bit.getWidth() && y < bit.getHeight()) {
            byte b;
            int pixel = bit.getPixel(x, y);
            int red = (pixel & 0x00ff0000) >> 16; // 取高两位
            int green = (pixel & 0x0000ff00) >> 8; // 取中两位
            int blue = pixel & 0x000000ff; // 取低两位
            int gray = RGB2Gray(red, green, blue);
            if (gray < 128) {
                b = 1;
            } else {
                b = 0;
            }
            return b;
        }
        return 0;
    }

    /**
     * 图片灰度的转化
     */
    private static int RGB2Gray(int r, int g, int b) {
        int gray = (int) (0.29900 * r + 0.58700 * g + 0.11400 * b);  //灰度转化公式
        return gray;
    }

其中的灰度化转换公式是一个广为流传的公式,具体原理不明。我们直接看灰度转化为黑白的函数 px2Byte(int x, int y, Bitmap bit)。对于一个 Bitmap 中的任意一个坐标点,取出其 RGB 三色信息后做灰度化处理,然后对于灰度小于128的,用黑色表示,灰度大于128的,用白色表示。

3.3 逐行打印图片

其实打印图片和打印文本是一样的,也是一行一行的打印。直接上代码吧,注释已经尽量详细了。

    /*************************************************************************
     * 假设一个240*240的图片,分辨率设为24, 共分10行打印
     * 每一行,是一个 240*24 的点阵, 每一列有24个点,存储在3个byte里面。
     * 每个byte存储8个像素点信息。因为只有黑白两色,所以对应为1的位是黑色,对应为0的位是白色
     **************************************************************************/
    /**
     * 把一张Bitmap图片转化为打印机可以打印的字节流
     *
     * @param bmp
     * @return
     */
    public static byte[] draw2PxPoint(Bitmap bmp) {
        //用来存储转换后的 bitmap 数据。为什么要再加1000,这是为了应对当图片高度无法      
        //整除24时的情况。比如bitmap 分辨率为 240 * 250,占用 7500 byte,
        //但是实际上要存储11行数据,每一行需要 24 * 240 / 8 =720byte 的空间。再加上一些指令存储的开销,
        //所以多申请 1000byte 的空间是稳妥的,不然运行时会抛出数组访问越界的异常。
        int size = bmp.getWidth() * bmp.getHeight() / 8 + 1000;
        byte[] data = new byte[size];
        int k = 0;
        //设置行距为0的指令
        data[k++] = 0x1B;
        data[k++] = 0x33;
        data[k++] = 0x00;
        // 逐行打印
        for (int j = 0; j < bmp.getHeight() / 24f; j++) {
            //打印图片的指令
            data[k++] = 0x1B;
            data[k++] = 0x2A;
            data[k++] = 33; 
            data[k++] = (byte) (bmp.getWidth() % 256); //nL
            data[k++] = (byte) (bmp.getWidth() / 256); //nH
            //对于每一行,逐列打印
            for (int i = 0; i < bmp.getWidth(); i++) {
                //每一列24个像素点,分为3个字节存储
                for (int m = 0; m < 3; m++) {
                    //每个字节表示8个像素点,0表示白色,1表示黑色
                    for (int n = 0; n < 8; n++) {
                        byte b = px2Byte(i, j * 24 + m * 8 + n, bmp);
                        data[k] += data[k] + b;
                    }
                    k++;
                }
            }
            data[k++] = 10;//换行
        }
        return data;
    }

4. 总结

用两篇介绍了一个比较冷门的应用,纯粹是因为自己花了很多时间去搞懂原理,所以希望记录下来。尤其是图片打印部分,废了好多纸啊哈哈哈,一个字节操作错误,打印出来就是一堆乱码。感觉和 java 的 .class 文件很像,每一个指令占用多少位,每一位表示什么都是严格规定好的,不能超出也不能缺少。
最后希望能帮到需要的人吧,感觉网上这部分资料还是比较少的。

作者:VitaminChen
链接:https://www.jianshu.com/p/c0b6d1a4823b
來源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

Android 蓝牙连接 ESC/POS 热敏打印机打印(蓝牙连接篇)

公司的一个手机端的 CRM 项目最近要增加小票打印的功能,就是我们点外卖的时候经常会见到的那种小票。这里主要涉及到两大块的知识:

  • 蓝牙连接及数据传输
  • ESC/POS 打印指令

蓝牙连接不用说了,太常见了,这篇主要介绍这部分的内容。但ESC/POS 打印指令是个什么鬼?简单说,我们常见的热敏小票打印机都支持这样一种指令,只要按照指令的格式向打印机发送指令,哪怕是不同型号品牌的打印机也会执行相同的动作。比如打印一行文本,换行,加粗等都有对应的指令,这部分内容放在下一篇介绍。

本篇主要基于官方文档,相比官方文档,省去了大段的说明,更加便于快速上手。
demo及打印指令讲解请看下篇

1. 蓝牙权限

想要使用蓝牙功能,首先要在 AndroidManifest 配置文件中声明蓝牙权限:

<manifest> 
  <uses-permission android:name="android.permission.BLUETOOTH" />
  <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
  ...
</manifest>

BLUETOOTH 权限只允许建立蓝牙连接以及传输数据,但是如果要进行蓝牙设备发现等操作的话,还需要申请 BLUETOOTH_ADMIN 权限。

2. 初始配置

这里主要用到一个类 BluetoothAdapter。用法很简单,直接看代码:

BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (mBluetoothAdapter == null) {
    // Device does not support Bluetooth
}

单例模式,全局只有一个实例,只要为 null,就代表设备不支持蓝牙,那么需要有相应的处理。
如果设备支持蓝牙,那么接着检查蓝牙是否打开:

if (!mBluetoothAdapter.isEnabled()) {
    Intent intent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
    startActivityForResult(intent, REQUEST_ENABLE_BT);
}

如果蓝牙未打开,那么执行 startActivityForResult() 后,会弹出一个对话框询问是否要打开蓝牙,点击`是`之后就会自动打开蓝牙。成功打开蓝牙后就会回调到 onActivityResult()

除了主动的打开蓝牙,还可以监听 BluetoothAdapter.ACTION_STATE_CHANGED
广播,包含EXTRA_STATEEXTRA_PREVIOUS_STATE两个 extra 字段,可能的取值包括 STATE_TURNING_ON, STATE_ON, STATE_TURNING_OFF, and STATE_OFF。含义很清楚了,不解释。

3. 发现设备

初始化完成之后,蓝牙打开了,接下来就是扫描附近的设备,只需要一句话:

mBluetoothAdapter.startDiscovery();

不过这样只是开始执行设备发现,这肯定是一个异步的过程,我们需要注册一个广播,监听发现设备的广播,直接上代码:

private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();
        
        // 当有设备被发现的时候会收到 action == BluetoothDevice.ACTION_FOUND 的广播
        if (BluetoothDevice.ACTION_FOUND.equals(action)) {

            //广播的 intent 里包含了一个 BluetoothDevice 对象
            BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);

            //假设我们用一个 ListView 展示发现的设备,那么每收到一个广播,就添加一个设备到 adapter 里
            mArrayAdapter.add(device.getName() + "\n" + device.getAddress());
        }
    }
};
// 注册广播监听
IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
registerReceiver(mReceiver, filter); // Don't forget to unregister during onDestroy

注释已经写的很清楚了,除了 BluetoothDevice.EXTRA_DEVICE 之外,还有一个 extra 字段 BluetoothDevice.EXTRA_CLASS, 可以得到一个 BluetoothClass 对象,主要用来保存设备的一些额外的描述信息,比如可以知道这是否是一个音频设备。

关于设备发现,有两点需要注意:

  • startDiscovery() 只能扫描到那些状态被设为 可发现 的设备。安卓设备默认是不可发现的,要改变设备为可发现的状态,需要如下操作:
Intent intent = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
//设置可被发现的时间,300s
intent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 300);
startActivity(intent);

执行之后会弹出对话窗询问是否允许设备被设为可发现的状态,点击`是`之后设备即被设为可发现的状态。

  • startDiscovery()是一个十分耗费资源的操作,所以需要及时的调用cancelDiscovery()来释放资源。比如在进行设备连接之前,一定要先调用cancelDiscovery()

4. 设备配对与连接

4.1 配对

当与一个设备第一次进行连接操作的时候,屏幕会弹出提示框询问是否允许配对,只有配对成功之后,才能建立连接。
系统会保存所有的曾经成功配对过的设备信息。所以在执行startDiscovery()之前,可以先尝试查找已配对设备,因为这是一个本地信息读取的过程,所以比startDiscovery()要快得多,也避免占用过多资源。如果设备在蓝牙信号的覆盖范围内,就可以直接发起连接了。

查找配对设备的代码如下:

Set<BluetoothDevice> pairedDevices = mBluetoothAdapter.getBondedDevices();
if (pairedDevices.size() > 0) {
    for (BluetoothDevice device : pairedDevices) {
        mArrayAdapter.add(device.getName() + "\n" + device.getAddress());
    }
}

代码很简单,不解释了,就是调用BluetoothAdapter.getBondedDevices()得到一个 Set<BluetoothDevice> 并遍历取得已配对的设备信息。

4.2 连接

蓝牙设备的连接和网络连接的模型十分相似,都是Client-Server 模式,都通过一个 socket 来进行数据传输。那么作为一个 Android 设备,就存在三种情况:

  • 只作为 Client 端发起连接
  • 只作为 Server 端等待别人发起建立连接的请求
  • 同时作为 Client 和 Server

因为是为了下一篇介绍连接热敏打印机打印做铺垫,所以这里先讲 Android 设备作为 Client 建立连接的情况。因为打印机是不可能主动跟 Android 设备建立连接的,所以打印机必然是作为 Server 被连接。

4.2.1 作为 Client 连接
  1. 首先需要获取一个 BluetoothDevice 对象。获取的方法前面其实已经介绍过了,可以通过调用 startDiscovery()并监听广播获得,也可以通过查询已配对设备获得。
  2. 通过 BluetoothDevice.createRfcommSocketToServiceRecord(UUID) 得到 BluetoothSocket 对象
  3. 通过BluetoothSocket.connect()建立连接
  4. 异常处理以及连接关闭

废话不多说,上代码:

private class ConnectThread extends Thread {
    private final BluetoothSocket mmSocket;
    private final BluetoothDevice mmDevice;
 
    public ConnectThread(BluetoothDevice device) {

        BluetoothSocket tmp = null;
        mmDevice = device;
        try {
            // 通过 BluetoothDevice 获得 BluetoothSocket 对象
            tmp = device.createRfcommSocketToServiceRecord(MY_UUID);
        } catch (IOException e) { }
        mmSocket = tmp;
    }
     
    @Override
    public void run() {
        // 建立连接前记得取消设备发现
        mBluetoothAdapter.cancelDiscovery();
        try {
            // 耗时操作,所以必须在主线程之外进行
            mmSocket.connect();
        } catch (IOException connectException) {
            //处理连接建立失败的异常
            try {
                mmSocket.close();
            } catch (IOException closeException) { }
            return;
        }
        doSomething(mmSocket);
    }
 
    //关闭一个正在进行的连接
    public void cancel() {
        try {
            mmSocket.close();
        } catch (IOException e) { }
    }
}

device.createRfcommSocketToServiceRecord(MY_UUID) 这里需要传入一个 UUID,这个UUID 需要格外注意一下。简单的理解,它是一串约定格式的字符串,用来唯一的标识一种蓝牙服务。

Client 发起连接时传入的 UUID 必须要和 Server 端设置的一样!否则就会报错!

如果是连接热敏打印机这种情况,不知道 Server 端设置的 UUID 是什么怎么办?
不用担心,因为一些常见的蓝牙服务协议已经有约定的 UUID。比如我们连接热敏打印机是基于 SPP 串口通信协议,其对应的 UUID 是 “00001101-0000-1000-8000-00805F9B34FB”,所以实际的调用是这样:

device.createRfcommSocketToServiceRecord(UUID.fromString("00001101-0000-1000-8000-00805F9B34FB"))

其他常见的蓝牙服务的UUID大家可以自行搜索。如果只是用于自己的应用之间的通信的话,那么理论上可以随便定义一个 UUID,只要 server 和 client 两边使用的 UUID 一致即可。更多关于 UUID 的介绍可以参考这里

4.2.2 作为 Server 连接
  1. 通过BluetoothAdapter.listenUsingRfcommWithServiceRecord(String, UUID)获取一个 BluetoothServerSocket 对象。这里传入的第一个参数用来设置服务的名称,当其他设备扫描的时候就会显示这个名称。UUID 前面已经介绍过了。
  2. 调用BluetoothServerSocket.accept()开始监听连接请求。这是一个阻塞操作,所以当然也要放在主线程之外进行。当该操作成功执行,即有连接建立的时候,会返回一个BluetoothSocket 对象。
  3. 调用 BluetoothServerSocket.close() 会关闭监听连接的服务,但是当前已经建立的链接并不会受影响。

还是看代码吧:

private class AcceptThread extends Thread {

    private final BluetoothServerSocket mmServerSocket;
 
    public AcceptThread() {

        BluetoothServerSocket tmp = null;
        try {
            // client 必须使用一样的 UUID !!!
            tmp = mBluetoothAdapter.listenUsingRfcommWithServiceRecord(NAME, MY_UUID);
        } catch (IOException e) { }
        mmServerSocket = tmp;
    }

    @Override
    public void run() {
        BluetoothSocket socket = null;
        //阻塞操作
        while (true) {
            try {
                socket = mmServerSocket.accept();
            } catch (IOException e) {
                break;
            }
            //直到有有连接建立,才跳出死循环
            if (socket != null) {
                //要在新开的线程执行,因为连接建立后,当前线程可能会关闭
                doSomething(socket);
                mmServerSocket.close();
                break;
            }
        }
    }
 
    public void cancel() {
        try {
            mmServerSocket.close();
        } catch (IOException e) { }
    }
}

5. 数据传输

终于经过了前面的4步,万事俱备只欠东风。而最后这一部分其实是最简单的,因为就只是简单的利用 InputStreamOutputStream进行数据的收发。
示例代码:

private class ConnectedThread extends Thread {
    private final BluetoothSocket mmSocket;
    private final InputStream mmInStream;
    private final OutputStream mmOutStream;
 
    public ConnectedThread(BluetoothSocket socket) {
        mmSocket = socket;
        InputStream tmpIn = null;
        OutputStream tmpOut = null;
        //通过 socket 得到 InputStream 和 OutputStream
        try {
            tmpIn = socket.getInputStream();
            tmpOut = socket.getOutputStream();
        } catch (IOException e) { }
 
        mmInStream = tmpIn;
        mmOutStream = tmpOut;
    }
 
    public void run() {
        byte[] buffer = new byte[1024];  // buffer store for the stream
        int bytes; // bytes returned from read()
 
        //不断的从 InputStream 取数据
        while (true) {
            try {
                bytes = mmInStream.read(buffer);
                mHandler.obtainMessage(MESSAGE_READ, bytes, -1, buffer)
                        .sendToTarget();
            } catch (IOException e) {
                break;
            }
        }
    }
 
    //向 Server 写入数据
    public void write(byte[] bytes) {
        try {
            mmOutStream.write(bytes);
        } catch (IOException e) { }
    }
 
    public void cancel() {
        try {
            mmSocket.close();
        } catch (IOException e) { }
    }
}

下一篇介绍通过手机操作热敏打印机打印的时候,还会用到这部分内容,所以这里就先不多讲了。

作者:VitaminChen
链接:https://www.jianshu.com/p/0fe3a7e06f57
來源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

[Android] 使用Retrofit如何避免Man in the middle攻擊

前幾天看到朋友寫如何用Charles Proxy來debug REST API, 突然就想到了這個問題, Charles Proxy是一個不錯的工具, 我自己也蠻常用的 , 除了可以用來debug HTTP以外, 其實HTTPS也可以(參照SSL proxying) , 只要在手機上安裝好它的SSL certificate即可

不過這不是這篇要講的重點, 這種debugging的方式原理即是用Proxy作為一個中間人來紀錄HTTP/HTTPS的傳輸內容 , 這也帶來一個問題, 也就是你的資料是可以這樣簡單的暴露出去, 這種即是所謂的MITM (Man in the middle) 中間人攻擊

graph LR; Client–>Proxy[Proxy – Middle man]; Proxy[Proxy – Middle man]–>Client; Proxy[Proxy – Middle man]–>Server; Server–>Proxy[Proxy – Middle man];

一般來說, 用了SSL/HTTPS後, 也不是那麼容易安插一個中間人的 ,像Charles Proxy這樣的東西, 還是需要使用者自己去手機上的安全性設定裝信任憑證(trusted certificate), 一般常見的問題反而來自於開發者本身 , 很多人常以為走了HTTPS就安全了, 卻常常為了debug方便, 或是省錢用Self-signed certificate 而去覆寫了系統預設的 X509TrustManager, 以至於整個檢查機制被跳過, 真的漏洞都來自於人的惰性, 更多資訊可以參考:

  1. 窃听风暴: Android平台https嗅探劫持漏洞
  2. Android安全之Https中间人攻击漏洞
  3. Android HTTPS中间人劫持漏洞浅析
  4. Android客户端安全 -> HTTPS敏感数据劫持漏洞
  5. Android App 安全的HTTPS 通信
  6. 手機應用程式開發上被忽略的 SSL 處理
  7. How To: Use mitmproxy to read and modify HTTPS traffic
  8. INTERCEPTING ANDROID SSL / HTTPS TRAFFIC

該避免去寫的, 就該避掉, 以免產生不必要的漏洞, 但不過也有可能碰到使用者的手機被(有意/無意)安裝了攻擊者的憑證到系統的信任憑證中, 那麼碰到這種, 應用程式本身該如何保護自己?

這邊有兩個方式可以採用 –

  1. certificate pinning
  2. 在程式中寫死certificate

以下的範例以Android最紅, 常被使用的Retrofit為範例(雖說是Retrofit, 但其實是在okhttp動的手腳)

Certificate Pinning

或叫HTTP Public Key Pinning (HPKP), 簡單的說, 就是在程式內綁定Certificate的public key, 如果今天中間人存在, certificate不同了, 連接就不會成功

先來看範例

OkHttpClient client = new OkHttpClient.Builder()
        .certificatePinner(new CertificatePinner.Builder().add("cdn.rawgit.com", "sha256/p0962nIqD0mv1APQ1mgmRiuwrhZXBuj+t6dey/Adk0U=").build())
        .build();
Retrofit retrofit = new Retrofit.Builder()
        .baseUrl("https://cdn.rawgit.com/julianshen/cpblschedule/")
        .addConverterFactory(GsonConverterFactory.create())
        .client(client)
        .build();

這邊所動的手腳(跟Retrofit沒啥鳥關係)是在OkHttpClient上加上一個CertificatePinner, 利用了certificate pinner加上了一個host name跟public key的對應, 以這例子來說: “sha256/p0962nIqD0mv1APQ1mgmRiuwrhZXBuj+t6dey/Adk0U=” 這個public key對應的是 “cdn.rawgit.com” , 因此碰到”cdn.rawgit.com”來的url, 會檢查public key是否符合, 這邊public key的參數字串是要以”sha1/”或”sha256/”開始的, 依使用的演算法而不同

但又要怎取得這串”天書”呢?

很簡單, 首先確認你的電腦裡面有沒安裝openssl , 有的話就用以下的指令:

openssl s_client -connect cdn.rawgit.com:443 -servername cdn.rawgit.com | openssl x509 -pubkey -noout | openssl rsa -pubin -outform der | openssl
dgst -sha256 -binary | openssl enc -base64

最後產生的內容如下

depth=2 /C=GB/ST=Greater Manchester/L=Salford/O=COMODO CA Limited/CN=COMODO RSA Certification Authority
verify error:num=20:unable to get local issuer certificate
verify return:0
writing RSA key
p0962nIqD0mv1APQ1mgmRiuwrhZXBuj+t6dey/Adk0U=

所以我們要填的參數就是 “sha256/p0962nIqD0mv1APQ1mgmRiuwrhZXBuj+t6dey/Adk0U=”

關於HPKP的openssl相關的指令可以參考Public Key Pinning

如果你用Charles Proxy當中間人來測試這段程式的話, 你會得到下面這樣的exception

javax.net.ssl.SSLPeerUnverifiedException: Certificate pinning failure!
                                            Peer certificate chain:
                                            sha256/ENlqFHtfARof3AK50Hbc1sj47M5hWhc5kQ5Z2vyfxq4=: CN=rawgit.com,OU=PositiveSSL Multi-Domain,OU=Domain Control Validated
                                            sha256/mXmLzo8k5ANwi11PlLSW/b4OC/Anjw1OeACDyZxD/WM=: C=NZ,ST=Auckland,L=Auckland,O=XK72 Ltd,OU=http://charlesproxy.com/ssl,CN=Charles Proxy Custom Root Certificate (built on Jlnmbp-retina.local\, 11 十二月 2015)
                                            Pinned certificates for cdn.rawgit.com:
                                            sha256/p0962nIqD0mv1APQ1mgmRiuwrhZXBuj+t6dey/Adk0U=

在程式中寫死certificate

這方法聽起來暴力了一點, 但原理其實跟前一個沒啥太大差別, 只是一個寫死public key一個寫死certificate

一樣先來看個範例:

String cert = ""
        +"-----BEGIN CERTIFICATE-----\n"
        +"MIIFdjCCBF6gAwIBAgIRAPbTBHdHHseM4hArA7n6uREwDQYJKoZIhvcNAQELBQAw\n"
        +"gZAxCzAJBgNVBAYTAkdCMRswGQYDVQQIExJHcmVhdGVyIE1hbmNoZXN0ZXIxEDAO\n"
        +"BgNVBAcTB1NhbGZvcmQxGjAYBgNVBAoTEUNPTU9ETyBDQSBMaW1pdGVkMTYwNAYD\n"
        +"VQQDEy1DT01PRE8gUlNBIERvbWFpbiBWYWxpZGF0aW9uIFNlY3VyZSBTZXJ2ZXIg\n"
        +"Q0EwHhcNMTYwMTAxMDAwMDAwWhcNMTcwMTEzMjM1OTU5WjBbMSEwHwYDVQQLExhE\n"
        +"b21haW4gQ29udHJvbCBWYWxpZGF0ZWQxITAfBgNVBAsTGFBvc2l0aXZlU1NMIE11\n"
        +"bHRpLURvbWFpbjETMBEGA1UEAxMKcmF3Z2l0LmNvbTCCASIwDQYJKoZIhvcNAQEB\n"
        +"BQADggEPADCCAQoCggEBALD2sUNSYp2R+mSEMF2qKvMS780qTkltvqP4rwEGKOLV\n"
        +"rR5QQWo8vzSlgZvxVsguRHi0pPBtVAH794L43tD+IuyQlDlNU2qc1aMqBkn3S2wN\n"
        +"Way8BS9w80pgeFnObZiFJtPI9pdwcB72Bgq8Nlc25oVrDVWr/Q8nLIKS/9FkNs+C\n"
        +"MPU00vGFZSFbR7s15ORj9+qPCskWZcHpQ+m9EKmZD3IVKj3QQyBD17cBoVkYoIpj\n"
        +"I8/r+NfVfKetlsB7Pcv8P3yLFVFC4+PGrnW9TLZK9aRtbDTvjElXwCQIPK5B2fKw\n"
        +"SJK26IJPDX0r1JkeuR53Afr509iEx3xAHcRz/kR5uo8CAwEAAaOCAf0wggH5MB8G\n"
        +"A1UdIwQYMBaAFJCvajqUWgvYkOoSVnPfQ7Q6KNrnMB0GA1UdDgQWBBSI83Kqafo7\n"
        +"kg9cmyT1vnceH4fAUjAOBgNVHQ8BAf8EBAMCBaAwDAYDVR0TAQH/BAIwADAdBgNV\n"
        +"HSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwTwYDVR0gBEgwRjA6BgsrBgEEAbIx\n"
        +"AQICBzArMCkGCCsGAQUFBwIBFh1odHRwczovL3NlY3VyZS5jb21vZG8uY29tL0NQ\n"
        +"UzAIBgZngQwBAgEwVAYDVR0fBE0wSzBJoEegRYZDaHR0cDovL2NybC5jb21vZG9j\n"
        +"YS5jb20vQ09NT0RPUlNBRG9tYWluVmFsaWRhdGlvblNlY3VyZVNlcnZlckNBLmNy\n"
        +"bDCBhQYIKwYBBQUHAQEEeTB3ME8GCCsGAQUFBzAChkNodHRwOi8vY3J0LmNvbW9k\n"
        +"b2NhLmNvbS9DT01PRE9SU0FEb21haW5WYWxpZGF0aW9uU2VjdXJlU2VydmVyQ0Eu\n"
        +"Y3J0MCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5jb21vZG9jYS5jb20wSwYDVR0R\n"
        +"BEQwQoIKcmF3Z2l0LmNvbYIVY2RuLW9yaWdpbi5yYXdnaXQuY29tgg5jZG4ucmF3\n"
        +"Z2l0LmNvbYINcmF3Z2l0aHViLmNvbTANBgkqhkiG9w0BAQsFAAOCAQEASx3e6y9e\n"
        +"9F67N5NIDausDHDL+/fz6uj2DDNJdaQvALAYDV8hKpz7+QGotlQfI042U2i83J7m\n"
        +"DGZiKDpzaXa7IFfGiq6PFPUjntElHoU2E4vRb5LApg1sJ5YueYmRd3X7x0/jPYg6\n"
        +"TiTtHyXOnlMuqL2FUYJM0BP7cMfOppvUF0R2zHUVA0rVHuLrSStg8bMg8aVUIkKg\n"
        +"n3NS+eg4ofo95jaMRVykhLnylkYBk9dWBM6B19Yw7LgQd94MZSa+Xix4HxeFgvAM\n"
        +"BF+oeDMaY7mEN4Xm5hnrIS1FElRDq/ckpDV8JVpR4SQqB+tzSZPPIiep8EvgRDzd\n"
        +"2EkL187FF3MQ+w==\n"
        +"-----END CERTIFICATE-----\n";

X509TrustManager trustManager = null;
SSLSocketFactory sslSocketFactory = null;

try {
    CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
    Collection<? extends Certificate> certificates = certificateFactory.generateCertificates(new Buffer().writeUtf8(cert).inputStream());
    if (certificates.isEmpty()) {
        throw new IllegalArgumentException("expected non-empty set of trusted certificates");
    } else {
        char[] password = "password".toCharArray(); // Any password will work.
        KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
        keyStore.load(null, password);

        int index = 0;
        for (Certificate certificate : certificates) {
            String certificateAlias = Integer.toString(index++);
            keyStore.setCertificateEntry(certificateAlias, certificate);
        }

        TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
        trustManagerFactory.init(keyStore);

        TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
        if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) {
            throw new IllegalStateException("Unexpected default trust managers:"
                    + Arrays.toString(trustManagers));
        }
        trustManager = (X509TrustManager) trustManagers[0];

        SSLContext sslContext = SSLContext.getInstance("TLS");
        sslContext.init(null, new TrustManager[] { trustManager }, null);
        sslSocketFactory = sslContext.getSocketFactory();
    }
} catch (CertificateException e) {
    e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
    e.printStackTrace();
} catch (KeyStoreException e) {
    e.printStackTrace();
} catch (IOException e) {
    e.printStackTrace();
} catch (KeyManagementException e) {
    e.printStackTrace();
}

OkHttpClient client = new OkHttpClient.Builder()
        .sslSocketFactory(sslSocketFactory, trustManager)
        .build();

Retrofit retrofit = new Retrofit.Builder()
        .baseUrl("https://cdn.rawgit.com/julianshen/cpblschedule/")
        .addConverterFactory(GsonConverterFactory.create())
        .client(client)
        .build();

看到這麼長的東西大概就不會想看了吧?(老實說我也寫的很懶, 這邊要特別提到有用到一個Class叫做Buffer,這是來自於okio, 蠻方便的東西), 跟前一個不同的地方在於, 前一個利用了CertificatePinner, 而這一個則是改了sslSocketFactory的TrustManager(喂!!!前面不是隱約有提到這樣不太好?!), 這個TrustManager全部也只信任這一個certifcate, 如果碰到其他的, 就會發生以下的exception:

javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.
        at com.android.org.conscrypt.OpenSSLSocketImpl.startHandshake(OpenSSLSocketImpl.java:328)

那, 上面那一大坨憑證資料是怎樣取得的? 方法其實差不多:

openssl s_client -connect cdn.rawgit.com:443 -showcerts

會得到這樣的結果:

CONNECTED(00000003)
depth=2 /C=GB/ST=Greater Manchester/L=Salford/O=COMODO CA Limited/CN=COMODO RSA Certification Authority
verify error:num=20:unable to get local issuer certificate
verify return:0
---
Certificate chain
 0 s:/OU=Domain Control Validated/OU=PositiveSSL Multi-Domain/CN=rawgit.com
   i:/C=GB/ST=Greater Manchester/L=Salford/O=COMODO CA Limited/CN=COMODO RSA Domain Validation Secure Server CA
 1 s:/OU=Domain Control Validated/OU=PositiveSSL Multi-Domain/CN=rawgit.com
   i:/C=GB/ST=Greater Manchester/L=Salford/O=COMODO CA Limited/CN=COMODO RSA Domain Validation Secure Server CA
 2 s:/C=GB/ST=Greater Manchester/L=Salford/O=COMODO CA Limited/CN=COMODO RSA Domain Validation Secure Server CA
   i:/C=GB/ST=Greater Manchester/L=Salford/O=COMODO CA Limited/CN=COMODO RSA Certification Authority
 3 s:/C=GB/ST=Greater Manchester/L=Salford/O=COMODO CA Limited/CN=COMODO RSA Certification Authority
   i:/C=SE/O=AddTrust AB/OU=AddTrust External TTP Network/CN=AddTrust External CA Root
---
Server certificate
-----BEGIN CERTIFICATE-----
MIIFdjCCBF6gAwIBAgIRAPbTBHdHHseM4hArA7n6uREwDQYJKoZIhvcNAQELBQAw
gZAxCzAJBgNVBAYTAkdCMRswGQYDVQQIExJHcmVhdGVyIE1hbmNoZXN0ZXIxEDAO
BgNVBAcTB1NhbGZvcmQxGjAYBgNVBAoTEUNPTU9ETyBDQSBMaW1pdGVkMTYwNAYD
VQQDEy1DT01PRE8gUlNBIERvbWFpbiBWYWxpZGF0aW9uIFNlY3VyZSBTZXJ2ZXIg
Q0EwHhcNMTYwMTAxMDAwMDAwWhcNMTcwMTEzMjM1OTU5WjBbMSEwHwYDVQQLExhE
b21haW4gQ29udHJvbCBWYWxpZGF0ZWQxITAfBgNVBAsTGFBvc2l0aXZlU1NMIE11
bHRpLURvbWFpbjETMBEGA1UEAxMKcmF3Z2l0LmNvbTCCASIwDQYJKoZIhvcNAQEB
BQADggEPADCCAQoCggEBALD2sUNSYp2R+mSEMF2qKvMS780qTkltvqP4rwEGKOLV
rR5QQWo8vzSlgZvxVsguRHi0pPBtVAH794L43tD+IuyQlDlNU2qc1aMqBkn3S2wN
Way8BS9w80pgeFnObZiFJtPI9pdwcB72Bgq8Nlc25oVrDVWr/Q8nLIKS/9FkNs+C
MPU00vGFZSFbR7s15ORj9+qPCskWZcHpQ+m9EKmZD3IVKj3QQyBD17cBoVkYoIpj
I8/r+NfVfKetlsB7Pcv8P3yLFVFC4+PGrnW9TLZK9aRtbDTvjElXwCQIPK5B2fKw
SJK26IJPDX0r1JkeuR53Afr509iEx3xAHcRz/kR5uo8CAwEAAaOCAf0wggH5MB8G
A1UdIwQYMBaAFJCvajqUWgvYkOoSVnPfQ7Q6KNrnMB0GA1UdDgQWBBSI83Kqafo7
kg9cmyT1vnceH4fAUjAOBgNVHQ8BAf8EBAMCBaAwDAYDVR0TAQH/BAIwADAdBgNV
HSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwTwYDVR0gBEgwRjA6BgsrBgEEAbIx
AQICBzArMCkGCCsGAQUFBwIBFh1odHRwczovL3NlY3VyZS5jb21vZG8uY29tL0NQ
UzAIBgZngQwBAgEwVAYDVR0fBE0wSzBJoEegRYZDaHR0cDovL2NybC5jb21vZG9j
YS5jb20vQ09NT0RPUlNBRG9tYWluVmFsaWRhdGlvblNlY3VyZVNlcnZlckNBLmNy
bDCBhQYIKwYBBQUHAQEEeTB3ME8GCCsGAQUFBzAChkNodHRwOi8vY3J0LmNvbW9k
b2NhLmNvbS9DT01PRE9SU0FEb21haW5WYWxpZGF0aW9uU2VjdXJlU2VydmVyQ0Eu
Y3J0MCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5jb21vZG9jYS5jb20wSwYDVR0R
BEQwQoIKcmF3Z2l0LmNvbYIVY2RuLW9yaWdpbi5yYXdnaXQuY29tgg5jZG4ucmF3
Z2l0LmNvbYINcmF3Z2l0aHViLmNvbTANBgkqhkiG9w0BAQsFAAOCAQEASx3e6y9e
9F67N5NIDausDHDL+/fz6uj2DDNJdaQvALAYDV8hKpz7+QGotlQfI042U2i83J7m
DGZiKDpzaXa7IFfGiq6PFPUjntElHoU2E4vRb5LApg1sJ5YueYmRd3X7x0/jPYg6
TiTtHyXOnlMuqL2FUYJM0BP7cMfOppvUF0R2zHUVA0rVHuLrSStg8bMg8aVUIkKg
n3NS+eg4ofo95jaMRVykhLnylkYBk9dWBM6B19Yw7LgQd94MZSa+Xix4HxeFgvAM
BF+oeDMaY7mEN4Xm5hnrIS1FElRDq/ckpDV8JVpR4SQqB+tzSZPPIiep8EvgRDzd
2EkL187FF3MQ+w==
-----END CERTIFICATE-----
subject=/OU=Domain Control Validated/OU=PositiveSSL Multi-Domain/CN=rawgit.com
issuer=/C=GB/ST=Greater Manchester/L=Salford/O=COMODO CA Limited/CN=COMODO RSA Domain Validation Secure Server CA
---
No client certificate CA names sent
---
SSL handshake has read 6716 bytes and written 456 bytes
---
New, TLSv1/SSLv3, Cipher is DHE-RSA-AES256-SHA
Server public key is 2048 bit
Secure Renegotiation IS supported
Compression: NONE
Expansion: NONE
SSL-Session:
    Protocol  : TLSv1
    Cipher    : DHE-RSA-AES256-SHA
    Session-ID: ACFED5197F4B5F64DBAAC97A47380CD9283EFD1797E27EF43A215B06982698F4
    Session-ID-ctx: 
    Master-Key: 856DFB81C863F461FE75EE11E633EA5CAB3CD6683563F597F406EF19AB37CD3F45B4FE659AB8726F9291360CBE856F48
    Key-Arg   : None
    Start Time: 1475223830
    Timeout   : 300 (sec)
    Verify return code: 0 (ok)
---

BEGIN到END那段剪貼下來即是

缺點

這兩個方法有一個極大的缺點, 如果沒有解決方案還是最好別用(啊講那麼多, 最後不能用?!) , 這缺點就是SSL Certificate是有時效性的, 因此server端的憑證是會換會改變的, 只要server端一換, 就可能造成client端的失效, 所以如果要使用這兩個方法, 前提必須先解決的是如何更新Client端要檢查的public key或certificate, 這邊就不討論這部份的機制了, 這應該有很多方法可以來作才對

#好久沒寫關於Android的內容了

[转]《教我兄弟学Android逆向04 动态调试smali代码 》

上一篇《教我兄弟学逆向03 破解第一个Android游戏》我带你用另一种方式破解了切水果游戏 我布置的课后作业你也完成了 并且自己又独立破解了另外几款游戏  很不错 在这里表扬一下-旭 ! 希望其他同学都能向旭同学学习  努力提高自己的逆向技术!

要么学!要么不学!学和不学之间没有中间值 不学就放弃,学就要去认真的学!    –致选择

在开始本节课之前我有个问题 为什么要学习动态调试smali 也就是为什么要学习本节课?


答: 因为我在招聘网站上面看到很多招逆向的公司都要求要会动态调试smali代码 因为《教我兄弟学逆向》系列课程就是为找工作而生的 并且有些东西通过静态分析是分析不出来的 要动静结合分析出来的东西才能更加全面。

那么本节课要教你的是用AndroidStudio动态调试smali 下面开始今天的教程

既然这节课我要教你的是用AndroidStudio动态调试smali 那么在开始动态调试smali之前 首先要把要调试的apk反编译成smali文件然后导入到用AndroidStudio中 对不对?
那么把怎么把这个apk反编译成smali文件呢?


你回答:我以前都是直接把apk拉到Androidkiller这个软件中 然后AndroidKiller自动就会把apk反编译成smali文件了

回答正确 可以啊兄弟! 用现在流行的一句话说 给你打call !

那么我现在给你演示这种方式

一.用AndroidKiller把apk反编译成smali文件

apk链接:https://pan.baidu.com/s/1smNIrST 密码:uzsa

1. 首先把apk拉到AndroidKiller中 点工程管理器 点smali目录右键 打开方式-打开文件路径

2.在E盘新建一个jwx02文件夹 然后把打开文件路径目录里的smali文件夹复制到E盘并改名为src 然后就可以用AndroidStudio导入src了

二  要用AndroidStudio动态调试smali代码 首先要安装ideasmali插件
1.打开AndroidStudio
2. 安装ideasmali插件,File->Settings->Plugins,下载安装ideasmali插件。

ideasmali下载链接:
链接:https://pan.baidu.com/s/1htl26qS 密码:0pre

三. 打开Android Device Monitor

四. 找到要调试APK的包名和入口的Activity

1.用AndroidKiller打开要调试的APK 这里是jwx02.apk

2.记住包名hfdcxy.com.myapplication 和入口的Activity名 hfdcxy.com.myapplication.MainActivity

3.这里要注意一下 在application标签里面要有 android:debuggable=”true” 这句代码,没有是不能调试apk的  现在调试的这个例子默认是有这句代码的 有的apk中没有这句代码要自行添加上

五 把自己要调试的APK安装到手机中 并用数据线连接手机 打开USB调试模式

六 打开命令行 输入命令 adb shell am start -D -n hfdcxy.com.myapplication/hfdcxy.com.myapplication.MainActivity  对apk进行动态调试 这条命令运行后手机屏幕将会进入到调试界面(如果手机屏幕没有进入到调试界面说明USB没有连接好手机 或者USB调试模式 没有打开 或者其他原因)

七.查看Android Device Monitor 记住下图圈起来的两个值 然后把Android Device Monitor关掉(这里一定要关掉,因为它会占用8700端口 导致后面转发端口失败)

八.打开命令行窗口 输入命令 adb forward tcp:8700 jdwp:19509    转发8700 端口 (这里 jdwp是自己Android Device Monitor中要调试app的Online值 )

九. 导入jwx02文件
1.用AndroidStudio导入jwx02

2.然后一路Next 最后点Finsh

十 点击Android选择Project 对之前反编译的 jwx02/src文件夹右键–>Make Directory As —>Source Root

十一 配置远程调试的选项,选择Run–>Edit Configurations 并增加一个Remote调试的调试选项,端口选择:8700

十二 选择File–>Project Structure 配置JDK

十三 在Smali中下好断点 断点的意思是程序运行到下断点的那行代码就会断下来

十四 下好断点之后Run–>Debug’smali’,这里的smali是我们之前配置好的调试选项

十五 此时手机会进入程序 在手机上输入用户名和密码 点登陆 程序会断在我下的断点处

十六 在自己关心的语句前面下断点按F8执行下一步 F9运行程序 可以看到程序执行的逻辑 Watches窗口中可以点+按钮添加自己想看的寄存器的名字 就可以看到这个寄存器的值了

结束
本节课给你讲了用Android动态调试smali代码 当然教程讲的并没有这么详细 破解过程中遇到不懂得自己去百度 再不懂的就来问我 学习逆向就是这样 学不会就会感觉迷茫 但是不用怕 记住多迷茫几次就都学会了 本节课的例子一定要多练习 多跟着课程做几遍 自己多下断点看程序的执行逻辑 每节课布置的课后作业一定要完成 课后作业都不做的话 那我的帖子你也不用继续跟了 学而不思则罔,思而不学则殆 ,不仅要完成还要能理解 那么本节课教程到此结束 下节课我将教你在smali中代码中插入log 。

课后作业:
把本节课的例子动态调试三遍 尝试在不同位置下断点 多下断点 多看寄存器的值 分析程序执行的逻辑