前言
简单分析{某点评}TCP私信协议,本帖只负责分析交流切勿他用。若侵犯了权益,麻烦管理帮忙删除,谢谢!
TCP交互流程
TCP协议交互理解:
- 请求服务器共享密钥,或本地按规则生成一组key【篇文比较长分段开始】
- RSA公钥加密并且组包发送到服务器统一加密密钥({某小破站登陆协议一样})【分析初始化并且登陆服务器】
- 登陆服务器:组包数据包括 1. 登陆的设备id 2. 附带APP版本号 3. 附带该账号的uid 4. 附带该账号的令牌{token,CK}之类
工具
Tools |
Version |
样本 |
11.12.14 |
Frida |
16.1.11 |
objection |
1.11.0 |
jadx-gui |
1.2.0 |
调试工具 |
Redmi Note 7 |
分析流程
1.前篇文章已经分析了统一密钥,接着突破口继续来初始化并且登陆服务器。frida脚本 hook java 层的系统加解密并且打印堆栈。观察到统一密钥之后的一个初始化token令牌堆栈。
AES/CTR/NoPadding: AAABwQADAHEAFQAA9Lz1gwAAAAIAAAAAOHf6P+g7xAc75dCBq9r2QGVivjy0MhT32lDB/Za6FZweGbqI0UA8W//3pEbkvFKkplHuLkyAhWgFgjXN1jI4E82dRjYtoQlEBQzWJZyHgfmQgIaWMuKHyDg/6BEzypM/b+ZX5QMZMYwS+Q+wM+LWLi1RWPIj07a/UdbeFSaU5Bq1YXLDcLEnSGvWDrrZZBv+c6tx95zSeh17rFnYEYjA7d9j4yGQ1mBUsYG5xbjGFNtszW/ov5lq+0/9pJ+A87c/gUVXAs3xtDPaJs/RfwNENDE2qCHOwFImwOqhDRgzAz6AtgouY7LLmzpcQSLvV0YlJ4YTfDxJ4TgGpALHKujkw5q+W+ZKK/UEYPYOqr7v+VqE477BtXOZx6zwSxZKMVCAFkOx21AYPTNo/02J9oy3koCwYGdpgxgs9hxIXNnk9OioJPDVzEYHF9ckvyHnClvdnhNXS5b17RDZlb+L55uCE0BjYJ1wmO+00SRff9MXiH5j30fJPNB3TAvN2Ev+FpgK9R2SlN5YfHoiBhHyKY4PrYd69KaUH2WZ7mTAO+aeF0o0leb8J81NMNY=
java.lang.Exception
at com.sankuai.xm.protobase.utils.a.c(Native Method)
at com.sankuai.xm.login.manager.channel.f.c(CryptProcessor.java:5)
at com.sankuai.xm.login.manager.channel.b.G(ConnectionChannel.java:8)
at com.sankuai.xm.login.manager.channel.b.F(Unknown Source:30)
at com.sankuai.xm.login.manager.channel.b.x(ConnectionChannel.java:69)
at com.sankuai.xm.login.manager.channel.b.w(ConnectionChannel.java:8)
at com.sankuai.xm.login.manager.channel.b$a.a(ConnectionChannel.java:29)
at com.sankuai.xm.login.manager.channel.b.a(ConnectionChannel.java:5)
at com.sankuai.xm.login.manager.channel.e.a(Connector.java:3)
at com.sankuai.xm.login.net.e.d(NetTcpLink.java:13)
at com.sankuai.xm.login.net.f.f(SocketPump.java:6)
at com.sankuai.xm.login.net.f.d(SocketPump.java:9)
at com.sankuai.xm.login.net.taskqueue.e.b(TaskPump.java:1)
at com.sankuai.xm.login.net.taskqueue.b$a.run(AbstractQueue.java:17)
at com.sankuai.android.jarvis.i.run(JarvisRunnableProxy.java:13)
at java.lang.Thread.run(Thread.java:919)
at com.sankuai.android.jarvis.p.run(JarvisThreadProxy.java:3)
at com.sankuai.android.jarvis.m.t(JarvisThreadPoolImpl.java:14)
at com.sankuai.android.jarvis.m$d.run(Unknown Source:20)
at java.lang.Thread.run(Thread.java:919)
2.模糊判定是初始化数据包,跟进堆栈com.sankuai.xm.login.manager.channel.b.x(ConnectionChannel.java:69)
方法太长判断很多 贴出主要代码部分
if (i3 == 0) {
com.sankuai.xm.login.beans.b bVar = (com.sankuai.xm.login.beans.b) this.f89418e;
com.sankuai.xm.base.proto.protosingal.h hVar = new com.sankuai.xm.base.proto.protosingal.h();// new 了一个类然后传参数进去
hVar.L(bVar.d);
hVar.f = bVar.f89407e; //TCP数据参数
hVar.g = bVar.f; //TCP数据参数
hVar.h = bVar.g; //TCP数据参数
hVar.i = bVar.h; //TCP数据参数
o.o().h();
hVar.j = 1; //TCP数据参数
hVar.k = o.o().a(); //TCP数据参数
o.o().k();
hVar.l = 1; //TCP数据参数
hVar.m = bVar.i //TCP数据参数
hVar.n = bVar.j; //TCP数据参数
hVar.o = h2 //TCP数据参数
hVar.p = q(); //TCP数据包参数 以上都是传参数进去
if (F.d(hVar.f) || F.d(hVar.h)) {
d.c("ConnectionChannel::doAuth:: PLoginByPassport, passport or device==null", new Object[0]);
u(21, 0, "", "", "", null);
} else {
byte[] marshall = hVar.marshall(); // 这里是取出原数据的方法
StringBuilder m2 = android.arch.core.internal.b.m("ConnectionChannel::doAuth:: PLoginByPassport, passport = ");
m2.append(bVar.f89407e);
m2.append(", device = ");
m2.append(bVar.g);
m2.append(",deviceData = ");
m2.append(hVar.n);
m2.append(", crc ");
if (marshall != null) {
str = CommonUtil.a(marshall);
}
m2.append(str);
d.f(m2.toString());
F(marshall); //这里是 x(ConnectionChannel.java:69) 最后TCP协议发送前处理数据的地方 包含加密
}
4.跟进 byte[] marshall = hVar.marshall();
public final class h extends g {
public static ChangeQuickRedirect changeQuickRedirect;
public String f;
public String g;
public String h;
public String i;
public short j;
public int k;
public short l;
public String m;
public String n;
public boolean o;
public long p;
@Override // com.sankuai.xm.base.proto.protobase.g, com.sankuai.xm.base.proto.protobase.b
public final byte[] marshall() { //返回原数据的marshall 方法
O(196721); //消息标签
D(this.f);
D(this.g);
D(this.h);
D(this.i);
C(this.j);
z(this.k);
C(this.l);
D(this.m);
D(this.n);
v(Boolean.valueOf(this.o));
A(this.p);
D(null); // 以上就是new的一个类 然后把参数全部都传进处理完毕后存放在 public ByteBuffer f87924b;
return super.marshall(); //返回的是他的父类的marshall方法 跟进去
}
}
5.简单列举一个D方法 传进的 D(this.f)
;
public final void D(String str) {
Object[] objArr = {str};
ChangeQuickRedirect changeQuickRedirect2 = changeQuickRedirect;
if (PatchProxy.isSupport(objArr, this, changeQuickRedirect2, 444570)) {
PatchProxy.accessDispatch(objArr, this, changeQuickRedirect2, 444570);
} else if (str == null) { //str 不为空
b(2);
this.f87924b.putShort(0);
} else {
try {
int d = d(str);
if (d <= 32767) {
short s = (short) d;
b(s + 2);
this.f87924b.putShort(s); //传进数据的长度
this.f87924b.put(str.getBytes()); //传进数据的内容
return;
}
throw new RuntimeException("string too long");
} catch (UnsupportedEncodingException e2) {
throw new RuntimeException(e2);
}
}
}
6.跟进父类super.marshall()
的marshall方法
public byte[] marshall() {
int i;
int i2;
this.c.d = 0;
b(0);
this.c.f87920a = this.f87924b.position();
this.f87924b.putInt(0, this.c.f87920a); // TCP数据包的总长度
this.f87924b.putInt(4, this.c.f87921b); //数据包标签
this.f87924b.putShort(8, this.c.c); //固定
this.f87924b.putShort(10, this.c.d); //固定
this.f87924b.putInt(12, 0); // CRC32校验
d dVar = this.c;
if (dVar.f <= 0) {
Object[] objArr2 = new Object[0];
ChangeQuickRedirect changeQuickRedirect3 = changeQuickRedirect;
if (PatchProxy.isSupport(objArr2, null, changeQuickRedirect3, 2979355)) {
i = ((Integer) PatchProxy.accessDispatch(objArr2, null, changeQuickRedirect3, 2979355)).intValue();
} else {
synchronized (g.class) {
f87925e++;
if (f87925e <= 0) {
f87925e = 1;
}
i2 = f87925e;
}
i = i2;
}
dVar.f = i;
}
this.f87924b.putInt(16, this.c.f); //类似于调用次数
this.f87924b.putInt(20, this.c.g); //固定为
byte[] bArr = new byte[this.c.f87920a]; //new 一个byte[]
this.f87924b.position(0);
this.f87924b.get(bArr); //把数据存放进bArr 里面
int F = F(bArr); //CRC32校验
this.f87924b.putInt(12, F); //把CRC32校验数据存放进第12 位置里面
this.c.f87922e = F;
this.f87924b.position(0);
this.f87924b.get(bArr); //把this.f87924b数据包存进bArr 里面
this.f87924b = null;
return bArr; //返回最终原数据包
}
7.原始数据包已经分析完毕,现在堆栈处at com.sankuai.xm.protobase.utils.a.c(Native Method)
最后调用该方法进行AES/CTR/NoPadding
加密之后再发送到服务器。
public final byte[] c(byte[] bArr) {
byte[] bArr2;
if (bArr != null && bArr.length >= 24) {
try {
int i = this.c ? 16 : 0;
byte[] bArr3 = new byte[(bArr.length + i)];
SecretKeySpec secretKeySpec = new SecretKeySpec(this.f89761b, "AES");
Cipher instance = Cipher.getInstance("AES/CTR/NoPadding");
if (i > 0) {
bArr2 = new byte[i];
new SecureRandom().nextBytes(bArr2);
} else {
bArr2 = new byte[instance.getBlockSize()];
}
instance.init(1, secretKeySpec, new IvParameterSpec(bArr2));
byte[] doFinal = instance.doFinal(bArr, 24, bArr.length - 24);// 取整个TCP数据包的第25位到末尾进行doFinal加密
System.arraycopy(bArr, 0, bArr3, 0, 24);
if (i > 0) {
ByteBuffer allocate = ByteBuffer.allocate(4);
allocate.position(0);
allocate.put(bArr3, 0, 4);
allocate.flip();
int i2 = allocate.getInt();
allocate.flip();
allocate.putInt(i2 + i);
allocate.flip();
byte[] bArr4 = new byte[4];
allocate.get(bArr4);
System.arraycopy(bArr4, 0, bArr3, 0, 4);
System.arraycopy(bArr2, 0, bArr3, 24, i);
}
System.arraycopy(doFinal, 0, bArr3, i + 24, doFinal.length);//取原数据包的前25位 + 加密后的数据组成最终TCP服务器认可的数据包
return bArr3;
} catch (Exception e2) {
com.sankuai.xm.log.a.e(e2);
}
}
return bArr;
}
8.至此 ,初始化数据包并登陆服务器分析完毕,代码没多少就是需要花费点时间调试。
二进制数据包解析:
原始数据包:{ 0, 0, 1, 193, 0, 3, 0, 113, 0, 21, 0, 0, 244, 188, 245, 131, 0, 0, 0, 2, 0, 0, 0, 0, 0, 10, 49, 57, 49, 53, 57, 56, 52, 52, 52, 53, 0, 228, 48, 50, 48, 50, 50, 55, 50, 51, 48, 98, 99, 97, 52, 49, 50, 48, 54, 99, 100, 48, 97, 52, 99, 100, 48, 55, 48, 54, 52, 55, 50, 56, 100, 52, 97, 100, 52, 98, 49, 97, 48, 53, 97, 48, 54, 50, 101, 53, 52, 98, 101, 97, 55, 97, 50, 97, 98, 57, 54, 98, 102, 50, 55, 97, 97, 97, 99, 100, 98, 53, 98, 101, 50, 52, 54, 99, 55, 51, 57, 102, 48, 52, 51, 50, 55, 51, 57, 99, 51, 101, 50, 52, 54, 52, 48, 97, 101, 102, 51, 51, 56, 48, 57, 50, 48, 55, 98, 50, 48, 57, 54, 56, 102, 102, 102, 56, 48, 48, 48, 48, 48, 48, 48, 48, 102, 99, 49, 100, 48, 48, 48, 48, 48, 102, 49, 53, 100, 99, 102, 52, 52, 99, 98, 54, 102, 54, 101, 48, 102, 54, 50, 52, 54, 97, 53, 98, 101, 101, 51, 100, 97, 48, 53, 102, 56, 54, 50, 101, 97, 53, 102, 51, 51, 51, 52, 57, 53, 101, 57, 57, 49, 50, 52, 101, 99, 99, 51, 50, 98, 49, 54, 54, 48, 102, 100, 49, 97, 54, 102, 51, 102, 99, 50, 49, 56, 51, 55, 54, 54, 56, 102, 48, 101, 98, 52, 48, 53, 54, 56, 54, 97, 55, 100, 54, 100, 102, 51, 101, 0, 41, 48, 49, 97, 57, 53, 53, 50, 52, 45, 101, 49, 57, 54, 45, 52, 50, 101, 55, 45, 98, 54, 54, 100, 45, 100, 102, 50, 49, 99, 98, 48, 52, 53, 97, 56, 98, 95, 49, 95, 50, 49, 0, 8, 49, 49, 46, 49, 50, 46, 49, 52, 0, 1, 0, 61, 193, 102, 0, 1, 0, 51, 50, 97, 102, 100, 100, 54, 51, 48, 54, 98, 57, 100, 52, 50, 52, 49, 56, 49, 52, 101, 50, 50, 52, 52, 50, 99, 98, 49, 53, 101, 99, 55, 97, 49, 54, 55, 49, 51, 57, 52, 49, 51, 50, 54, 53, 51, 49, 57, 55, 48, 50, 0, 56, 123, 34, 100, 101, 118, 105, 99, 101, 73, 100, 34, 58, 34, 48, 49, 97, 57, 53, 53, 50, 52, 45, 101, 49, 57, 54, 45, 52, 50, 101, 55, 45, 98, 54, 54, 100, 45, 100, 102, 50, 49, 99, 98, 48, 52, 53, 97, 56, 98, 95, 49, 95, 50, 49, 34, 125, 0, 3, 69, 29, 101, 57, 67, 207, 186, 0, 0 }
1. { 0, 0, 1, 193 } //数据包总长度 449
2. { 0, 3, 0, 113 } //消息标签【196721】
3. { 0, 21, 0, 0 } //未知 暂时固定
4. { 244, 188, 245, 131 } //CRC32 数据包校验
5. { 0, 0, 0, 2, 0, 0, 0, 0, 0 } //调用次数 固定
6. {10, 49, 57, 49, 53, 57, 56, 52, 52, 52, 53 } //长度为10位的 userid 由于是文章 此处设为随机
7. { 228, 48, 50, 48, 50, 50, 55, 50, 51, 48, 98, 99, 97, 52, 49, 50, 48, 54, 99, 100, 48, 97, 52, 99, 100, 48, 55, 48, 54, 52, 55, 50, 56, 100, 52, 97, 100, 52, 98, 49, 97, 48, 53, 97, 48, 54, 50, 101, 53, 52, 98, 101, 97, 55, 97, 50, 97, 98, 57, 54, 98, 102, 50, 55, 97, 97, 97, 99, 100, 98, 53, 98, 101, 50, 52, 54, 99, 55, 51, 57, 102, 48, 52, 51, 50, 55, 51, 57, 99, 51, 101, 50, 52, 54, 52, 48, 97, 101, 102, 51, 51, 56, 48, 57, 50, 48, 55, 98, 50, 48, 57, 54, 56, 102, 102, 102, 56, 48, 48, 48, 48, 48, 48, 48, 48, 102, 99, 49, 100, 48, 48, 48, 48, 48, 102, 49, 53, 100, 99, 102, 52, 52, 99, 98, 54, 102, 54, 101, 48, 102, 54, 50, 52, 54, 97, 53, 98, 101, 101, 51, 100, 97, 48, 53, 102, 56, 54, 50, 101, 97, 53, 102, 51, 51, 51, 52, 57, 53, 101, 57, 57, 49, 50, 52, 101, 99, 99, 51, 50, 98, 49, 54, 54, 48, 102, 100, 49, 97, 54, 102, 51, 102, 99, 50, 49, 56, 51, 55, 54, 54, 56, 102, 48, 101, 98, 52, 48, 53, 54, 56, 54, 97, 55, 100, 54, 100, 102, 51, 101 } //长度为228位的 token令牌 由于是文章 此处设为随机
8. { 41, 48, 49, 97, 57, 53, 53, 50, 52, 45, 101, 49, 57, 54, 45, 52, 50, 101, 55, 45, 98, 54, 54, 100, 45, 100, 102, 50, 49, 99, 98, 48, 52, 53, 97, 56, 98, 95, 49, 95, 50, 49 } //长度为41 的 devicesid
9. { 8, 49, 49, 46, 49, 50, 46, 49, 52 } // 长度为8 的 APP Vsersion
10. { 1, 0, 61, 193, 102, 0, 1 } //未知 固定
11. { 51, 50, 97, 102, 100, 100, 54, 51, 48, 54, 98, 57, 100, 52, 50, 52, 49, 56, 49, 52, 101, 50, 50, 52, 52, 50, 99, 98, 49, 53, 101, 99, 55, 97, 49, 54, 55, 49, 51, 57, 52, 49, 51, 50, 54, 53, 51, 49, 57, 55, 48, 50 } //长度为51的dpid 设备id
12. { 56, 123, 34, 100, 101, 118, 105, 99, 101, 73, 100, 34, 58, 34, 48, 49, 97, 57, 53, 53, 50, 52, 45, 101, 49, 57, 54, 45, 52, 50, 101, 55, 45, 98, 54, 54, 100, 45, 100, 102, 50, 49, 99, 98, 48, 52, 53, 97, 56, 98, 95, 49, 95, 50, 49, 34, 125 } //长度为56的 devicesid
13. { 0, 3, 69, 29, 101, 57, 67, 207, 186, 0, 0 } // 未知 固定
组包发送登陆成功,小号发送消息解密
2024年2月15日22时20分45秒 更新通讯密钥成功!
登陆聊天服务器:103.X.X.X:8500 | 连接是否成功: 真
===================================================================
当前心跳包1发送:真
当前心跳包2发送:真
------------------------------------------------
对方回复消息时间: 2024年2月15日22时21分3秒
Content:【 1 】
------------------------------------------------------------------
对方回复消息时间: 2024年2月15日22时21分9秒
Content:【 登录服务器的流程已经完成仅供学习交流 若侵犯了权益,麻烦管理帮忙删除,谢谢 】
------------------------------------------------------------------
【至此 {登陆服务器}二进制数据包解析完毕!再次声明仅供学习交流 若侵犯了权益,麻烦管理帮忙删除,谢谢】