中国健身手环逆向工程的历史

购买中国手镯,对官方软件感到失望,自行编写!




这个故事等待出版已经超过六个月,在这段时间里发生了很多变化,固件和软件已更新,我的许多开发工作已经过时了。

前言


众多公司在可穿戴技术和智能手表领域的积极工作并没有使我心安理得。我看到了带有屏幕的可穿戴设备的巨大潜力。不,我不是在谈论数步和其他健身运动,它们当然很酷,但与平庸的“恭喜!您走了4公里,走了20k多步!”以及精美的进度和回归图表,他们没有提出任何特别的建议。
但是我可以直接在手腕上的显示器上收到通知这一事实很方便。如果我还可以通过按1-2-3按钮与他或附近的某人进行交互-那就更酷了。

再次耕作速卖通的过程,我遇到了一个iWown i5健身手链。他立即以低得难以置信的价格(当时免费送货约800r)和OLED显示器引起了我的注意。仔细阅读卖方的描述和客户评论后,我决定订购此奇迹。

声明的规格(来自速卖通的翻译说明):
  • 显示方式:OLED
  • 电池:锂聚合物
  • 充电:标准USB充电
  • 待机时间:72小时以上
  • 尺寸:69.1 * 15.8 * 11.2mm
  • 重量:18克
  • 材质:ABS表带,钢扣
  • 防水等级:IP55
  • 工作温度:-20°C〜+ 45°C
  • 闪存介质的工作温度:-40°C〜+ 45°C


能力:
  • 运动监控器:所有时间都记录步数和运动,行进的距离和燃烧的卡路里,所有数字均考虑到您的体重和身高。
  • 监控睡眠质量:睡眠时,跟踪器会记录睡眠的阶段,确定深度睡眠和快速睡眠,八组静音警报可让您唤醒自己而不会打扰其他家庭成员
  • 蓝牙4.0低功耗无线同步
  • 通过USB支持PC同步
  • IP55防护:在大雨中保护设备,但仅此而已

以及中国行销风格的其他“牵强附会”的优势,

我对有机会追寻梦想并在正确的阶段醒来的机会非常感兴趣。我的许多朋友正是由于此功能而购买了廉价的健身追踪器,并对mi band等产品感到满意。我一直都没有屏幕,但这里却是多合一的。
在工作中,我经常不得不为Android开发简单的应用程序,因此我决定,如果我的本机应用程序没有足够的功能,我会自己编写。

包裹很快就到了,我立即赶紧研究了一个精美的手镯。在经过一个小时的Zeroner应用程序(必须按照说明将其安装在我的Android设备上)上玩了一个小时之后,我意识到该功能相当糟糕且令人遗憾。像其他所有制造商一样,Zeroner专注于计算步数和卡路里,显示漂亮的图表,具有电话搜索功能(我稍后会介绍),可以通知您来电,facebook和whatsapp上的消息到达,以及从任何选定应用程序之一发送通知,这将被视为SMS的应用程序。
手镯的振动非常有争议,他们在论坛上写道,手镯的振动很弱,有人说这很正常。对我来说,它本来可以更强大。手镯对“手表观看”手势有反应,如果您像手表一样看手镯,举手并弯曲肘部,屏幕将自动打开并显示时间或错过的通知。



通常,我会毫不犹豫地决定编写带有通知,振动和同步的应用程序。我会继续前进,这花了4天的时间和几个漫长的夜晚...

对企业


鉴于使用蓝牙不是蓝牙,我决定用傻瓜拦截电话和手镯之间交换的数据。为此,我进入了开发人员选项卡,并打开了“启用HCI蓝牙广播日志”复选框。启用此选项后,Android与任何蓝牙设备的通信的整个转储都将添加到/sdcard/Android/data/btsnoop_hci.log文件中(路径可能会因设备而异,文件名似乎始终相同)。
下载了WireShark之​​后,我开始研究与手镯通信的日志,并看到了类似的内容:



花了将近两个小时,研究了日志,进行了网络成瘾,在Internet上使用了google协议,我才意识到这条路不适合我。

由于我的手机仍然将手镯解释为常规的BLE设备,并在已连接的设备部分中进行了显示,因此我决定使用Android SDK中使用BLE的示例。
克隆了存储库https://github.com/googlesamples/android-BluetoothLeGatt之后,在肚脐上将Android Studio设置为带有源代码,从而构建并启动了该应用程序。 (链接到具有Bluetooth LE的Android SDK的描述

事实如github中的图片所示:


运行扫描后,应用程序看不到设备。事实证明,通过连接到手环的本机应用程序不允许BLE查找设备。一切都由简单地移除Zeroner决定的,可以简单地断开连接,但是完全拆除它更可靠。

因此,Bluetooth LE是一种建立在低功耗设备上的技术,用于新型传感器,标签和许多其他设备。这项技术的基础是通用属性配置文件(GATT),这是一个蓝牙配置文件,可让您交换小的数据“属性”。我不会在很长一段时间内写到这一切的工作方式。在Internet和Internet上,我还必须搜索很多信息以寻找解决方案。

我希望我需要的所有数据都存储在手镯的特征和描述符中,并且我可以毫无问题地接收和记录数据。我错了...

测试BLE应用程序仅向我显示了4个服务:
0000180f-0000-1000-8000-00805f9b34fb
00001800-0000-1000-8000-00805f9b34fb
0000ff20-0000-1000-8000-00805f9b34fb
00001801-0000-1000-8000-00805f9b34fb

中的特征很少,那些读取返回的void或零,并且写无用。但令我感到鼓舞的是,我能够连接并至少获取一些数据。

此外,我决定不可能盲目行动,并决定剖析Zeroner应用程序。在互联网上积累了几个在线APK反编译器后,我将它们提供给zeroner.apk,并在输出中获得了2个zip存档。
第一个是JADX版本,第二个包含apktool的结果。

在源代码中弄乱了我对中文代码的恐惧(尽管在我的工作中,我经常以网站和服务的后端形式遇到他,但是他从未以他的曲折和独创性使我惊奇,但是无论如何,它都很难读懂)
经过大量研究,我终于偶然发现了WristBandDevice.java文件,在路径com.kunekt /蓝牙上。
在这一节课中,设备的所有工作都只是隐藏起来,但又一次伏击正在等待我。
后来发现,在手镯的先前固件中,特性中使用了更多的服务(正如我之前预期的那样),但是后来,开发人员只留下了2个,​​一个用于读取,第二个用于写入。所有命令都在一个数据包中传输。

理解包装的外观并不是一件容易的事,我决定首先从手镯中清楚地确定我想要的东西,以便我可以开始跟踪函数调用。我想在手镯上显示自定义消息。
毫不犹豫地,我进入了com.kunekt / receiver / CallReceiver.java,因为来电显示非常稳定,甚至使用俄语字符,因此我决定这是一个不错的开始,因为我已经在Android中遇到过来电事件,它可能如何工作。

打开文件,我看到了:
一大段中文代码
public void onReceive(Context context, Intent intent) {
        Log.e(this.TAG, "+++ ON RECEIVE +++");
        switch (((TelephonyManager) context.getSystemService("phone")).getCallState()) {
            case C08571.POSITION_OPEN /*0*/:
                if (ZeronerApplication.newAPI) {
                    BackgroundThreadManager.getInstance().addTask(new WriteOneDataTask(context, WristBandDevice.getInstance(context).setPhoneStatue()));
                }
            case BitmapCacheManagementTask.MESSAGE_INIT_DISK_CACHE /*1*/:
                incomingNumber = intent.getStringExtra("incoming_number");
                Contact contact = getContact(context, incomingNumber);
                if (!WristBandDevice.getInstance(context).isConnected() || !ZeronerApplication.phoneAlert) {
                    return;
                }
                if (ZeronerApplication.newAPI) {
                    this.fMdeviceInfo = jsonToFMdeviceInfo(UserConfig.getInstance(context).getDevicesInfo());
                    if (this.fMdeviceInfo.getModel().indexOf("5+") != -1) {
                        if (UserConfig.getInstance(context).getFont_lib() == 1 || UserConfig.getInstance(context).getFont_lib() == 2 || UserConfig.getInstance(context).getSysFont().equalsIgnoreCase("en") || UserConfig.getInstance(context).getSysFont().equalsIgnoreCase("es")) {
                            if (contact.getDisplayName().length() > 11) {
                                WristBandDevice.getInstance(context).writeWristBandFontLibrary(context, 1, contact.getDisplayName().substring(0, 11));
                            } else if (contact.getDisplayName().length() <= 6 || contact.getDisplayName().length() > 11) {
                                WristBandDevice.getInstance(context).writeWristBandFontLibrary(context, 1, contact.getDisplayName());
                            } else {
                                WristBandDevice.getInstance(context).writeWristBandFontLibrary(context, 1, contact.getDisplayName().substring(0, contact.getDisplayName().length()));
                            }
                        } else if (contact.getDisplayName().length() > 11) {
                            WristBandDevice.getInstance(context).writeWristBandPhoneAlertNew(context, contact.getDisplayName().substring(0, 11));
                        } else if (contact.getDisplayName().length() <= 6 || contact.getDisplayName().length() > 11) {
                            WristBandDevice.getInstance(context).writeWristBandPhoneAlertNew(context, contact.getDisplayName());
                        } else {
                            WristBandDevice.getInstance(context).writeWristBandPhoneAlertNew(context, contact.getDisplayName().substring(0, contact.getDisplayName().length()));
                        }
                    } else if (contact.getDisplayName().length() > 11) {
                        WristBandDevice.getInstance(context).writeWristBandPhoneAlertNew(context, contact.getDisplayName().substring(0, 11));
                    } else if (contact.getDisplayName().length() <= 6 || contact.getDisplayName().length() > 11) {
                        WristBandDevice.getInstance(context).writeWristBandPhoneAlertNew(context, contact.getDisplayName());
                    } else {
                        WristBandDevice.getInstance(context).writeWristBandPhoneAlertNew(context, contact.getDisplayName().substring(0, contact.getDisplayName().length()));
                    }
                } else if (contact.getDisplayName().length() > 11) {
                    WristBandDevice.getInstance(context).writeWristBandPhoneAlert(context, contact.getDisplayName().substring(0, 11));
                } else if (contact.getDisplayName().length() <= 6 || contact.getDisplayName().length() > 11) {
                    WristBandDevice.getInstance(context).writeWristBandPhoneAlert(context, contact.getDisplayName());
                } else {
                    WristBandDevice.getInstance(context).writeWristBandPhoneAlert(context, contact.getDisplayName().substring(0, contact.getDisplayName().length()));
                }
            case BitmapCacheManagementTask.MESSAGE_FLUSH /*2*/:
                if (ZeronerApplication.newAPI) {
                    BackgroundThreadManager.getInstance().addTask(new WriteOneDataTask(context, WristBandDevice.getInstance(context).setPhoneStatue()));
                }
            default:
        }
    }



在这里,我们清楚地看到API有2个选项,它们的名称非常合乎逻辑,它们分别是newAPI,第二个分别是oldAPI。在所有这些丰富的条件下,我只对重复的一行感兴趣:
WristBandDevice.getInstance(上下文).writeWristBandPhoneAlertNew(上下文,contact.getDisplayName .....)

这就是我想要的。展望未来,我会说iWown也有i5 +和i6型号,它们具有更大的屏幕,并因此放置了更多字符,为此需要进行所有这些检查。目前尚不清楚他们为什么不写一个类之类的东西,也许是反编译的恶作剧,但是这段代码在很多地方都在重复。
转到此函数的定义,我看到了:

    public void writeWristBandPhoneAlertNew(Context context, String displayName) {
        writeAlertNew(context, displayName, 1);
    }

    public void writeWristBandSmsAlertNew(Context context, String displayName) {
        writeAlertNew(context, displayName, 2);
    }


太好了,它使用相同的功能发送文本,只是带有不同的参数。带有单词New的所有函数只是我们的选择,因为如上所述,我有一个新的API。

高兴地转到writeAlertNew函数的定义,我看到了以下内容:
private void writeAlertNew(Context context, String displayName, int type) {
        ArrayList<Byte> datas = new ArrayList();
        datas.add(Byte.valueOf((byte) type));
        int i = 0;
        while (i < displayName.length()) {
            if (displayName.charAt(i) < '@' || (displayName.charAt(i) < '\u0080' && displayName.charAt(i) > '`')) {
                char e = displayName.charAt(i);
                datas.add(Byte.valueOf((byte) 0));
                for (byte valueOf : PebbleBitmap.fromString(context, String.valueOf(e), 8, 1).data) {
                    datas.add(Byte.valueOf(valueOf));
                }
            } else {
                char c = displayName.charAt(i);
                datas.add(Byte.valueOf((byte) 1));
                for (byte valueOf2 : PebbleBitmap.fromString(context, String.valueOf(c), 16, 1).data) {
                    datas.add(Byte.valueOf(valueOf2));
                }
            }
            i++;
        }
        byte[] data = writeWristBandDataByte(true, form_Header(3, 1), datas);
        for (i = 0; i < data.length; i += 20) {
            byte[] writeData;
            if (i + 20 > data.length) {
                writeData = Arrays.copyOfRange(data, i, data.length);
            } else {
                writeData = Arrays.copyOfRange(data, i, i + 20);
            }
            NewAgreementBackgroundThreadManager.getInstance().addTask(new WriteOneDataTask(context, writeData));
        }
    }


显然,这里使用的几个功能使我与利润分开。
writeWristBandDataByte-形成一个带有手镯消息的包,有趣的是,有一个特殊的函数form_Header(3,1),它形成包的头,手镯通过该函数了解他们想要的东西。3是团队组的编号,1是团队本身
public static byte form_Header(int grp, int cmd) {
        return (byte) (((((byte) grp) & 15) << 4) | (((byte) cmd) & 15));
    }


该功能很简单,无需更改即可复制到我的项目中。接下来是

NewAgreementBackgroundThreadManager.getInstance()。AddTask(新的WriteOneDataTask(上下文,writeData));

事实证明,没有什么异常,应用程序创建一个流,在其中不断检查要发送的数据包队列,如果一个数据包出现在队列中,则该流写入给定的设备特征,如果有多个数据包,则以240毫秒的延迟发送它们。
接下来是最不可理解的:

PebbleBitmap.fromString(上下文,String.valueOf(e),8,1).data)

为何以这种方式调用该类尚不清楚,因为该设备与Pebble无关。打开类源代码,我看到了以下内容:

PebbleBitmap类源
public class PebbleBitmap {
    public static boolean f1285D;
    public final byte[] data;
    public final UnsignedInteger flags;
    public final short height;
    public int index;
    public int offset;
    public final UnsignedInteger rowLengthBytes;
    public final short width;
    public final short f1286x;
    public final short f1287y;

    static {
        f1285D = true;
    }

    private PebbleBitmap(UnsignedInteger _rowLengthBytes, UnsignedInteger _flags, short _x, short _y, short _width, short _height, byte[] _data) {
        this.offset = 0;
        this.index = 0;
        this.rowLengthBytes = _rowLengthBytes;
        this.flags = _flags;
        this.f1286x = _x;
        this.f1287y = _y;
        this.width = _width;
        this.height = _height;
        this.data = _data;
    }

    public static PebbleBitmap fromString(Context context, String text, int w, int l) {
        TextPaint textPaint = new TextPaint();
        textPaint.setAntiAlias(true);
        textPaint.setTextSize(16.5f);
        if (w == 32) {
            textPaint.setTextAlign(Align.CENTER);
        }
        textPaint.setTypeface(ZeronerApplication.unifont);
        StaticLayout sl = new StaticLayout(text, textPaint, w, Alignment.ALIGN_NORMAL, 1.0f, 0.49f, false);
        int h = sl.getHeight();
        if (h > l * 16) {
            h = l * 16;
        }
        Bitmap newBitmap = Bitmap.createBitmap(w, h, Config.ARGB_8888);
        sl.draw(new Canvas(newBitmap));
        return fromAndroidBitmap(newBitmap);
    }

    public static PebbleBitmap fromAndroidBitmap(Bitmap bitmap) {
        int width = bitmap.getWidth();
        int height = bitmap.getHeight();
        int rowLengthBytes = width / 8;
        ByteBuffer data = ByteBuffer.allocate(rowLengthBytes * height);
        data.order(ByteOrder.LITTLE_ENDIAN);
        StringBuffer stringBuffer = new StringBuffer(StatConstants.MTA_COOPERATION_TAG);
        for (int y = 0; y < height; y++) {
            int[] pixels = new int[width];
            bitmap.getPixels(pixels, 0, width * 2, 0, y, width, 1);
            stringBuffer = new StringBuffer(StatConstants.MTA_COOPERATION_TAG);
            for (int x = 0; x < width; x++) {
                if (pixels[x] == 0) {
                    stringBuffer.append(Constants.VIA_RESULT_SUCCESS);
                    if (f1285D) {
                        stringBuffer.append("-");
                    }
                } else {
                    stringBuffer.append(Constants.VIA_TO_TYPE_QQ_GROUP);
                    if (f1285D) {
                        stringBuffer.append("#");
                    }
                }
            }
            for (int k = 0; k < rowLengthBytes * 8; k += 8) {
                ByteBuffer byteBuffer = data;
                byteBuffer.put(Byte.valueOf((byte) new BigInteger(stringBuffer.substring(k, k + 8), 2).intValue()).byteValue());
            }
            if (f1285D) {
                stringBuffer.append("\n");
            }
            Log.i("info", stringBuffer.toString());
        }
        if (f1285D) {
            System.out.println(stringBuffer.toString());
        }
        if (!(bitmap == null || bitmap.isRecycled())) {
            bitmap.recycle();
        }
        System.gc();
        return new PebbleBitmap(UnsignedInteger.fromIntBits(rowLengthBytes), UnsignedInteger.fromIntBits(DfuSettingsConstants.SETTINGS_DEFAULT_MBR_SIZE), (short) 0, (short) 0, (short) width, (short) height, data.array());
    }

    public static PebbleBitmap fromPng(InputStream paramInputStream) throws IOException {
        return fromAndroidBitmap(BitmapFactory.decodeStream(paramInputStream));
    }
}



多反射后,我的结论是fromString创建使用特定字体(其被缝合到该应用程序)的信的照片,然后将像素转换为0或1,这取决于填充,从而,信Ò将看起来像这样:
00011100
01100011
01100011
01100011
00011100

真的不钻研细节,我在使用从谷歌BLE GATT比如你的项目复制的一切。
还有...哦,一个奇迹!手镯振动了!但是没有显示该消息,一个空行和一个来电图标。
原来,一堆尺寸检查不是偶然的,手镯愚蠢地忽略了太长的消息和长度为11个字符的消息,尽管正常显示12个字符。在这些功能上进行了两个小时的跳舞终于产生了结果,我学会了同时显示俄语和英语文本,同时我了解到消息组中有几种操作模式:
  1. 来电。显示听筒,来电者姓名和手镯振动
  2. 留言内容 显示文本和信封图标。振动2次时
  3. 多云。与2相同,但不是信封,而是云图标
  4. 错误。与2相同,只是一个带有感叹号的图标。




教了我的应用程序后,我从不同的应用程序,whatsapp,vk,viber,电报和其他应用程序发送通知给我,我决定现在该教手镯响应来电了,最后使用一个按钮重置来电。

我不会描述这个过程,该帖子原来是肿的,我只是说,回复传入的消息并不难,但是使用按钮不是。

Zeroner截获了来自手镯的所有传入消息,并在一个特殊班级中进行了拦截。经过长时间的调试和测试之后,传入的程序包具有命令组的标题和命令的编号,然后我找出了使用的组,然后在Zeroner代码中找到了说明。

手镯组和团队
// HEADER GROUPS //
DEVICE = 0
CONFIG = 1
DATALOG = 2
MSG = 3
PHONE_MSG = 4

// CONFIG = 1 ///
CMD_ID_CONFIG_GET_AC = 5
CMD_ID_CONFIG_GET_BLE = 3
CMD_ID_CONFIG_GET_HW_OPTION = 9
CMD_ID_CONFIG_GET_NMA = 7
CMD_ID_CONFIG_GET_TIME = 1

CMD_ID_CONFIG_SET_AC = 4
CMD_ID_CONFIG_SET_BLE = 2
CMD_ID_CONFIG_SET_HW_OPTION = 8
CMD_ID_CONFIG_SET_NMA = 6
CMD_ID_CONFIG_SET_TIME = 0

// DATALOG = 2 //
CMD_ID_DATALOG_CLEAR_ALL = 2
CMD_ID_DATALOG_GET_BODY_PARAM = 1
CMD_ID_DATALOG_SET_BODY_PARAM = 0

CMD_ID_DATALOG_GET_CUR_DAY_DATA = 7

CMD_ID_DATALOG_START_GET_DAY_DATA = 3
CMD_ID_DATALOG_START_GET_MINUTE_DATA = 5
CMD_ID_DATALOG_STOP_GET_DAY_DATA = 4
CMD_ID_DATALOG_STOP_GET_MINUTE_DATA = 6

// DEVICE = 0 //
CMD_ID_DEVICE_GET_BATTERY = 1
CMD_ID_DEVICE_GET_INFORMATION = 0
CMD_ID_DEVICE_RESE = 2
CMD_ID_DEVICE_UPDATE = 3

// MSG = 3 //
CMD_ID_MSG_DOWNLOAD = 1
CMD_ID_MSG_MULTI_DOWNLOAD_CONTINUE = 3
CMD_ID_MSG_MULTI_DOWNLOAD_END = 4
CMD_ID_MSG_MULTI_DOWNLOAD_START = 2
CMD_ID_MSG_UPLOAD = 0

// PHONE_MSG = 4 //
CMD_ID_PHONE_ALERT = 1
CMD_ID_PHONE_PRESSKEY = 0



有了这个,我得以实现手镯的成熟功能。我可以接收有关步骤,睡眠的数据。我可以管理设置,设置警报。我设法从数据库中存储数据的类中获得了包的字节标识,我在家里实现了所有它们。

最后


经过一番思考,我决定所有这些不仅对我有用,而且编写了一个新应用程序,其中包含使用手镯所需的所有必要数据和功能,并实现了一个简单的接口,用于将通知从任何应用程序发送到手镯。 从那以后,

WiliX iWown for Geek

过去了很多时间,许多时间在更新到Android 6之后,该应用程序停止运行。它也不能与第二版手镯的固件稳定运行。但我希望有时间进行修订。

源代码发布在GitHub上您可以随意分叉并享受乐趣。接受审核后的所有请求请求将被接受,并且测试后将立即上传到Google Play。

目前,该应用程序可以:
  • 显示来自任何应用程序的通知
  • BT


已实现与Google Fit的连接以保存训练数据,但是由于我没有选择适合Fit的SDK,我通过大量链接和论坛进行了翻阅,但仍然不了解如何从自定义设备中显示Fit数据。现在还不清楚为什么还要使用此功能。
如果有人使用Google Fit,并且知道如何让他使用来自自定义传感器的数据来显示图表,在评论中告诉我或给我写信,那么用户和我将不胜感激!

将手镯连接到睡眠作为Adnroid也是一个想法。实际上,为了监视睡眠,购买了一条手镯。但是,事实证明,iWown仅可以返回睡眠阶段的持续时间。即,已经从加速度计计算出的数据。
像Android一样,“睡眠”需要来自加速度计的裸露数据,并且期望的频率为10秒。

总而言之。我邀请开发人员和所有者使用他们的代码,技巧和任何东西来支持该项目。离开请求请求,在Github上发布。
该应用程序在国外非常受欢迎,外国人经常写信给我,要求我添加/修复/翻译某些内容。

顺便说一下,iWown i5拥有多个具有类似固件的克隆:
Vidonn X5
Harper BFB-301
Excelvan i5

参考文献

Google Play-
GitHub
上的iWown for Geek信息库,在w3bsit3-dns.com 上进行讨论

PS从第5版开始,幕后的android系统中出现了一个附加类别,该类别未出现在锁定屏幕上。
有人可以告诉我如何将我的通知转移到此类别吗?谢谢你

Source: https://habr.com/ru/post/zh-CN391109/


All Articles