在一些app中通信架构的方式,没有进入so库就让让逆向分析人员非常难处理,以某大厂的某某视频介绍下整体的流程。
⊙一.抓包分析
⊙二.找到加密点
⊙三.还原加密流程
⊙四.代码还原
⊙总结
一.抓包分析
如下图所示请求体和响应体都被加密了,如果我们想要获得这个接口的数据那么就找到请求发送的逻辑及其请求被解包的逻辑
二.找到加密点
frida-trace -U - -j "*!AdFeedImagePoster" com.t***.******
frida-trace -U -F -j "AdFeedImagePoster!*" com.t***.******
result:
/* TID 0x37fb */
3192 ms AdFeedImagePoster$ProtoAdapter_AdFeedImagePoster.decode("<instance: com.squareup.wire.ProtoReader>")
3194 ms <= "<instance: com.*****.*****protocol.pb.AdFeedImagePoster>"
3248 ms AdFeedImagePoster$ProtoAdapter_AdFeedImagePoster.decode("<instance: com.squareup.wire.ProtoReader>")
3249 ms <= "<instance: com.*****.*****protocol.pb.AdFeedImagePoster>"
7125 ms AdFeedImagePoster$ProtoAdapter_AdFeedImagePoster.decode("<instance: com.squareup.wire.ProtoReader>")
7126 ms <= "<instance: com.*****.*****protocol.pb.AdFeedImagePoster>"
18272 ms AdFeedImagePoster$ProtoAdapter_AdFeedImagePoster.decode("<instance: com.squareup.wire.ProtoReader>")
18273 ms | AdFeedImagePoster$ProtoAdapter_AdFeedImagePoster.decode("<instance: com.squareup.wire.ProtoReader>")
18273 ms | | AdFeedImagePoster$ProtoAdapter_AdFeedImagePoster.decode("<instance: com.squareup.wire.ProtoReader>")
18273 ms | | <= "<instance: com.*****.*****protocol.pb.AdFeedImagePoster>"
18273 ms | <= "<instance: com.*****.*****protocol.pb.AdFeedImagePoster>"
18274 ms <= "<instance: java.lang.Object, $className: com.*****.*****protocol.pb.AdFeedImagePoster>"
18279 ms AdFeedImagePoster$ProtoAdapter_AdFeedImagePoster.decode("<instance: com.squareup.wire.ProtoReader>")
18279 ms | AdFeedImagePoster$ProtoAdapter_AdFeedImagePoster.decode("<instance: com.squareup.wire.ProtoReader>")
18279 ms | <= "<instance: com.*****.*****protocol.pb.AdFeedImagePoster>"
18280 ms <= "<instance: java.lang.Object, $className: com.*****.*****protocol.pb.AdFeedImagePoster>"
18287 ms AdFeedImagePoster$ProtoAdapter_AdFeedImagePoster.decode("<instance: com.squareup.wire.ProtoReader>")
18287 ms | AdFeedImagePoster$ProtoAdapter_AdFeedImagePoster.decode("<instance: com.squareup.wire.ProtoReader>")
18287 ms | <= "<instance: com.*****.*****protocol.pb.AdFeedImagePoster>"
18288 ms <= "<instance: java.lang.Object, $className: com.*****.*****protocol.pb.AdFeedImagePoster>"
18319 ms AdFeedImagePoster$ProtoAdapter_AdFeedImagePoster.decode("<instance: com.squareup.wire.ProtoReader>")
18319 ms | AdFeedImagePoster$ProtoAdapter_AdFeedImagePoster.decode("<instance: com.squareup.wire.ProtoReader>")
18320 ms | <= "<instance: com.*****.*****protocol.pb.AdFeedImagePoster>"
18320 ms <= "<instance: java.lang.Object, $className: com.*****.*****protocol.pb.AdFeedImagePoster>"
frida可以显示出来调用流程,但是我们想要的数据为主页广告,现在请求包是加密的,虽然上面的代码被调用,但是我们无法确定想要的数据就在那个请求包内
三.还原加密流程
但是在apk中的源码中 UnifiedProtocolUtils类里面找到了
方法
encodeUnifiedRequest
decodeUnifiedResponse
经过charles抓包的的数据和这个两个方法的数据来看,是经过这两个方法进行组包后发送到服务器上。
那么如何确定广告在哪呢?
首先让腾讯视频停留在将要进视频详情页单还没进去视频详情页的时候,打开charles进行抓包
截取距离g**广告最近的acc.**.com发送的请求
并将其使用charles保存为二进制文件
小插曲:观察**视频的整个网络请求都在NetWorkTask类下面 package com.t***。***.route
在其请求完成之后调用了
int[] iArr = new int[1];
#bArr2是acc这个请求返回的二进制数据
bArr3 = UnifiedProtocolUtils.decodeUnifiedResponse(bArr2, iArr);
代码如下
import qqbrower.GzipUtils;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.ByteBuffer;
public class main {
private static final int CMD = 65281;
private String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02x", b));
}
System.out.println(sb.toString());
return sb.toString();
}
private static byte[] getJceDataFromBuffer(byte[] bArr, boolean z, int i, int[] iArr) {
bArr = GZIP.Decode(bArr);
ByteBuffer wrap = ByteBuffer.wrap(bArr);
int position = wrap.position() + 16;
int length = (bArr.length - position) - 1;
if (length <= 0) {
iArr[0] = -869;
return null;
}
byte[] bArr2 = new byte[length];
System.arraycopy(bArr, position, bArr2, 0, length);
return bArr2;
}
public static byte[] decodeUnifiedResponse(byte[] bArr, int[] iArr) {
boolean z;
if (bArr == null || bArr.length == 0) {
return null;
}
ByteBuffer wrap = ByteBuffer.wrap(bArr);
if (wrap.get() != 19 || wrap.getInt() != bArr.length) {
return null;
}
wrap.getShort();
if ((wrap.getShort() & 65535) != CMD) {
return null;
}
wrap.getShort();
int i = wrap.getShort() & 65535;
if (i != 0) {
iArr[0] = i;
return null;
}
wrap.getLong();
int i2 = wrap.getInt();
if ((i2 & 2) <= 0) {
z = false;
} else if ((i2 & 16) > 0) {
z = true;
} else {
iArr[0] = -867;
return null;
}
if (wrap.getInt() == 5) {
return null;
}
wrap.getLong();
wrap.position(wrap.position() + 32);
wrap.get();
wrap.position(wrap.position() + 10);
wrap.get();
wrap.position(wrap.position() + (wrap.getShort() & 65535));
int i3 = 65535 & wrap.getShort();
wrap.position(wrap.position() + i3);
int i4 = 83 + i3 + 2;
int i5 = wrap.getInt();
int i6 = i4 + 4;
if (wrap.get(bArr.length - 1) != 3) {
iArr[0] = -869;
return null;
}
int length = (bArr.length - i6) - 1;
if (length <= 0) {
iArr[0] = -868;
return null;
}
byte[] bArr2 = new byte[length];
System.arraycopy(bArr, i6, bArr2, 0, length);
return getJceDataFromBuffer(bArr2, z, i5, iArr);
}
public static void main(String[] args) throws IOException {
File file = new File("C:\mycode\javatools\src\main\java\qqlive\acc25196");
FileInputStream inputStream = new FileInputStream(file);
byte[] bytes = new byte[(int) file.length()];
inputStream.read(bytes);
main tsmain = new main();
int[] iArr = new int[1];
main.decodeUnifiedResponse(bytes, iArr);
tsmain.bytesToHex( main.decodeUnifiedResponse(bytes, iArr));
}
}
输出的二进制文件通过肉眼观察不符合jcestruct的格式
这时猜想是不是protobuf
然后验证了下
[1](i):53
[2](b):com.t**.adService
[3](b):/com.**.adService/getAdDetail
[9](b):
[9.1](i):217
[9.3](i):1655792059010
[9.4](i):1655792059227
[1(1)](b):
[1(1).1](b):mod_recommend_ad
[1(1).2](b):
[1(1).2.2](i):9
[1(1).2.3](b):
[1(1).2.3.1](i):5
[1(1).2.3.2](i):1
[1(1).2.3.3](b):
[1(1).2.3.3.1](b):type.googleapis.com/com.****.protocol.pb.AdFeedImagePoster
[1(1).2.3.3.2](b):
[1(1).2.3.3.2.1](b):
[1(1).2.3.3.2.1.1](b):http://pgdt.gtimg.cn/gdt/0/EABeJclAPAAIcAAAQuTBiocBWCa2eKvgt.jpg/0?ck=f28abb5b10650481e1a4d76ba72d1a49
[1(1).2.3.3.2.1.2](b):抢爆了!悄悄上线的300个经典英语短视频,0元抢!趣味学英语
[1(1).2.3.3.2.1.3](b):英孚教育
[1(1).2.3.3.2.1.4](b):
[1(1).2.3.3.2.1.4.1](i
那么UnifiedProtocolUtils类 不是走的这个地方
但是仔细观察 除了 gzip之外,其组包的方式是相同的
那么我们可以搜索组包的公共代码 发现下方有四处调用
观察四个之后
初步确定 package com.***.**route.v3; QmProtocolTools类
QmProtocolTools类开始追踪 看到底做了什么
经过分析笔者画出来了其通信图
bArr = QmfProtocolTools.packageQmfRequest(this.netContext, mockPBRequestData);
mockPBRequestData =>byte[] pkgProtocolData =pkgProtocolData();
bArr = PBProtocolTools.PbProtocolAsQmfBody.packageRequest(this.netContext);
packagePBFrameHead((short) packageRequestHead.length, pbBusinessReqBytes.length).array();
int length = packageRequestHead.length + 16 + pbBusinessReqBytes.length;
省略--------
因为
预先处理
acc25196req是 请求的二进制文件
将其转化为hex,中所周知gzip经过压缩后,头文件的标志为1f8b,并把二进制最后一个字节单独提出来
1f8b这段就是gzip压缩后的数据 ,我们先分析gzip压缩后的数据是如何产生的 然后再分析第一段1300开头的数据如何构造,其实也就完成了发包的请求
1300000b910001ff01626c00000000000000000035000002130000271c0000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000025d8
1f8bxxxxxxxxxxxx
03
四.代码还原
解析gzip压缩的数据
import gzip
if __name__ == '__main__':
hex1 = "1f8b0800000000000000ed5ac96f245719ef9e2da6218963401a5922b29c300af1b8fbed55cfc6686a6b4f8f778f673cf6a5525d9bcbeeee6a5757b597241244424a500e11ca29e280222171e64214258ac491235c728620c20189bf00f155b5b799c910100805a95b5eeabdf7bdf7adeffbfd4af6d750093edffdf4322e159f113ef6921bb7aba9df71fd4e5adddf6f457dbfda4de23476e356b5dbac3ade5d3fe947ae3fce6bffaa682df453cd33fdd4895ae4c7e5910f4646fffaecf50f2e4f965e2ecd54dd0011e1313f903250294332902a57b0a2b8cc0b9a8eea790c218411c10ac7581f7508a79431a1aa2af57dc2ef5c596a4ca8abe5ed09411816bed2f43ddfc5d87755e97ac295a41930b5e936c92ba59d5272ed41e4c4ed688657bea1566955a02ac1928bb1abc5afeb97272bf9713789bc092aaf622c309b790aa3fcc36f95f46f8d5c1e2d8f3f4308ad2aa88a19ae6245bd53f9761ebeefe43fd6af8c94464bdb5747fef2d45869177f994db7b0e05c9158128e54768bd3e40fe5caa765693043612a68c78a59975c1a5c63266754a15a1daba6a406a786610a5d97583724e31499c8acd7eb58e375a611cad5bac5b13414a2720b5155d4996ed67552d775811503234674824d8d33d5e496d4f3182ba6c91037a86e69dc8424803e5ed738d14d13e5a64845ea9c5b0637a46a5253a526cc4ac4292748c7481716434ca95b8602c2dc34558b68521a583328a186260c3a5afa5bb9345e3aae559e5a6b2c590d2ac65ea48a4b039535b14b691008465d4f5287b13ce8388018c061c751e5e9344ba24e68a791bbe7a7630fa024b0a680a99a863553e18223c550b055d7552e4d222df08ad6b98918932a31306396243aa39a4615a2883a46d2c44addd490412d9383f30617c4508f472b23aed36a6d1c75fdb12b3dbf151c5faf5cc97a9137367a962c41a4108c559eaf7c3d4208fca78edfdf197bb627d36696e0fd3d7918527c38f6623bf6ecc4879bd2f63b9eed7873f2463e95267017fc249f98c6e3bf302a1f5dabbc02ae6c0977c5378ecc29ddd8bddb8a0da3be6bacedf53a0de37837e0fd9500d5369bc91a6ef4d5e61d16de5fd3eaa1b6a519b073fe4075e6ddeeb671200ee61bda5690c82cabd1d5db0e5e9b5fd3d6b4b94acb5a11bb3b0c59f30bcda9f4200e5d6357434e045a4c1caad9bcb9111d76b756fabbfbc2cdb5ec67cdfe9d96a66d6a7aa83534fd406cf2d8255b2d86ad48bbdda7b57ead26a95a6badac69cdf9c6413759db776e276168e5dac0aaa91523017fccf9a3a8d3f3f62dd054539689a51defd60f1565b527069a7656a77a8761f7dc9fc0f4bbbd5da3dd131b7a23d46b99c053a236d5d94f56b46d4dcfc4427b6f7b31142bfd485b9b9babbc5b76bcbe9fa451cf4fe60855105c1a7c23ea78592f4d8ee61845e40674272f73d339aaaa8a541157941b4ee4cd81a060aa545474e3fc0cbbe3b47d7bc7e9edf89edd775a993f071583a450289ceb26be9342c3b303284910ee4269a67352100c552b115c2e2a89e4a4f266398fc2c1e2e68616e9e9c2aab12c1b6c7969f9b69a6dc5d6ca1209536f77a1bbe5a4e6bd4864d9fc9a5e37945a2de03ace23a11f996ba16eadd620d2533453e7b5e3b07e646aa161adcc73355d8871b7bdb909b9d98ba37de66d62b190ae45dafcd454164cd1fe83a43f85f43dcdd206f9e879c9eaa0be22fd78c1b9505f91d16d3867993f6a044bed70f93c1fee6614668b4677db713758a88be0414d098e93feb6f220cf47b8b5b8d4bb7357135bf3eb793ec6feb7dafe7ea9f262c0a41320c79b0e5444a699a77ad3900939cd5893369b8c7005d1b16b04ba8aa0e3bfbee474bb36247b6e8002b36d27ead8ad388c3a739db8e3cf2e6e1ad0703a734492d9d86ee78504a5d2b967ed04e4bebbb8615877378c3bc7d9ca6abcbab91df5b73363a9b107dbee2f16fb24c230b8533c334ce059bf5b0c54acce2ede6f2c17034a25acacea83812a665b7dbbeda47e12392d3b4ce2ac3b373b380f1a6d6ed3f6c02a2ed5597020cb8bdd4475a1222a04158a25b10962b71707ba18cd8d58ac1703456530581818c1113c9b830508170cee0e6c154899dd3fb6432f9ddbf329758ef643c77102de0a591cc7ceec4489946646738cbbfec377defde5a5891fbdf1ce9b97c67f7fb972cd8bba5d3f192bf078fcb2a8921ce0c97388cc20f4d0d733dfcf3a7b9df8a033d183b6fa03fd0493ef3ca7b1198e66989831614f7d86c9f5c7e07ebb8c9aa5dd5272952142c9f144402815c8038c00d4c034f002a950ea3b8c10ce3d82de289f9cfe937261d84fcb0572a84202b2095a670aa0a962321d00981a16e6ba2298a48253806e43811543a1b8ce54a56e48ac48039010ee37a0a3958336ab5b8a29095320f4ef95092c1a4862ca754b5a8c63bd0e0dc33204174803b580df02613812e056981ab38478bf3c81b0ae49a221956266524d3053609320ceeb5221d8621f964bbf29977e5b9ef069c0c12e0768870afe3619f2034f003d0ab0871ce6ffae5cd13a5e1247de0446ef5fba653aad7eb45723555c45132f2d469dec7076e2deecc4b9d0ec441e13f871af51bb8f4915c16540d5354b33961f2c7defe5b745e58fbc72cdcda0c385639ff25727bba1dd4bbb933318dfcc9fddc89b9c997c04f5266f4e267e60fb2d7f72e65578ccd1d4765a21487286312c07adf8c00e92b80d5314c64ebe6c479edd8a7ae9b9182065d6f201f1d3fca8c93fbffdb3cf3efef9671fbdf7f9271fc36a0f88272c024cc35a2ede728ee22cbd30d14b5c7bcf3fca474025242e34fdf343cfd60bc7e066c0c662fa316d5927171dc825000418294d817d5fe226c38c379b943499ab785c0d80d170d8e1e7e1b2bb60a65f1c3ef0104e383c39d2b71dd7f57b3ddb8d7b2958d82e742968b0f44511f2737201df4e2fee3c9e8817f29dadd8f14eed46275b12a7b3f79012921b13a57efbc41a34f0d9dfef64798ebe8cc5dae72cb6ca5441b8b40bf31cb00cf478851f700730c6059586a53071ba3b763772cf33f1e1fb9fbff52b48c39fdefa044b26f304fa6e1ac59d0b218a7a76d689822370e22c68e751b531824e9d8727dfdcf5ddbc8de64df2c4a5530fa1c7e6078ab30c0c727896ee27d5c0e3f5e49fd5d320c267d230e324f0e253505627ecc19ab723ed4e9cdafda87bd3dba1f0addc3c19171b809e04c00a1fce3e7ad445e022855d6290cb4733c94e9d7a42813fe9c6fac57c174e826841f0d2a396ff50d5f81dc0c6478c13ac48d27f5825cd56ecee5dd0755e1a59d282b99d34edced46add2c8baafb305d753bb5be1b0378db3bc7b95c0dd51e71087ab42214c404bc5b8a9ac251d1937280b38b1a18184cc4798d5db8d9fe40e66486e45b4f9ef3e01539ffa220be5034b6acb773727a11b6c3eea08a340308c3dd698c38c22f18daba39dd5836ad07d32a705181e4c5296365d1bc28b8ba6ead6bcb0b0f4fadd41b8bd6c5a9424465f9bb8818ccdcb5162d636365fda2d4c6d2c9c85a9e6f2c9f1e30d03b787e58dbc38a4e5ed9a68ddb96712a7141787de5de8635bd7eef4cfecc029502bb16fc7ce2828fa7265d8081f3fe96f8fb45b1f552a7dd2d2aeeb48260b1eb244ebb67379db0b8bea72f59adb813be96278cbcd6747a5045c5440139f9d379829f7013c08a27b72d48289033d0939e748713cf06be9c66f3240f1720ec91ee3028f182e00dce29f40eabe5dfa99602fb726a71b12571e5b4cdf7dcb8c0085445c096a99c7c7d404bba61ce4a1e45c5335e83f2e7412f744278018ce12dd0dd713a1dbf0562c769b194b77f34a01ca0da6db97b43b633643b43b633643b43b633c4af21db19b29daf02dbc95b68333e3c87439873a1f10f19d090010d19d090010d19d090010d19d09001fd9f54cb7f89015de4365ef13f4993af575eac3c0da8011dc6745267c13f1afbe660c92ec48ae63887274a2f7f75fe9c4fc62ba38ffe73c9d8b591e7474bd7cbff001a0533a4d8250000"
deData = gzip.decompress(bytes.fromhex(hex1))
hexStr3 = deData.hex()
print(hexStr3)
09300000000025d80331000000000000前16个字节
000025d8:第一个protobuf的字节长度+ 16 +第二个字节的protobuf长度
0331:第一个字节的protobuf的数据长度
08351228636f6d2e74656e63656e742e71716c6976652e70726f746f636f6c2e70622e6164536572766963651a352f636f6d2e74656e63656e742e71716c6976652e70726f746f636f6c2e70622e6164536572766963652f676574416444657461696c32850108b80810ed0f18b80322002a003a2e636630323664346566393966383334303966393835373137376334646662613864643430303031303231373531314210613235333334343638383833656532354a044d49203850015a203632343136653762656465
组装的代码如下所示:
rom appReverse.tencent.txlive.bytebuffer import ByteBuffer
import gzip
class Pbpa:
def __init__(self):
pass
#拼接gzip压缩前的头
def packagePBFrameHead(self, packageRequestHead_len,pbBusinessReqBytes_len):
allocate = ByteBuffer.allocate(16)
allocate.put_UBInt16(0x0930)
allocate.put_UBInt8(0)
allocate.put_UBInt8(0)
allocate.put_UBInt32(packageRequestHead_len +16 +pbBusinessReqBytes_len)
allocate.put_UBInt16(packageRequestHead_len)
allocate.put_UBInt16(0)
allocate.put_UBInt32(0)
logger.info((allocate._array).hex())
return allocate._array
def packageRequest(self):
#protobuf组包的第一个包
packageRequestHead = "123".encode()
#protobuf组包的第二个包
pbBusinessReqBytes = "456".encode()
allocate = ByteBuffer.allocate(len(packageRequestHead) + 16 +len(pbBusinessReqBytes))
headerdata = self.packagePBFrameHead(len(packageRequestHead) ,len(pbBusinessReqBytes))
allocate.put(headerdata)
allocate.put_bytes(packageRequestHead)
allocate.put_bytes(pbBusinessReqBytes)
logger.info((allocate._array).hex())
#gzip压缩
deData = gzip.compress(allocate._array)
logger.info(deData.hex())
return
组装request 请求的二进制的头
上面得到的数据就是 中间的数据 ;
完整的请求数据应该是
头数据 + gzip压缩后的数据 + 0x03
头数据的组装涉及到
import gzip
from loguru import logger
from appReverse.tencent.txlive.bytebuffer import ByteBuffer
import hexdump
class QmfProtocolTools:
def __init__(self):
pass
def packageQmfRequest(gzipdata=b"helloword"):
CMD = 65281
QMF_PB_CMD = 0x626c
allocate = ByteBuffer.allocate(9999)
allocate.put_UBInt8(19)
allocate.put_UBInt32(0)
allocate.put_UBInt16(1)
CMD = 65281
allocate.put_UBInt16(CMD)
allocate.put_UBInt16(QMF_PB_CMD)
allocate.put_UBInt16(0)
getRequestId = 0x35
allocate.put_UBInt64(getRequestId)
allocate.put_UBInt32(531)
QmfAppId = 0x271c
allocate.put_UBInt32(QmfAppId)
LoginQQUin =0x0
allocate.put_UBInt64(LoginQQUin)
allocate.put_UBInt64(0x0)
allocate.put_UBInt64(0x0)
allocate.put_UBInt64(0x0)
allocate.put_UBInt64(0x0)
allocate.put_UBInt16(0x0100)
allocate.put_UBInt64(0x0)
allocate.put_UBInt32(0x0)
allocate.put_UBInt16(0x0)
#这个位置压入四字节的长度
allocate.put_UBInt32(len(gzipdata))
enData = gzip.compress(gzipdata)
allocate.put_bytes(enData)
#压入结尾
allocate.put_UBInt8(3)
hexdump.hexdump(bytes.fromhex((allocate._array).hex()[:allocate._position *2]))
logger.info((allocate._array).hex()[:allocate._position *2])
if __name__ == '__main__':
QmfProtocolTools.packageQmfRequest()
解析gzip压缩后的protobuf:
第一个protobuf:
package com.tencent.qqlive.protocol.vb.pb; =>RequestHead
通过还原的第一部分的protobuf的可以确认第一个为这个
那么我们将其还原
第二个protobuf
根据第一个protobuf
可以判断出来走的是这个接口,那么我们直接去寻找
在
剩下的都是苦力活了 自己拼装probuf结构即可,真是个苦力活。。
然后组装完了发包,搞定
总结:
加大通信逻辑的复杂度也会增长分析的时间,如果再加上一些vmp就真的没得看了。
我是BestToYou,分享工作或日常学习中关于二进制逆向和分析的一些思路和一些自己闲暇时刻调试的一些程序,文中若有错误的地方,恳请大家联系我批评指正。
原文始发于微信公众号(二进制科学):某xun视频通信协议逆向分析
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论