"Hack" Ruff开发板(一)

"Hack"是加了引号的,并没有发现什么问题。至于里面JavaScript解释器有没有问题,那就是另外一回事了。

Ruff是一个使用JavaScript进行开发的的嵌入式开发板,官网是 https://ruff.io/zh-cn/。好多天之前就买了一个,一直在玩自带的几个传感器、开关等等。今天突然想了解一下它的内部的系统,就简单的分析了一下。

网络通信协议

首先长按reset键,将开发板reset掉,然后开启Wireshark抓包。

首先是运行rap wifi,提示选择网卡,输入WiFI SSID和密码。发现电脑会向广播地址发送大量的UDP数据包,鉴于开发板还没有进行过任何设置,查了一下,应该是智能家居中常用的SmartConfig,连接步骤是

  • 设备进入初始化状态,开始收听附近的WiFi数据包。
  • 控制端设置WiFi名字和密码后,发送UDP广播包。
  • 设备通过UDP包(长度)获取配置信息,切换网络模式,连接上WiFi,配置完成

对的,数据就在包的长度信息中,因为不知道 WiFi密码,不能把信息放在数据包内,否则无法解密的。更多的可以参考 https://www.zhihu.com/question/21783165

提示设备连接之后,需要运行rap scan,这个步骤需要为开发板设置名字和密码。使用(ip.src==192.168.1.105 && ip.dst==192.168.1.120) || (ip.src==192.168.1.120 && ip.dst==192.168.1.105)的规则过滤。

首先电脑会ping开发板

然后开发板会向电脑发送"ACK"

还有一种prefligh数据包,之后开发板会返回设备信息,包括设备版本、是否设置了密码等等

之后的比较清楚,是我在命令行中设置了密码。我设置的密码是123456,可以看到,指令是system.setTokennewToken字段是密码的32位md5。从这个地方开始数据中出现了signature字段。

还有一个rename指令

剩下的部署代码之类的操作都是大同小异,不再截图说明了。

固件中的JavaScript

在Ruff官网上有固件下载,file了一下是ruffos-1.0.0.bin: u-boot legacy uImage, MIPS OpenWrt Linux-3.18.21, Linux/MIPS, OS Kernel Image (lzma), 1106250 bytes, Thu Apr 28 19:12:46 2016, Load Address: 0x80000000, Entry Point: 0x80000000, Header CRC: 0xCC428240, Data CRC: 0x4817B166,看来是基于OpenWrt的固件二次开发的?

binwalk一下

DECIMAL     HEX         DESCRIPTION
-------------------------------------------------------------------------------------------------------
0           0x0         uImage header, header size: 64 bytes, header CRC: 0xCC428240, created: Thu Apr 28 19:12:46 2016, image size: 1106250 bytes, Data Address: 0x80000000, Entry Point: 0x80000000, data CRC: 0x4817B166, OS: Linux, CPU: MIPS, image type: OS Kernel Image, compression type: lzma, image name: "MIPS OpenWrt Linux-3.18.21"
64          0x40        LZMA compressed data, properties: 0x6D, dictionary size: 8388608 bytes, uncompressed size: 3280540 bytes
1106314     0x10E18A    Squashfs filesystem, little endian, version 4.0, compression: gzip, size: 3241549 bytes,  916 inodes, blocksize: 65536 bytes, created: Thu Apr 28 19:12:43 2016 

直接binwalk -e解包,squashfs的也可以直接解开得到所有文件,就是正常的Linux目录结构,然后看到根目录下的ruff目录,tree了一下,感兴趣的东西应该都在这里。

.
├── ruffd
│   ├── package.json
│   └── src
│       ├── commands
│       │   ├── app.js
│       │   └── system.js
│       ├── config.json
│       ├── libs
│       │   └── md5.js
│       ├── modules
│       │   ├── app.js
│       │   ├── discovery.js
│       │   ├── error.js
│       │   ├── log-reader.js
│       │   ├── packet.js
│       │   ├── server.js
│       │   ├── storage.js
│       │   ├── system.js
│       │   └── test.js
│       └── ruffd.js
└── sdk
    ├── bin
    │   └── ruff
    ├── core-modules.json
    └── ruff_modules
        ├── _console_eval.js
        ├── _file
        │   ├── package.json
        │   └── src
        │       ├── _async.js
        │       ├── _basefile.js
        │       ├── _helper.js
        │       ├── dir.js
        │       ├── file.js
        │       └── index.js
        ├── adc.js
        ├── assert.js
        ├── dgram
        │   ├── package.json
        │   └── src
        │       ├── index.js
        │       ├── socket.js
        │       └── udp_handle.js
        ├── dns
        │   ├── package.json
        │   └── src
        │       ├── dns-packet
        │       │   ├── index.js
        │       │   └── types.js
        │       ├── index.js
        │       └── ip.js
        ├── fs
        │   ├── package.json
        │   └── src
        │       ├── binding.js
        │       ├── fs_util.js
        │       ├── index.js
        │       ├── read_file_context.js
        │       ├── read_stream.js
        │       ├── req_wrap.js
        │       ├── watcher.js
        │       └── write_stream.js
        ├── gpio.js
        ├── http
        │   ├── package.json
        │   └── src
        │       ├── http-client-inners.js
        │       ├── http-client.js
        │       ├── http-common.js
        │       ├── http-events.js
        │       ├── http-freeList.js
        │       ├── http-incoming.js
        │       ├── http-outgoing-end.js
        │       ├── http-outgoing-inner.js
        │       ├── http-outgoing.js
        │       ├── http-parser.js
        │       ├── http-server-response.js
        │       ├── http-server.js
        │       ├── http-statuscodes.js
        │       ├── http-util.js
        │       └── index.js
        ├── i2c.js
        ├── kernel-module.js
        ├── launcher.js
        ├── mmap
        │   └── src
        │       ├── index.js
        │       └── mmap.so
        ├── net
        │   ├── package.json
        │   └── src
        │       ├── index.js
        │       ├── net-events.js
        │       ├── net-normalizeArgs.js
        │       ├── net-util.js
        │       ├── server-cleanup.js
        │       ├── server-property.js
        │       ├── server.js
        │       ├── socket-cleanup.js
        │       ├── socket-connect.js
        │       ├── socket-data-ending.js
        │       ├── socket-data.js
        │       ├── socket-property.js
        │       └── socket.js
        ├── os.js
        ├── process.js
        ├── pwm.js
        ├── querystring.js
        ├── ruff
        │   ├── package.json
        │   └── src
        │       ├── device-parser.js
        │       └── index.js
        ├── ruff-driver.js
        ├── stream
        │   ├── package.json
        │   └── src
        │       ├── _stream_duplex.js
        │       ├── _stream_passthrough.js
        │       ├── _stream_readable.js
        │       ├── _stream_transform.js
        │       ├── _stream_wrap.js
        │       ├── _stream_writable.js
        │       ├── index.js
        │       └── string_decoder.js
        ├── sys-gpio
        │   ├── driver.json
        │   ├── package.json
        │   └── src
        │       ├── gpio.so
        │       └── index.js
        ├── sys-i2c
        │   ├── driver.json
        │   ├── package.json
        │   └── src
        │       ├── i2c.so
        │       └── index.js
        ├── sys-uart
        │   ├── driver.json
        │   ├── package.json
        │   └── src
        │       ├── index.js
        │       └── uart.so
        ├── trait.js
        ├── uart.js
        └── url.js

33 directories, 113 files

大致翻看了一下代码,应用入口应该是/ruff/ruffd/src/ruffd.js,然后进入/ruff/ruffd/src/commands/app.js,在这里面执行了

var handle = uv.spawn(
    uv.exepath(),
    args,
    app.path,
    -1, // stdin
    appLogFileHandle, // stdout
    appLogFileHandle, // stderr
    function (code) {
        app.updateStatus('not-started');
        app.removeListener('started', onstarted);
        console.log('Application exited with code ' + code + '.');
    }
);

目录中的代码非常清晰,包括广播组包发包,日志,存储,监听server等等的实现。

signature检查

function authenticate(command, type) {
    var token = storage.get('auth-token');

    if (!token || hop.call(bypassAuthCommandSet, type)) {
        return;
    }

    var signature = command.signature;

    if (!signature) {
        throw new ExpectedError('Signature expected', SIGNATURE_REQUIRED_STATUS);
    }

    delete command.signature;

    command.token = token;

    var dataArray = Object
        .keys(command)
        .filter(function (key) {
            // This should be redundant, but let's make sure.
            return command[key] !== undefined;
        })
        .sort()
        .map(function (key) {
            return [key, command[key]];
        });

    var calculatedSignature = md5(JSON.stringify(dataArray));

    if (calculatedSignature !== signature) {
        throw new ExpectedError('Wrong signature', WRONG_SIGNATURE_STATUS);
    }
}

大致的原理是开发板本地存储了auth-token,通过storage模块获取,然后将收到的数据按照key排序,加上auth-token,转换为JSON,然后求md5。这样发送端没有auth-token的情况下是无法通过修改数据和通过验证的。

里面还有bypassAuthCommandSet,部分命令是不需要验证的。

var token = storage.get('auth-token');是获取auth-token的方法,去storage模块看下。这个模块会在config.json中读取或写入数据。

Storage.prototype.get = function (key, defaultValue) {
    var value = hop.call(this.data, key) ?
        this.data[key] : undefined;

    if (value !== undefined) {
        return value;
    } else {
        return defaultValue;
    }
};

调用时并没有设置defaultValueconfig.json中也没有这个字段,那么返回的肯定是undefined。虽然前面设置密码的数据包中是有signature的,但是因为本地没有auth-token,所以这个函数执行到下面的语句就结束了。

if (!token || hop.call(bypassAuthCommandSet, type)) {
    return;
}

将里面的authenticate函数拿出来单独运行验证,也确实如此。

本来是打算来找点逻辑问题的,仔细看了一会,觉得没有什么问题。

执行命令

虽然开发板定位于JavaScript开发,不需要接触到系统层面,但是这样也导致很多功能受限制,比如写文件,比如执行命令等。翻了下没有找到相关的文档,想起来之前启动app的那个uv.spawn,应该是可以执行任意命令的。经过几次猜测,终于搞懂了这个函数的参数含义。

uv.spawn(
    
    uv.exepath(), // 可执行文件名称
    
    args, //运行参数,数组,类似["-h", "-t"]
    
    app.path, //可执行文件所在目录
    
    -1, // stdin //stdin的fd,0就是真正的stdin,其他的可以是文件的fd
    
    appLogFileHandle, // stdout的fd,为1的时候输出到屏幕
    
    appLogFileHandle, // stderr的fd
    
    function (code) { //返回值的回调函数
        
        console.log('Application exited with code ' + code + '.');
    
    }

);

之前固件中可以看到,Linux常见的命令的可执行文件都还是有的,链接到busybox的,如果需要运行ls命令,代码就是

var handle = uv.spawn("ls", ["/"], "/bin/", -1, 1, 2, function(code){console.log("Return code: " + code)});

使用UDP写了一个类似ssh的东西

'use strict';
$.ready(function (error) {
    if (error) {
        console.log(error);
        return;
    }
    var dgram = require("dgram");

    var server = dgram.createSocket("udp4");

    server.on("error", function (err) {
        console.log("server error:\n" + err.stack);
        server.close();
    });

    server.on("message", function (msg, rinfo) {
        var m = JSON.parse(msg);
        console.log("server got: " + msg + " from " +
            rinfo.address + ":" + rinfo.port);
        try {
            uv.spawn(
                m.command,
                m.args,
                m.path,
                -1,
                1,
                2,
                function (code) { //返回值的回调函数

                    console.log('Application exited with code ' + code + '.');
                });
        }
        catch (err) {
            console.log(err);
        }


    });

    server.on("listening", function () {
        var address = server.address();
        console.log("server listening " +
            address.address + ":" + address.port);
    });

    server.bind(41234);
});

$.end(function () {
});

Python客户端

# coding=utf-8
import socket  
import json


address = (YOUR_RUFF_IP, 41234)  
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)  

while True:  
    path = raw_input("path: ")
    command = raw_input("command: ")
    args = raw_input("args: ").split()
    s.sendto(json.dumps({"path": path, "args": args, "command": command}), address)  

s.close() 

已有 3 条评论

  1. 为什么不用 Lua 好像 OpenWRT 的界面都是用 Lua 写的把

    1. Senevan

      luci确实是Lua的CGI写的,但是和ruffOS没有什么联系吧- -

  2. 按照你的步骤,都实验成功了,膜拜

添加新评论