Category

编程笔记

unidbg自行支持其他版本的协议

编程笔记

两个so文件

首先从自带的文件中可以看出,需要获取两个so文件

这两个so文件只要下载对应版本的qqapk,使用解压软件打开就可以获得

config.json中的协议信息

这儿使用jadx来反编译qqapk

通过自带的版本信息可以知道,qua是V1_AND_SQ开头的,我们在jadx中搜索

可以看到,前缀匹配上了,但是后缀不一样,所以这儿我们只复制版本信息以及code(版本号后面的四位数字应该就是需要的code)

dtconfig

一样在jadx中搜索dtconfig,可以看到搜出来很多选项,但是我们一个一个看过去

可以看到,在第5个搜索出来的文件里面,看到了一串硬编码的二进制数据

有些常量可以ctrl+左键跳转,将反编译的内容复制出来然后替换掉常量并修改格式,这样就获取到了dtconfig

至此所有需要的内容就都从apk中获取到了

Mirai设置设备信息

编程笔记

本教程对应的是V2的设备信息,若你在 device.json 中看到 "deviceInfoVersion": 2 ,那么你用的就是V2的设备信息

下面是一份默认的"随机"生成的设备信息,可以很明显的看出,这是一个野生的机器人

json
{
  "deviceInfoVersion": 2,
  "data": {
    "display": "MIRAI.524848.001",
    "product": "mirai",
    "device": "mirai",
    "board": "mirai",
    "brand": "mamoe",
    "model": "mirai",
    "bootloader": "unknown",
    "fingerprint": "mamoe/mirai/mirai:10/MIRAI.200122.001/7868854:user/release-keys",
    "bootId": "16FD34B2-AD5D-469D-8535-C6C6859DB3DB",
    "procVersion": "Linux version 3.0.31-7nHaGuio ([email protected])",
    "baseBand": "",
    "version": {
      "incremental": "5891938",
      "release": "10",
      "codename": "REL"
    },
    "simInfo": "T-Mobile",
    "osType": "android",
    "macAddress": "02:00:00:00:00:00",
    "wifiBSSID": "02:00:00:00:00:00",
    "wifiSSID": "<unknown ssid>",
    "imsiMd5": "947794dd7c61f984994e89de00b2ba75",
    "imei": "950450581801049",
    "apn": "wifi"
  }
}

如果不知道对应的参数表示什么,编也编不出来。所以我直接不编,拿个旧手机用旧手机的设备信息

设置adb环境

首先需要adb,下载地址:https://dl.google.com/android/repository/platform-tools-latest-windows.zip

下载之后解压,完成之后应该是如图的样式

然后打开cmd

  1. 在adb的文件夹中(这个文件夹中必须包含adb.exe)按住shift然后右键空白处(不要点文件)
  2. 点击 在此处打开 powershell 窗口
  3. 在窗口中输入cmd

连接设备(手机平板均可)

这里我用旧平板做演示

启用开发者选项和USB调试

连接并开启USB调试

使用adb获取设备信息

此时我们已经成功将设备连接至pc

下面的表格是每个信息的获取指令,使用方式如图

因为设备制造厂商不同,以下内容可能会有不同的,可以使用 adb shell getprop 获取所有属性,然后自行寻找对应的属性

信息

对应指令

display

adb shell getprop ro.build.id

product

adb shell getprop ro.build.product

device

adb shell getprop ro.vendor.product.device

board

adb shell getprop ro.product.board

brand

adb shell getprop ro.product.brand

model

adb shell getprop ro.product.model

fingerprint

adb shell getprop ro.vendor.build.fingerprint

procVersion

adb shell getprop cat /proc/version

除了表格中的内容,还有一些内容可以自己编的

macAddress:按照原有个格式,将里面的数字和字母替换(可用字符 0123456789abcdef)

wifiBSSID:同上,乱写就可以

wifiSSID:写WIFI名字,也可以乱写一个

到此设备信息就编好了

UTC时间转北京时间

编程笔记
kotlin
fun main() {
    val formatPattern = "yyyy-MM-dd HH:mm:ss"
    val utc = "2019-07-10T16:00:00.000Z"
    val time = parse(utc, formatPattern)
    println(time)
}

fun parse(utc: String, formatPattern: String): String {
    val zdt = ZonedDateTime.parse(utc)
    val ldt = zdt.toLocalDateTime()
    val formatter = DateTimeFormatter.ofPattern(formatPattern)
    return formatter.format(ldt.plusHours(8))
}

参考 https://www.jianshu.com/p/62fa934ad2bc

使用Gson序列化和反序列化

编程笔记

引入依赖

Gradle

纯文本
implementation 'com.google.code.gson:gson:2.8.8'

Maven

xml
<dependency>
    <groupId>com.google.code.gson</groupId>
    <artifactId>gson</artifactId>
    <version>2.8.8</version>
</dependency>

示例代码

java
package com.e404.test

import com.google.gson.Gson
import com.google.gson.GsonBuilder
import java.io.FileInputStream
import java.io.FileOutputStream

fun main() {
    // 反序列化
    val stu: Student = FileInputStream("in.json").use {
        it.bufferedReader().use { br ->
            Gson().fromJson(br, Student::class.java)
        }
    }
    println("stu.name: ${stu.name}")
    println("stu.age: ${stu.age}")
    // 序列化
    FileOutputStream("out.json").use {
        it.bufferedWriter().use { bw ->
            // 要写入的json字符串
            // setPrettyPrinting 是生成带缩进的字符串
            val str: String = GsonBuilder().setPrettyPrinting().create().toJson(stu)
            bw.write(str)
        }
    }
}

/**
 * 必须要有无参的构造方法和每个参数的getter和setter
 *
 * (idea下setter可以用alt+insert快速生成)
 *
 * 否则会报错
 */
class Student(var name: String = "", var age: Int = 0)

使用SnakeYaml序列化和反序列化

编程笔记

SnakeYaml官网:http://www.snakeyaml.org/

引入依赖

Gradle

纯文本
implementation 'org.yaml:snakeyaml:1.29'

Maven

xml
<dependency>
    <groupId>org.yaml</groupId>
    <artifactId>snakeyaml</artifactId>
    <version>1.29</version>
</dependency>

使用示例

java
package com.e404.test

import org.yaml.snakeyaml.DumperOptions
import org.yaml.snakeyaml.Yaml
import org.yaml.snakeyaml.nodes.Tag
import java.io.FileInputStream
import java.io.FileOutputStream

fun main() {
    // 反序列化
    val stu: Student = FileInputStream("in.yml").use {
        Yaml().loadAs(it, Student::class.java)
    }
    println("stu.name: ${stu.name}")
    println("stu.age: ${stu.age}")
    // 序列化
    FileOutputStream("out.yml").use {
        it.bufferedWriter().use { bw ->
            // 字符串 (使用dumpAs以移除bean标签)
            val str: String = Yaml().dumpAs(stu, Tag.MAP, DumperOptions.FlowStyle.BLOCK)
            bw.write(str)
        }
    }
}

/**
 * 必须要有无参的构造方法和每个参数的getter和setter
 *
 * (idea下setter可以用alt+insert快速生成)
 *
 * 否则会报错
 */
class Student(var name: String = "", var age: Int = 0)

使用zxing生成&识别二维码

编程笔记

GitHub

zxing/zxing

引入依赖

Gradle

纯文本
implementation 'com.google.zxing:core:3.4.1'

Maven

xml
<dependency>
    <groupId>com.google.zxing</groupId>
    <artifactId>core</artifactId>
    <version>3.4.1</version>
</dependency>

示例代码

kotlin
import com.google.zxing.*
import com.google.zxing.common.HybridBinarizer
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
import java.awt.geom.AffineTransform
import java.awt.image.BufferedImage


object QrUtil {
    private val mfw = MultiFormatWriter()
    private val encodeHints = object : HashMap<EncodeHintType, Any>() {
        init {
            put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H)
            put(EncodeHintType.CHARACTER_SET, Charsets.UTF_8)
            put(EncodeHintType.MARGIN, 1)
        }
    }
    
    private val decodeHints = object : HashMap<DecodeHintType, Any>() {
        init {
            put(DecodeHintType.CHARACTER_SET, Charsets.UTF_8)
            //优化精度
            put(DecodeHintType.TRY_HARDER, true)
            //复杂模式,开启PURE_BARCODE模式
            put(DecodeHintType.PURE_BARCODE, true)
        }
    }

    /**
     * 生成二维码
     *
     * 若宽/高小于生成的二维码的最小宽度则会忽略此宽/高度, 否则会生成此宽/高度的二维码图片
     *
     * @param content 正文
     * @param width 目标图片宽度
     * @param height 目标图片高度
     * @param color1 二维码颜色
     * @param color2 二维码背景
     * @return 二维码图片
     */
    @Throws(WriterException::class)
    fun getQrImage(
        content: String,
        width: Int,
        height: Int,
        color1: Int,
        color2: Int,
    ): BufferedImage {
        val bitMatrix = mfw.encode(content, BarcodeFormat.QR_CODE, width, height, encodeHints)
        val w = bitMatrix.width
        val h = bitMatrix.height
        val image = BufferedImage(w, h, 1)
        for (x in 0 until w) for (y in 0 until h) image.setRGB(x, y, if (bitMatrix[x, y]) color1 else color2)
        return image
    }

    /**
     * 识别二维码
     *
     * @param image 要识别的图片
     * @return 识别的结果
     */
    @Throws(NotFoundException::class)
    fun decode(image: BufferedImage): String {
        val source = BufferedImageLuminanceSource(image)
        val bitmap = BinaryBitmap(HybridBinarizer(source))
        return MultiFormatReader().decode(bitmap, decodeHints).text
    }

    class BufferedImageLuminanceSource(
        image: BufferedImage,
        left: Int = 0,
        top: Int = 0,
        width: Int = image.width,
        height: Int = image.height,
    ) :
        LuminanceSource(width, height) {
        private val image: BufferedImage
        private val left: Int
        private val top: Int
        override fun getRow(y: Int, rowBytes: ByteArray): ByteArray {
            var row = rowBytes
            require(!(y < 0 || y >= height)) { "Requested row is outside the image: $y" }
            if (row.size < width) row = ByteArray(width)
            image.raster.getDataElements(left, top + y, width, 1, row)
            return row
        }

        override fun getMatrix(): ByteArray {
            val width = width
            val height = height
            val area = width * height
            val matrix = ByteArray(area)
            image.raster.getDataElements(left, top, width, height, matrix)
            return matrix
        }

        override fun isCropSupported() = true
        override fun crop(left: Int, top: Int, width: Int, height: Int) =
            BufferedImageLuminanceSource(image, this.left + left, this.top + top, width, height)

        override fun isRotateSupported() = true

        override fun rotateCounterClockwise(): LuminanceSource {
            val sourceWidth = image.width
            val sourceHeight = image.height
            val transform = AffineTransform(0.0, -1.0, 1.0, 0.0, 0.0, sourceWidth.toDouble())
            val rotatedImage = BufferedImage(sourceHeight, sourceWidth, BufferedImage.TYPE_BYTE_GRAY)
            val g = rotatedImage.createGraphics()
            g.drawImage(image, transform, null)
            g.dispose()
            val width = width
            return BufferedImageLuminanceSource(rotatedImage, top, sourceWidth - (left + width), height, width)
        }

        init {
            val sourceWidth = image.width
            val sourceHeight = image.height
            require(!(left + width > sourceWidth || top + height > sourceHeight)) { "Crop rectangle does not fit within image data." }
            for (y in top until top + height) {
                for (x in left until left + width) {
                    if (image.getRGB(x, y) and -0x1000000 == 0) {
                        image.setRGB(x, y, -0x1) // = white
                    }
                }
            }
            this.image = BufferedImage(sourceWidth, sourceHeight, BufferedImage.TYPE_BYTE_GRAY)
            this.image.graphics.drawImage(image, 0, 0, null)
            this.left = left
            this.top = top
        }
    }
}

Java中获取到的颜色是负数

编程笔记

这几天写图片处理相关的代码的时候遇到个问题,BufferedImage.getRGB()的返回值有时会是负数

百度一番之后找到了原因:

颜色的取值范围0x00000000-0xffffffff,会超过int类型的最大值

为了能让其正常取值,超过int最大值的会变成 他本身 - 1 - 0xffffffff

这里我写了一段转化的代码供参考

kotlin
/**
 * 处理颜色补码(- -> +)
 * 将可能为负的颜色转成正数的Long
 *
 * @param color 可能为负数的颜色
 * @return 经过转换的颜色
 */
fun fromRgb(color: Int): Long {
    return (if (color < 0) 0xffffffff + color + 1 else color).toLong()
}

/**
 * 处理颜色补码(+ -> -)
 * 将长度超过Int的颜色转成可能为负的Int颜色
 *
 * @param color 正数颜色, Long
 * @return 经过转换的颜色
 */
fun toRgb(color: Long): Int {
    return (if (color > Int.MAX_VALUE) (color - 1 - 0xffffffff) else color).toInt()
}

js实现无限下划加载

编程笔记

首先新建一个html模板

xml
<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>test</title>
</head>
<body>
    <div id="main"></div>
</body>
</html>

实现加载内容的js,这里举一个简单的例子

javascript
function load(title) {
  // div
  let div = document.createElement("div");
  div.setAttribute("class", "test")
  // h2
  let h2 = document.createElement("h2");
  h2.appendChild(document.createTextNode("标题: " + title));
  div.appendChild(h2);
  // 添加
  document.getElementById("main").appendChild(div);
}

添加一个监听器,在网页浏览快要结束时添加新内容

javascript
// 设置初始时间
let time = new Date().getTime();
// 监听器
function eventHandle(e) {
  // 这里设置一个时间,原因是每次浏览器滚动条位置变动都会出发event
  // 加载较慢时可能会出现滚动条到底了,第一次触发的内容还未加载出来
  // 这里设置每200ms可以触发一次
  let t = new Date().getTime();
  if (t - time > 200) {
    time = t;
    // 浏览器上方内容的高度
    let viewportHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight || 0;
    // 页面总高度
    let pageHeight = Math.max(document.body.scrollHeight, document.body.offsetHeight) - viewportHeight;
    // 滚动条高度
    let scrollHeight = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0;
    // 当页面浏览进度超过90%时使用鼠标滚轮触发
    if (scrollHeight / pageHeight > 0.9) load("awa");
  }
}
// 启用监听器 scroll是监听器名字
document.addEventListener('scroll', eventHandle);
// 删除监听器
document.removeEventListener('scroll', eventHandle);

这样的话一个简单的无限下划就完成了

最后贴一个demo

javascript
<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>test</title>
    <style>
        #main {
            height: 1200px;
        }

        .test {
            height: 200px;
        }
    </style>
</head>
<body>
    <div id="main"></div>
    <script>
        function load(title) {
            // div
            let div = document.createElement("div");
            div.setAttribute("class", "test")
            // h2
            let h2 = document.createElement("h2");
            h2.appendChild(document.createTextNode("标题: " + title));
            div.appendChild(h2);
            // 添加
            document.getElementById("main").appendChild(div);
        }
        load("awa");
        // 设置初始时间
        let time = new Date().getTime();
        // 监听器
        let count = 1;
        function eventHandle(e) {
            // 这里设置一个时间,原因是每次浏览器滚动条位置变动都会出发event
            // 加载较慢时可能会出现滚动条到底了,第一次触发的内容还未加载出来
            // 这里设置每200ms可以触发一次
            let t = new Date().getTime();
            if (t - time > 200) {
                time = t;
                // 浏览器上方内容的高度
                let viewportHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight || 0;
                // 页面总高度
                let pageHeight = Math.max(document.body.scrollHeight, document.body.offsetHeight) - viewportHeight;
                // 滚动条高度
                let scrollHeight = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0;
                // 当页面浏览进度超过90%时使用鼠标滚轮触发
                if (scrollHeight / pageHeight > 0.9) {
                    for (let i = 0; i < 5; i++) {
                        load("第" + count + "次自动加载");
                    }
                    count++;
                }
            }
        }
        // 启用监听器 scroll是监听器名字
        document.addEventListener('scroll', eventHandle);
        // 删除监听器
        // document.removeEventListener('scroll', eventHandle);
    </script>
</body>
</html>