Merge remote-tracking branch 'origin/master'

This commit is contained in:
Him188 2019-10-08 16:38:44 +08:00
commit db8315689e
18 changed files with 651 additions and 679 deletions

View File

@ -8,7 +8,7 @@
项目处于开发阶段,学生无法每日大量更新。
项目还有很多未完善的地方, 欢迎任何的代码贡献, 或是 issue.
部分协议来自网络上开源项目
一切开发旨在学习,请勿用于非法用途
**一切开发旨在学习,请勿用于非法用途**
## 抢先体验
核心框架结构已经开发完毕,一些核心功能也测试完成。

View File

@ -17,6 +17,8 @@ kotlin {
}
}
jvmMain {
apply plugin: 'java'
dependencies {
implementation rootProject.ext.kotlinJvm
implementation rootProject.ext.reflect
@ -28,6 +30,7 @@ kotlin {
implementation 'org.jsoup:jsoup:1.12.1'
implementation 'org.ini4j:ini4j:0.5.2'
implementation project(":mirai-protocol-timpc")
}
}
jvmTest {

View File

@ -1,174 +0,0 @@
package net.mamoe.mirai.message;
/**
* @author LamGC
* @author Him188moe
*/
public enum FaceID {
unknown(0xff),
Face_jingya(0),
Face_piezui(1),
Face_se(2),
Face_fadai(3),
Face_deyi(4),
Face_liulei(5),
Face_haixiu(6),
Face_bizui(7),
Face_shui(8),
Face_daku(9),
Face_ganga(10),
Face_fanu(11),
Face_tiaopi(12),
Face_ciya(13),
Face_weixiao(14),
Face_nanguo(15),
Face_ku(16),
Face_zhuakuang(18),
Face_tu(19),
Face_touxiao(20),
Face_keai(21),
Face_baiyan(22),
Face_aoman(23),
Face_ji_e(24),
Face_kun(25),
Face_jingkong(26),
Face_liuhan(27),
Face_hanxiao(28),
Face_dabing(29),
Face_fendou(30),
Face_zhouma(31),
Face_yiwen(32),
Face_yun(34),
Face_zhemo(35),
Face_shuai(36),
Face_kulou(37),
Face_qiaoda(38),
Face_zaijian(39),
Face_fadou(41),
Face_aiqing(42),
Face_tiaotiao(43),
Face_zhutou(46),
Face_yongbao(49),
Face_dan_gao(53),
Face_shandian(54),
Face_zhadan(55),
Face_dao(56),
Face_zuqiu(57),
Face_bianbian(59),
Face_kafei(60),
Face_fan(61),
Face_meigui(63),
Face_diaoxie(64),
Face_aixin(66),
Face_xinsui(67),
Face_liwu(69),
Face_taiyang(74),
Face_yueliang(75),
Face_qiang(76),
Face_ruo(77),
Face_woshou(78),
Face_shengli(79),
Face_feiwen(85),
Face_naohuo(86),
Face_xigua(89),
Face_lenghan(96),
Face_cahan(97),
Face_koubi(98),
Face_guzhang(99),
Face_qiudale(100),
Face_huaixiao(101),
Face_zuohengheng(102),
Face_youhengheng(103),
Face_haqian(104),
Face_bishi(105),
Face_weiqu(106),
Face_kuaikule(107),
Face_yinxian(108),
Face_qinqin(109),
Face_xia(110),
Face_kelian(111),
Face_caidao(112),
Face_pijiu(113),
Face_lanqiu(114),
Face_pingpang(115),
Face_shiai(116),
Face_piaochong(117),
Face_baoquan(118),
Face_gouyin(119),
Face_quantou(120),
Face_chajin(121),
Face_aini(122),
Face_bu(123),
Face_hao(124),
Face_zhuanquan(125),
Face_ketou(126),
Face_huitou(127),
Face_tiaosheng(128),
Face_huishou(129),
Face_jidong(130),
Face_jiewu(131),
Face_xianwen(132),
Face_zuotaiji(133),
Face_youtaiji(134),
Face_shuangxi(136),
Face_bianpao(137),
Face_denglong(138),
Face_facai(139),
Face_K_ge(140),
Face_gouwu(141),
Face_youjian(142),
Face_shuai_qi(143),
Face_hecai(144),
Face_qidao(145),
Face_baojin(146),
Face_bangbangtang(147),
Face_he_nai(148),
Face_xiamian(149),
Face_xiangjiao(150),
Face_feiji(151),
Face_kaiche(152),
Face_gaotiezuochetou(153),
Face_chexiang(154),
Face_gaotieyouchetou(155),
Face_duoyun(156),
Face_xiayu(157),
Face_chaopiao(158),
Face_xiongmao(159),
Face_dengpao(160),
Face_fengche(161),
Face_naozhong(162),
Face_dasan(163),
Face_caiqiu(164),
Face_zuanjie(165),
Face_shafa(166),
Face_zhijin(167),
Face_yao(168),
Face_shouqiang(169),
Face_qingwa(170),
// TODO: 2019/9/1 添加更多表情
;
private final int id;
FaceID(int id) {
this.id = id;
}
public int getId() {
return id;
}
public static FaceID ofId(int id) {
for (FaceID value : FaceID.values()) {
if (value.id == id) {
return value;
}
}
return FaceID.unknown;
}
}

View File

@ -0,0 +1,167 @@
package net.mamoe.mirai.message
/**
* @author LamGC
* @author Him188moe
*/
@Suppress("EnumEntryName", "unused", "SpellCheckingInspection")
enum class FaceID constructor(val id: Int) {
unknown(0xff),
// TODO: 2019/9/1 添加更多表情
jingya(0),
piezui(1),
se(2),
fadai(3),
deyi(4),
liulei(5),
haixiu(6),
bizui(7),
shui(8),
daku(9),
ganga(10),
fanu(11),
tiaopi(12),
ciya(13),
weixiao(14),
nanguo(15),
ku(16),
zhuakuang(18),
tu(19),
touxiao(20),
keai(21),
baiyan(22),
aoman(23),
ji_e(24),
kun(25),
jingkong(26),
liuhan(27),
hanxiao(28),
dabing(29),
fendou(30),
zhouma(31),
yiwen(32),
yun(34),
zhemo(35),
shuai(36),
kulou(37),
qiaoda(38),
zaijian(39),
fadou(41),
aiqing(42),
tiaotiao(43),
zhutou(46),
yongbao(49),
dan_gao(53),
shandian(54),
zhadan(55),
dao(56),
zuqiu(57),
bianbian(59),
kafei(60),
fan(61),
meigui(63),
diaoxie(64),
aixin(66),
xinsui(67),
liwu(69),
taiyang(74),
yueliang(75),
qiang(76),
ruo(77),
woshou(78),
shengli(79),
feiwen(85),
naohuo(86),
xigua(89),
lenghan(96),
cahan(97),
koubi(98),
guzhang(99),
qiudale(100),
huaixiao(101),
zuohengheng(102),
youhengheng(103),
haqian(104),
bishi(105),
weiqu(106),
kuaikule(107),
yinxian(108),
qinqin(109),
xia(110),
kelian(111),
caidao(112),
pijiu(113),
lanqiu(114),
pingpang(115),
shiai(116),
piaochong(117),
baoquan(118),
gouyin(119),
quantou(120),
chajin(121),
aini(122),
bu(123),
hao(124),
zhuanquan(125),
ketou(126),
huitou(127),
tiaosheng(128),
huishou(129),
jidong(130),
jiewu(131),
xianwen(132),
zuotaiji(133),
youtaiji(134),
shuangxi(136),
bianpao(137),
denglong(138),
facai(139),
K_ge(140),
gouwu(141),
youjian(142),
shuai_qi(143),
hecai(144),
qidao(145),
baojin(146),
bangbangtang(147),
he_nai(148),
xiamian(149),
xiangjiao(150),
feiji(151),
kaiche(152),
gaotiezuochetou(153),
chexiang(154),
gaotieyouchetou(155),
duoyun(156),
xiayu(157),
chaopiao(158),
xiongmao(159),
dengpao(160),
fengche(161),
naozhong(162),
dasan(163),
caiqiu(164),
zuanjie(165),
shafa(166),
zhijin(167),
yao(168),
shouqiang(169),
qingwa(170);
override fun toString(): String {
return "$name($id)"
}
companion object {
fun ofId(id: Int): FaceID {
for (value in values()) {
if (value.id == id) {
return value
}
}
return unknown
}
}
}

View File

@ -20,7 +20,11 @@ class Face(val id: FaceID) : Message() {
override val type: MessageKey = Key
override fun toStringImpl(): String {
return String.format("[face%d]", id.id)
return "[face${id.id}]"
}
override fun toObjectString(): String {
return "Face[$id]"
}
override fun toByteArray(): ByteArray = dataEncode { section ->
@ -48,7 +52,7 @@ class Face(val id: FaceID) : Message() {
override operator fun contains(sub: String): Boolean = false
internal object PacketHelper {
object PacketHelper {
fun ofByteArray(data: ByteArray): Face = dataDecode(data) {
//00 01 AF 0B 00 08 00 01 00 04 52 CC F5 D0 FF 00 02 14 F0
//00 01 0C 0B 00 08 00 01 00 04 52 CC F5 D0 FF 00 02 14 4D

View File

@ -22,7 +22,11 @@ open class Image(val imageId: String) : Message() {
override val type: MessageKey = Key
override fun toStringImpl(): String {
return imageId
return "[$imageId]"
}
override fun toObjectString(): String {
return "Image[$imageId]"
}
override fun toByteArray(): ByteArray = dataEncode { section ->
@ -55,7 +59,7 @@ open class Image(val imageId: String) : Message() {
override operator fun contains(sub: String): Boolean = false //No string can be contained in a image
internal object PacketHelper {
object PacketHelper {
@JvmStatic
fun ofByteArray0x06(data: ByteArray): Image = dataDecode(data) {
it.skip(1)

View File

@ -2,7 +2,12 @@ package net.mamoe.mirai.message.defaults
import net.mamoe.mirai.message.Message
import net.mamoe.mirai.message.MessageKey
import net.mamoe.mirai.network.protocol.tim.packet.readLVByteArray
import net.mamoe.mirai.network.protocol.tim.packet.readNBytes
import net.mamoe.mirai.utils.dataDecode
import net.mamoe.mirai.utils.dataEncode
import net.mamoe.mirai.utils.toUHexString
import java.io.DataInputStream
import java.util.*
import java.util.stream.Collectors
import java.util.stream.Stream
@ -95,4 +100,69 @@ class MessageChain : Message {
operator fun component1(): Message = this.list[0]
operator fun component2(): Message = this.list[1]
operator fun component3(): Message = this.list[2]
object PacketHelper {
@JvmStatic
fun ofByteArray(byteArray: ByteArray): MessageChain = dataDecode(byteArray) {
it.readMessageChain()
}
}
}
fun DataInputStream.readMessage(): Message? {
val messageType = this.readByte().toInt()
val sectionLength = this.readShort().toLong()//sectionLength: short
val sectionData = this.readNBytes(sectionLength)
return when (messageType) {
0x01 -> PlainText.PacketHelper.ofByteArray(sectionData)
0x02 -> Face.PacketHelper.ofByteArray(sectionData)
0x03 -> Image.PacketHelper.ofByteArray0x03(sectionData)
0x06 -> Image.PacketHelper.ofByteArray0x06(sectionData)
0x19 -> {//长文本
val value = readLVByteArray()
//todo 未知压缩算法
PlainText(String(value))
// PlainText(String(GZip.uncompress( value)))
}
0x14 -> {//长文本
val value = readLVByteArray()
println(value.size)
println(value.toUHexString())
//todo 未知压缩算法
this.skip(7)//几个TLV
return PlainText(String(value))
}
0x0E -> {
//null
null
}
else -> {
println("未知的messageType=0x${messageType.toByte().toUHexString()}")
println("后文=${this.readAllBytes().toUHexString()}")
null
}
}
}
fun DataInputStream.readMessageChain(): MessageChain {
val chain = MessageChain()
var got: Message? = null
do {
if (got != null) {
chain.concat(got)
}
if (this.available() == 0) {
return chain
}
got = this.readMessage()
} while (got != null)
return chain
}

View File

@ -38,7 +38,7 @@ class PlainText(private val text: String) : Message() {
override operator fun contains(sub: String): Boolean = this.toString().contains(sub)
internal object PacketHelper {
object PacketHelper {
@JvmStatic
fun ofByteArray(data: ByteArray): PlainText = dataDecode(data) {
it.skip(1)

View File

@ -39,6 +39,7 @@ object TIMProtocol {
*/
const val fixVer2 = "02 00 00 00 01 01 01 00 00 68 20"
// 02 38 03 00 CD 48 68 3E 03 3F A2 02 00 00 00
// 02 00 00 00 01 2E 01 00 00 69 35
/**
* 0825data1
*/
@ -105,6 +106,7 @@ object TIMProtocol {
* length=15
*/
const val messageConst1 = "00 00 0C E5 BE AE E8 BD AF E9 9B 85 E9 BB 91"
// TIM最新 22 00 0C E5 BE AE E8 BD AF E9 9B 85 E9 BB 91
private val hexToByteArrayCacheMap: MutableMap<Int, ByteArray> = mutableMapOf()

View File

@ -2,11 +2,8 @@
package net.mamoe.mirai.network.protocol.tim.packet
import net.mamoe.mirai.message.Message
import net.mamoe.mirai.message.defaults.Face
import net.mamoe.mirai.message.defaults.Image
import net.mamoe.mirai.message.defaults.MessageChain
import net.mamoe.mirai.message.defaults.PlainText
import net.mamoe.mirai.message.defaults.readMessageChain
import net.mamoe.mirai.network.protocol.tim.TIMProtocol
import net.mamoe.mirai.utils.dataDecode
import net.mamoe.mirai.utils.hexToBytes
@ -49,7 +46,7 @@ abstract class ServerEventPacket(input: DataInputStream, val packetId: ByteArray
@PacketId("00 17")
class Encrypted(input: DataInputStream, private val packetId: ByteArray) : ServerPacket(input) {
fun decrypt(sessionKey: ByteArray): Raw = Raw(decryptBy(sessionKey), packetId).setId(this.idHex)
fun decrypt(sessionKey: ByteArray): Raw = Raw(this.decryptBy(sessionKey), packetId).setId(this.idHex)
}
}
@ -135,7 +132,7 @@ class ServerGroupMessageEventPacket(input: DataInputStream, packetId: ByteArray,
this.input.goto(108)
this.input.readLVByteArray()
input.skip(2)//2个0x00
message = input.readSections()
message = input.readMessageChain()
val map = input.readTLVMap(true)
if (map.containsKey(18)) {
@ -262,7 +259,7 @@ class ServerFriendMessageEventPacket(input: DataInputStream, packetId: ByteArray
input.goto(93 + l1)
input.readLVByteArray()//font
input.skip(2)//2个0x00
message = input.readSections()
message = input.readMessageChain()
val map: Map<Int, ByteArray> = input.readTLVMap(true).withDefault { byteArrayOf() }
println(map.getValue(18))
@ -278,64 +275,6 @@ class ServerFriendMessageEventPacket(input: DataInputStream, packetId: ByteArray
}
}
private fun DataInputStream.readSection(): Message? {
val messageType = this.readByte().toInt()
val sectionLength = this.readShort().toLong()//sectionLength: short
val sectionData = this.readNBytes(sectionLength)
return when (messageType) {
0x01 -> PlainText.PacketHelper.ofByteArray(sectionData)
0x02 -> Face.PacketHelper.ofByteArray(sectionData)
0x03 -> Image.PacketHelper.ofByteArray0x03(sectionData)
0x06 -> Image.PacketHelper.ofByteArray0x06(sectionData)
0x19 -> {//长文本
val value = readLVByteArray()
//todo 未知压缩算法
PlainText(String(value))
// PlainText(String(GZip.uncompress( value)))
}
0x14 -> {//长文本
val value = readLVByteArray()
println(value.size)
println(value.toUHexString())
//todo 未知压缩算法
this.skip(7)//几个TLV
return PlainText(String(value))
}
0x0E -> {
//null
null
}
else -> {
println("未知的messageType=0x${messageType.toByte().toUHexString()}")
println("后文=${this.readAllBytes().toUHexString()}")
null
}
}
}
private fun DataInputStream.readSections(): MessageChain {
val chain = MessageChain()
var got: Message? = null
do {
if (got != null) {
chain.concat(got)
}
if (this.available() == 0) {
return chain
}
got = this.readSection()
} while (got != null)
return chain
}
/*
牛逼 (10404

View File

@ -34,11 +34,22 @@ class ClientSendFriendMessagePacket(
writeRandom(2)
writeTime()
writeHex("00 00" +
"00 00 00 00 01 00 00 00 01 4D 53 47 00 00 00 00 00")
//01 1D 00 00 00 00 01 00 00 00 01 4D 53 47 00 00 00 00 00
"00 00 00 00")
//消息过多要分包发送
//如果只有一个
writeByte(0x01)
writeByte(0)//第几个包
writeByte(0)
//如果大于一个,
//writeByte(0x02)//数量
//writeByte(0)//第几个包
//writeByte(0x91)//why?
writeHex("00 01 4D 53 47 00 00 00 00 00")
writeTime()
writeRandom(4)
writeHex("00 00 00 00 09 00 86")
writeHex("00 00 00 00 09 00 86")//TIM最新 0C 00 86
writeHex(TIMProtocol.messageConst1)//... 85 E9 BB 91
writeZero(2)
@ -56,9 +67,5 @@ class ClientSendFriendMessagePacket(
}
}
fun main() {
}
@PacketId("00 CD")
class ServerSendFriendMessageResponsePacket(input: DataInputStream) : ServerPacket(input)

View File

@ -37,18 +37,19 @@ class ClientTryGetImageIDPacket(
writeZero(2)
writeHex("5E")
writeHex("5B")//原5E
writeHex("08")
writeHex("01 12 03 98 01 01 10 01")
writeHex("1A")
writeHex("5A")
writeHex("57")//原5A
writeHex("08")
writeUVarInt(groupNumberOrQQNumber)
writeUVarInt(groupNumberOrQQNumber)//FB D2 D8 94
writeByte(0x02)
writeHex("10")
writeUVarInt(botNumber)
writeUVarInt(botNumber)//A2 FF 8C F0
writeHex("18 00")
@ -57,10 +58,13 @@ class ClientTryGetImageIDPacket(
write(md5(byteArray))
writeHex("28")
writeUVarInt(byteArray.size.toUInt())
writeUVarInt(byteArray.size.toUInt())//E2 0D
writeHex("32")
writeHex("1A")
//28 00 5A 00 53 00 41 00 58 00 40 00 57 00 4B 00 52 00 4A 00 5A 00 31 00 7E 00 38 01 48 01 50 38 58 34 60 04 6A 05 32 36 39 33 33 70 00 78 03 80 01 00
writeHex("37 00 4D 00 32 00 25 00 4C 00 31 00 56 00 32 00 7B 00 39 00 30 00 29 00 52 00")
writeHex("38 01")

View File

@ -84,6 +84,11 @@ fun <R> dataDecode(byteArray: ByteArray, t: (DataInputStream) -> R): R = byteArr
fun <R> ByteArray.decode(t: (DataInputStream) -> R): R = this.dataInputStream().let(t)
fun ByteArray.decryptBy(key: ByteArray): ByteArray = TEA.decrypt(this, key)
fun ByteArray.decryptBy(key: String): ByteArray = TEA.decrypt(this, key)
fun DataInputStream.skip(n: Number) {
this.skip(n.toLong())
}

View File

@ -69,7 +69,6 @@ fun DataOutputStream.writeVarInt(signedInt: Int) {
this.writeUVarInt(encodeZigZag32(signedInt))
}
@Throws(IOException::class)
fun DataOutputStream.writeUVarInt(uint: UInt) {
return writeUVarInt(uint.toLong())

View File

@ -3,11 +3,12 @@ apply plugin: "java"
dependencies {
implementation project(':mirai-core')
compile 'com.google.protobuf:protobuf-java:3.5.0'
compile files('./lib/jpcap.jar')
compile 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0-M2'
compile 'org.jetbrains.kotlin:kotlin-stdlib:1.3.50'
compile rootProject.ext.coroutineCommon
compile rootProject.ext.kotlinJvm
compile group: 'com.google.protobuf', name: 'protobuf-java', version: rootProject.ext.protobuf_version
}
tasks.withType(JavaCompile) {

View File

@ -1,373 +0,0 @@
import kotlin.ranges.IntRange;
import net.mamoe.mirai.network.protocol.tim.TIMProtocol;
import net.mamoe.mirai.network.protocol.tim.packet.ClientPacketKt;
import net.mamoe.mirai.utils.UtilsKt;
import java.awt.*;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.Scanner;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiConsumer;
/**
* This could be used to check packet encoding..
* but better to run under UNIX
*
* @author NaturalHG
*/
public class HexComparator {
/**
* a string result
*/
private static final String RED = "\033[31m";
private static final String GREEN = "\033[33m";
private static final String UNKNOWN = "\033[30m";
private static final String BLUE = "\033[34m";
public static final List<HexReader> consts = new LinkedList<>() {{
add(new HexReader("90 5E 39 DF 00 02 76 E4 B8 DD 00"));
}};
private static class ConstMatcher {
private static final List<Field> CONST_FIELDS = new LinkedList<>() {{
List.of(TIMProtocol.class).forEach(aClass -> Arrays.stream(aClass.getDeclaredFields()).peek(this::add).forEach(Field::trySetAccessible));
List.of(TestConsts.class).forEach(aClass -> Arrays.stream(aClass.getDeclaredFields()).peek(this::add).forEach(Field::trySetAccessible));
}};
@SuppressWarnings({"unused", "NonAsciiCharacters"})
private static class TestConsts {
private static final String NIU_BI = UtilsKt.toUHexString("牛逼".getBytes(), " ");
private static final String _1994701021 = ClientPacketKt.toUHexString(1994701021, " ");
private static final String _1040400290 = ClientPacketKt.toUHexString(1040400290, " ");
private static final String _580266363 = ClientPacketKt.toUHexString(580266363, " ");
private static final String _1040400290_ = "3E 03 3F A2";
private static final String _1994701021_ = "76 E4 B8 DD";
private static final String _jiahua_ = "B1 89 BE 09";
private static final String _Him188moe_ = UtilsKt.toUHexString("Him188moe".getBytes(), " ");
private static final String 发图片 = UtilsKt.toUHexString("发图片".getBytes(), " ");
private static final String = UtilsKt.toUHexString("发图片".getBytes(), " ");
private static final String SINGLE_PLAIN_MESSAGE_HEAD = "00 00 01 00 09 01";
private static final String MESSAGE_TAIL_10404 = "0E 00 07 01 00 04 00 00 00 09 19 00 18 01 00 15 AA 02 12 9A 01 0F 80 01 01 C8 01 00 F0 01 00 F8 01 00 90 02 00".replace(" ", " ");
//private static final String MESSAGE_TAIL2_10404 ="".replace(" ", " ");
}
private final List<Match> matches = new LinkedList<>();
private ConstMatcher(String hex) {
CONST_FIELDS.forEach(field -> {
for (IntRange match : match(hex, field)) {
matches.add(new Match(match, field.getName()));
}
});
}
private String getMatchedConstName(int hexNumber) {
for (Match match : this.matches) {
if (match.range.contains(hexNumber)) {
return match.constName;
}
}
return null;
}
private static List<IntRange> match(String hex, Field field) {
final String constValue;
try {
constValue = ((String) field.get(null)).trim();
if (constValue.length() / 3 <= 3) {//Minimum numbers of const hex bytes
return new LinkedList<>();
}
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (ClassCastException ignored) {
return new LinkedList<>();
}
return new LinkedList<>() {{
int index = -1;
while ((index = hex.indexOf(constValue, index + 1)) != -1) {
add(new IntRange(index / 3, (index + constValue.length()) / 3));
}
}};
}
private static class Match {
private IntRange range;
private String constName;
Match(IntRange range,String constName){
this.range = range;
this.constName = constName;
}
}
}
private static void buildConstNameChain(int length, ConstMatcher constMatcher, StringBuilder constNameBuilder) {
//System.out.println(constMatcher.matches);
for (int i = 0; i < length; i++) {
constNameBuilder.append(" ");
String match = constMatcher.getMatchedConstName(i / 4);
if (match != null) {
int appendedNameLength = match.length();
constNameBuilder.append(match);
while (match.equals(constMatcher.getMatchedConstName(i++ / 4))) {
if (appendedNameLength-- < 0) {
constNameBuilder.append(" ");
}
}
constNameBuilder.append(" ".repeat(match.length() % 4));
}
}
}
private static String compare(String hex1s, String hex2s) {
StringBuilder builder = new StringBuilder();
String[] hex1 = hex1s.trim().replace("\n", "").split(" ");
String[] hex2 = hex2s.trim().replace("\n", "").split(" ");
ConstMatcher constMatcher1 = new ConstMatcher(hex1s);
ConstMatcher constMatcher2 = new ConstMatcher(hex2s);
if (hex1.length == hex2.length) {
builder.append(GREEN).append("长度一致:").append(hex1.length);
} else {
builder.append(RED).append("长度不一致").append(hex1.length).append("/").append(hex2.length);
}
StringBuilder numberLine = new StringBuilder();
StringBuilder hex1ConstName = new StringBuilder();
StringBuilder hex1b = new StringBuilder();
StringBuilder hex2b = new StringBuilder();
StringBuilder hex2ConstName = new StringBuilder();
int dif = 0;
int length = Math.max(hex1.length, hex2.length) * 4;
buildConstNameChain(length, constMatcher1, hex1ConstName);
buildConstNameChain(length, constMatcher2, hex2ConstName);
for (int i = 0; i < Math.max(hex1.length, hex2.length); ++i) {
String h1 = null;
String h2 = null;
boolean isDif = false;
if (hex1.length <= i) {
h1 = RED + "__";
isDif = true;
} else {
String matchedConstName = constMatcher1.getMatchedConstName(i);
if (matchedConstName != null) {
h1 = BLUE + hex1[i];
}
}
if (hex2.length <= i) {
h2 = RED + "__";
isDif = true;
} else {
String matchedConstName = constMatcher2.getMatchedConstName(i);
if (matchedConstName != null) {
h2 = BLUE + hex2[i];
}
}
if (h1 == null && h2 == null) {
h1 = hex1[i];
h2 = hex2[i];
if (h1.equals(h2)) {
h1 = GREEN + h1;
h2 = GREEN + h2;
} else {
h1 = RED + h1;
h2 = RED + h2;
isDif = true;
}
} else {
if (h1 == null) {
h1 = RED + hex1[i];
}
if (h2 == null) {
h2 = RED + hex2[i];
}
}
numberLine.append(UNKNOWN).append(getFixedNumber(i)).append(" ");
hex1b.append(" ").append(h1).append(" ");
hex2b.append(" ").append(h2).append(" ");
if (isDif) {
++dif;
}
//doConstReplacement(hex1b);
//doConstReplacement(hex2b);
}
return (builder.append(" ").append(dif).append(" 个不同").append("\n")
.append(numberLine).append("\n")
.append(hex1ConstName).append("\n")
.append(hex1b).append("\n")
.append(hex2b).append("\n")
.append(hex2ConstName).append("\n")
)
.toString();
}
private static void doConstReplacement(StringBuilder builder) {
String mirror = builder.toString();
HexReader hexs = new HexReader(mirror);
for (AtomicInteger i = new AtomicInteger(0); i.get() < builder.length(); i.addAndGet(1)) {
hexs.setTo(i.get());
consts.forEach(a -> {
hexs.setTo(i.get());
List<Integer> posToPlaceColor = new LinkedList<>();
AtomicBoolean is = new AtomicBoolean(false);
a.readFully((c, d) -> {
if (c.equals(hexs.readHex())) {
posToPlaceColor.add(d);
} else {
is.set(false);
}
});
if (is.get()) {
AtomicInteger adder = new AtomicInteger();
posToPlaceColor.forEach(e -> {
builder.insert(e + adder.getAndAdd(BLUE.length()), BLUE);
});
}
});
}
}
private static String getFixedNumber(int number) {
if (number < 10) {
return "00" + number;
}
if (number < 100) {
return "0" + number;
}
return String.valueOf(number);
}
private static String getClipboardString() {
Transferable trans = Toolkit.getDefaultToolkit().getSystemClipboard().getContents(null);
if (trans.isDataFlavorSupported(DataFlavor.stringFlavor)) {
try {
return (String) trans.getTransferData(DataFlavor.stringFlavor);
} catch (Exception e) {
e.printStackTrace();
}
}
return null;
}
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
while (true) {
System.out.println("Hex1: ");
var hex1 = scanner.nextLine();
System.out.println("Hex2: ");
var hex2 = scanner.nextLine();
System.out.println("\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n");
System.out.println(HexComparator.compare(hex1, hex2));
System.out.println();
}
/*
System.out.println(HexComparator.compare(
//mirai
"2A 22 96 29 7B 00 40 00 01 01 00 00 00 00 00 00 00 4D 53 47 00 00 00 00 00 EC 21 40 06 18 89 54 BC Protocol.messageConst1 00 00 01 00 0A 01 00 07 E7 89 9B E9 80 BC 21\n"
,
//e
"2A 22 96 29 7B 00 3F 00 01 01 00 00 00 00 00 00 00 4D 53 47 00 00 00 00 00 5D 6B 8E 1A FE 39 0B FC Protocol.messageConst1 00 00 01 00 0A 01 00 07 6D 65 73 73 61 67 65"
));
/*
System.out.println(HexComparator.compare(
//e
"90 5E 39 DF 00 02 76 E4 B8 DD 00 00 04 53 00 00 00 01 00 00 15 85 00 00 01 55 35 05 8E C9 BA 16 D0 01 63 5B 59 4B 59 52 31 01 B9 00 00 00 00 00 00 00 00 00 00 00 00 00 7B 7B 7B 7B 00 00 00 00 00 00 00 00 00 10 15 74 C4 89 85 7A 19 F5 5E A9 C9 A3 5E 8A 5A 9B AA BB CC DD EE FF AA BB CC",
//mirai
"6F 0B DF 92 00 02 76 E4 B8 DD 00 00 04 53 00 00 00 01 00 00 15 85 00 00 01 55 35 05 8E C9 BA 16 D0 01 63 5B 59 4B 59 52 31 01 B9 00 00 00 00 00 00 00 00 00 00 00 00 00 E9 E9 E9 E9 00 00 00 00 00 00 00 00 00 10 15 74 C4 89 85 7A 19 F5 5E A9 C9 A3 5E 8A 5A 9B AA BB CC DD EE FF AA BB CC\n\n\n"
));*/
}
}
class HexReader {
private String s;
private int pos = 0;
private int lastHaxPos = 0;
public HexReader(String s) {
this.s = s;
}
public String readHex() {
boolean isStr = false;
String next = "";
for (; pos < s.length() - 2; ++pos) {
char s1 = ' ';
if (pos != 0) {
s1 = this.s.charAt(0);
}
char s2 = this.s.charAt(pos + 1);
char s3 = this.s.charAt(pos + 2);
char s4 = ' ';
if (this.s.length() != (this.pos + 3)) {
s4 = this.s.charAt(pos + 3);
}
if (
Character.isSpaceChar(s1) && Character.isSpaceChar(s4)
&&
(Character.isDigit(s2) || Character.isAlphabetic(s2))
&&
(Character.isDigit(s3) || Character.isAlphabetic(s3))
) {
this.pos += 2;
this.lastHaxPos = this.pos + 1;
return String.valueOf(s2) + s3;
}
}
return "";
}
public void readFully(BiConsumer<String, Integer> processor) {
this.reset();
String nextHax = this.readHex();
while (!nextHax.equals(" ")) {
processor.accept(nextHax, this.lastHaxPos);
nextHax = this.readHex();
}
}
public void setTo(int pos) {
this.pos = pos;
}
public void reset() {
this.pos = 0;
}
}

View File

@ -0,0 +1,291 @@
@file:Suppress("ObjectPropertyName", "unused", "NonAsciiCharacters", "MayBeConstant")
import net.mamoe.mirai.network.protocol.tim.TIMProtocol
import net.mamoe.mirai.network.protocol.tim.packet.toUHexString
import net.mamoe.mirai.utils.toUHexString
import java.awt.Toolkit
import java.awt.datatransfer.DataFlavor
import java.lang.reflect.Field
import java.util.*
import kotlin.math.max
/**
* Hex 比较器, 并着色已知常量
*
* This could be used to check packet encoding..
* but better to run under UNIX
*
* @author NaturalHG
* @author Him188moe
*/
object HexComparator {
private val RED = "\u001b[31m"
private val GREEN = "\u001b[33m"
private val UNKNOWN = "\u001b[30m"
private val BLUE = "\u001b[34m"
private val clipboardString: String?
get() {
val trans = Toolkit.getDefaultToolkit().systemClipboard.getContents(null)
if (trans.isDataFlavorSupported(DataFlavor.stringFlavor)) {
try {
return trans.getTransferData(DataFlavor.stringFlavor) as String
} catch (e: Exception) {
e.printStackTrace()
}
}
return null
}
class ConstMatcher constructor(hex: String) {
private val matches = LinkedList<Match>()
object TestConsts {
val NIU_BI = "牛逼".toByteArray().toUHexString()
val _1994701021 = 1994701021.toUHexString(" ")
val _1040400290 = 1040400290.toUHexString(" ")
val _580266363 = 580266363.toUHexString(" ")
val _1040400290_ = "3E 03 3F A2"
val _1994701021_ = "76 E4 B8 DD"
val _jiahua_ = "B1 89 BE 09"
val _Him188moe_ = "Him188moe".toByteArray().toUHexString()
val 发图片 = "发图片".toByteArray().toUHexString()
val = "".toByteArray().toUHexString()
val SINGLE_PLAIN_MESSAGE_HEAD = "00 00 01 00 09 01"
val MESSAGE_TAIL_10404 = "0E 00 07 01 00 04 00 00 00 09 19 00 18 01 00 15 AA 02 12 9A 01 0F 80 01 01 C8 01 00 F0 01 00 F8 01 00 90 02 00"
.replace(" ", " ")
}
@Suppress("SpellCheckingInspection")
object PacketIds {
val heartbeat = "00 58"
val friendmsg = "00 CD"
}
init {
CONST_FIELDS.forEach { field ->
for (match in match(hex, field)) {
matches.add(Match(match, field.name))
}
}
}
fun getMatchedConstName(hexNumber: Int): String? {
for (match in this.matches) {
if (match.range.contains(hexNumber)) {
return match.constName
}
}
return null
}
private class Match internal constructor(val range: IntRange, val constName: String)
companion object {
private val CONST_FIELDS: List<Field> = listOf(
TestConsts::class.java,
TIMProtocol::class.java,
PacketIds::class.java
).map { it.declaredFields }.flatMap { fields ->
fields.map { field ->
field.trySetAccessible()
field
}
}
}
private fun match(hex: String, field: Field): List<IntRange> {
val constValue: String
try {
constValue = (field.get(null) as String).trim { it <= ' ' }
if (constValue.length / 3 <= 3) {//Minimum numbers of const hex bytes
return LinkedList()
}
} catch (e: IllegalAccessException) {
throw RuntimeException(e)
} catch (ignored: ClassCastException) {
return LinkedList()
}
return object : LinkedList<IntRange>() {
init {
var index = -1
index = hex.indexOf(constValue, index + 1)
while (index != -1) {
add(IntRange(index / 3, (index + constValue.length) / 3))
index = hex.indexOf(constValue, index + 1)
}
}
}
}
}
private fun buildConstNameChain(length: Int, constMatcher: ConstMatcher, constNameBuilder: StringBuilder) {
//System.out.println(constMatcher.matches);
var i = 0
while (i < length) {
constNameBuilder.append(" ")
val match = constMatcher.getMatchedConstName(i / 4)
if (match != null) {
var appendedNameLength = match.length
constNameBuilder.append(match)
while (match == constMatcher.getMatchedConstName(i++ / 4)) {
if (appendedNameLength-- < 0) {
constNameBuilder.append(" ")
}
}
constNameBuilder.append(" ".repeat(match.length % 4))
}
i++
}
}
fun compare(hex1s: String, hex2s: String): String {
val builder = StringBuilder()
val hex1 = hex1s.trim { it <= ' ' }.replace("\n", "").split(" ".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
val hex2 = hex2s.trim { it <= ' ' }.replace("\n", "").split(" ".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
val constMatcher1 = ConstMatcher(hex1s)
val constMatcher2 = ConstMatcher(hex2s)
if (hex1.size == hex2.size) {
builder.append(GREEN).append("长度一致:").append(hex1.size)
} else {
builder.append(RED).append("长度不一致").append(hex1.size).append("/").append(hex2.size)
}
val numberLine = StringBuilder()
val hex1ConstName = StringBuilder()
val hex1b = StringBuilder()
val hex2b = StringBuilder()
val hex2ConstName = StringBuilder()
var dif = 0
val length = max(hex1.size, hex2.size) * 4
buildConstNameChain(length, constMatcher1, hex1ConstName)
buildConstNameChain(length, constMatcher2, hex2ConstName)
for (i in 0 until max(hex1.size, hex2.size)) {
var h1: String? = null
var h2: String? = null
var isDif = false
if (hex1.size <= i) {
h1 = RED + "__"
isDif = true
} else {
val matchedConstName = constMatcher1.getMatchedConstName(i)
if (matchedConstName != null) {
h1 = BLUE + hex1[i]
}
}
if (hex2.size <= i) {
h2 = RED + "__"
isDif = true
} else {
val matchedConstName = constMatcher2.getMatchedConstName(i)
if (matchedConstName != null) {
h2 = BLUE + hex2[i]
}
}
if (h1 == null && h2 == null) {
h1 = hex1[i]
h2 = hex2[i]
if (h1 == h2) {
h1 = GREEN + h1
h2 = GREEN + h2
} else {
h1 = RED + h1
h2 = RED + h2
isDif = true
}
} else {
if (h1 == null) {
h1 = RED + hex1[i]
}
if (h2 == null) {
h2 = RED + hex2[i]
}
}
numberLine.append(UNKNOWN).append(getFixedNumber(i)).append(" ")
hex1b.append(" ").append(h1).append(" ")
hex2b.append(" ").append(h2).append(" ")
if (isDif) {
++dif
}
//doConstReplacement(hex1b);
//doConstReplacement(hex2b);
}
return builder.append(" ").append(dif).append(" 个不同").append("\n")
.append(numberLine).append("\n")
.append(hex1ConstName).append("\n")
.append(hex1b).append("\n")
.append(hex2b).append("\n")
.append(hex2ConstName).append("\n")
.toString()
}
private fun getFixedNumber(number: Int): String {
if (number < 10) {
return "00$number"
}
return if (number < 100) {
"0$number"
} else number.toString()
}
}
fun main() {
val scanner = Scanner(System.`in`)
while (true) {
println("Hex1: ")
val hex1 = scanner.nextLine()
println("Hex2: ")
val hex2 = scanner.nextLine()
println("\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n")
println(HexComparator.compare(hex1, hex2))
println()
}
/*
System.out.println(HexComparator.compare(
//mirai
"2A 22 96 29 7B 00 40 00 01 01 00 00 00 00 00 00 00 4D 53 47 00 00 00 00 00 EC 21 40 06 18 89 54 BC Protocol.messageConst1 00 00 01 00 0A 01 00 07 E7 89 9B E9 80 BC 21\n"
,
//e
"2A 22 96 29 7B 00 3F 00 01 01 00 00 00 00 00 00 00 4D 53 47 00 00 00 00 00 5D 6B 8E 1A FE 39 0B FC Protocol.messageConst1 00 00 01 00 0A 01 00 07 6D 65 73 73 61 67 65"
));
*/
/*
System.out.println(HexComparator.compare(
//e
"90 5E 39 DF 00 02 76 E4 B8 DD 00 00 04 53 00 00 00 01 00 00 15 85 00 00 01 55 35 05 8E C9 BA 16 D0 01 63 5B 59 4B 59 52 31 01 B9 00 00 00 00 00 00 00 00 00 00 00 00 00 7B 7B 7B 7B 00 00 00 00 00 00 00 00 00 10 15 74 C4 89 85 7A 19 F5 5E A9 C9 A3 5E 8A 5A 9B AA BB CC DD EE FF AA BB CC",
//mirai
"6F 0B DF 92 00 02 76 E4 B8 DD 00 00 04 53 00 00 00 01 00 00 15 85 00 00 01 55 35 05 8E C9 BA 16 D0 01 63 5B 59 4B 59 52 31 01 B9 00 00 00 00 00 00 00 00 00 00 00 00 00 E9 E9 E9 E9 00 00 00 00 00 00 00 00 00 10 15 74 C4 89 85 7A 19 F5 5E A9 C9 A3 5E 8A 5A 9B AA BB CC DD EE FF AA BB CC\n\n\n"
));*/
}

View File

@ -3,14 +3,16 @@
import jpcap.JpcapCaptor
import jpcap.packet.IPPacket
import jpcap.packet.UDPPacket
import net.mamoe.mirai.message.defaults.readMessageChain
import net.mamoe.mirai.network.protocol.tim.TIMProtocol
import net.mamoe.mirai.network.protocol.tim.packet.ServerEventPacket
import net.mamoe.mirai.network.protocol.tim.packet.ServerPacket
import net.mamoe.mirai.network.protocol.tim.packet.UnknownServerEventPacket
import net.mamoe.mirai.network.protocol.tim.packet.UnknownServerPacket
import net.mamoe.mirai.utils.*
import java.io.DataInputStream
/**
* 模拟登录并抓取到 session key
* 抓包分析器
*
* @author Him188moe
*/
@ -59,8 +61,8 @@ object Main {
dataReceived(pk.data)
} else {
try {
println("size = " + pk.data.size)
dataSent(pk.data)
println()
} catch (e: Exception) {
e.printStackTrace()
}
@ -74,24 +76,47 @@ object Main {
/**
* TIM 内存中读取.
* TIM 内存中读取
*
* 方法:
* Common.dll 中搜索
* 1. x32dbg 附加 TIM
* 2. `符号` 中找到 common.dll
* 3. 搜索函数 `oi_symmetry_encrypt2` (TEA 加密函数)
* 4. 双击跳转
* 5. 断点并在TIM发送消息以触发
* 6. 运行到 `mov eax,dword ptr ss:[ebp+10]`
* 7. eax 开始的 16 bytes 便是 `sessionKey`
*/
const val sessionKey: String = "70 BD 1E 12 20 C1 25 12 A0 F8 4F 0D C0 A0 97 0E"
val sessionKey: ByteArray = "48 C0 11 42 2D FD 8F 36 6E BA BF FD D3 AA B7 AE".hexToBytes()
fun dataReceived(data: ByteArray) {
//println("--------------")
//println("接收数据包")
//println("raw packet = " + data.toUHexString())
packetReceived(ServerPacket.ofByteArray(data))
}
fun packetReceived(packet: ServerPacket) {
when (packet) {
is ServerEventPacket.Raw.Encrypted -> {
val sessionKey = "8B 45 10 0F 10 00 66 0F 38 00 05 20 39 18 64 0F".hexToBytes()
println("! ServerEventPacket.Raw.Encrypted")
packetReceived(packet.decrypt(sessionKey))
println("! decrypt succeed")
}
is ServerEventPacket.Raw -> packetReceived(packet.distribute())
is UnknownServerEventPacket -> {
println("--------------")
println("未知事件ID=" + packet.packetId.toUHexString())
println("未知事件: " + packet.input.readAllBytes().toUHexString())
}
is ServerEventPacket -> {
println("事件")
println(packet)
}
is UnknownServerPacket -> {
//ignore
}
else -> {
@ -99,47 +124,45 @@ object Main {
}
}
fun dataSent(rawPacket: ByteArray) = rawPacket.cutTail(1).decode { packet ->
println("---------------------------")
packet.skip(3)//head
val idHex = packet.readNBytes(4).toUHexString()
println("发出包ID = $idHex")
packet.skip(TIMProtocol.fixVer2.hexToBytes().size + 1 + 5 - 3 + 1)
val encryptedBody = packet.readAllBytes()
println("body = ${encryptedBody.toUHexString()}")
encryptedBody.decode { data ->
fun dataSent(data: ByteArray) {
data.cutTail(1).decode { base ->
base.skip(3)
val idHex = base.readNBytes(4).toUHexString()
println("发出包$idHex")
when (idHex.substring(0, 5)) {
"00 CD" -> {
println("好友消息发出: ")
dataDecode(data) {
//it.readShort()
//println(it.readUInt())
println(it.readNBytes(TIMProtocol.fixVer2.hexToBytes().size + 1 + 5 - 3 + 1).toUHexString())
it.readAllBytes().let {
println("解密")
println(it.size)
println(it.toUHexString())
println(it.decryptBy(sessionKey).toUHexString())
}
println("好友消息")
val raw = data.readAllBytes()
println("解密前数据: " + raw.toUHexString())
val messageData = raw.decryptBy(sessionKey)
println("解密结果: " + messageData.toUHexString())
println("尝试解消息")
messageData.decode {
it.skip(
4 + 4 + 12 + 2 + 4 + 4 + 16 + 2 + 2 + 4 + 2 + 16 + 4 + 4 + 7 + 15 + 2
+ 1
)
val chain = it.readMessageChain()
println(chain.toObjectString())
}
}
"03 88" -> {
println("上传图片-获取图片ID")
data.skip(8)
val body = data.readAllBytes().decryptBy(sessionKey)
println(body.toUHexString())
}
}
}
}
private fun ByteArray.decryptBy(key: ByteArray): ByteArray = TEA.decrypt(this, key)
private fun ByteArray.decryptBy(key: String): ByteArray = TEA.decrypt(this, key)
private fun DataInputStream.skipHex(uHex: String) {
this.skip(uHex.hexToBytes().size.toLong())
}
}
/*
00 19
tim的 publicKey = 02 F4 07 37 2D F1 82 1D 45 E8 30 14 41 74 AF E3 03 AB 29 D7 82 D9 E2 E5 89
00 00
tim的 key0836=70 BE 41 20 3A FA 05 B2 2D 66 2E 29 33 55 99 7E
552
76 AF AE 95 EB 89 BE B5 1C 83 D2 87 23 3B 5A 3B 6B 4C 78 AD F9 93 86 CA 13 D7 86 B5 0C D1 84 FB 2B ED 59 26 42 3B E0 6F 1A 91 A5 98 91 20 25 3F 6D C0 F6 FC 27 3D F8 34 EA 50 95 8C 2A BB 22 73 BD 76 60 2A 6B 68 51 07 4A 2F 37 6D 97 42 51 C5 14 47 96 3A A9 6B 8F 66 F8 D4 F4 52 22 13 D5 CC 9F B1 B4 06 BC 4B 35 B6 CF D8 CB 70 0F 0C E6 AA D9 12 E9 A2 C7 7F D8 24 7E 1B 2D 97 67 DA 34 0A FD 8E 44 D3 58 50 0D F0 0A 20 08 0A 46 28 68 0A 06 17 36 84 94 2C 97 2A 22 32 7B 01 67 3F E4 90 71 88 B2 F9 7B 7B AC 1A 00 CD 54 4A D7 AE 71 68 B3 FB E5 F3 94 9A C2 A1 C3 CA A5 4E AB 2C B0 78 AD EE 63 3F E6 24 6E AC 31 A5 00 F4 DB C7 4B 65 44 7B 92 87 30 7D 73 B3 21 81 C8 99 33 06 65 28 0C 98 56 EF 41 DC 64 79 55 69 AD B7 F4 A4 CF 4A 28 4B 3B E3 5A 2B C1 72 20 95 D9 8E 9F 1E A5 DE 9A DD 39 0B BE 76 A8 BE 95 9D 7C C2 C5 A8 3A D3 76 B6 D4 ED 15 34 5D 3C 8E 96 C6 93 64 78 A1 89 78 DA F8 17 E5 96 75 5F B6 97 FC 41 18 A4 54 67 BA 3B ED 97 27 B7 E3 90 81 1E DC 8D 17 25 46 2D 08 0D BB 95 D0 CB C8 9B 78 36 2D 70 E3 C6 4C 21 E9 C0 02 69 3B C5 F7 91 6B 62 D8 E4 10 F0 01 5B 7F 1A 3E 9F 1A D4 D3 A9 2B 4A C2 BD 6D 8B B0 0A AE A4 E9 72 71 F4 39 28 CE 18 42 ED FD BB 61 08 B1 95 93 8E F6 29 D7 B6 CB 15 2A AA AF A7 81 AD DF 3B D5 3F 47 29 AB 61 0C 86 48 82 93 AE 8C 2C 32 CC 83 83 68 08 C6 9D 10 81 82 BA 92 24 0E ED 71 B1 83 E1 08 D0 01 BB DF E2 26 D0 20 DF 8C 95 E1 A6 42 C2 A2 E7 85 00 E6 AA 54 A8 0C 5D BB 8D 46 37 AD 47 88 38 B9 D7 3B 48 13 13 81 3B A5 05 4D 32 24 A4 CE 08 73 6D 89 FD 6D CC F5 AB 8B 6A 39 4B 9D 30 33 73 F1 01 7F E4 43 03 72 44 67 3A 24 28 40 51 2B EB 48 EB F9 05 A9 3C 20 EB 4D B7 45 56 D3 4E BD A0 B5 40 65 D1 16 57 73 A4 81 B1 A6 8C 3F 68 28 AA EB 83
*/
}