[0821 | google play] 99$ Supergrok Heavy 复活 完整方案

Sophomores 2026-08-21 03:59 1

https://linux.do/t/topic/2785654


书接上文


昨天晚上无意间跑通的, 刚刚在mumu复现了几次, 自己订购了几个玩玩, 目前还在测试存活时间, 不确定是否会封号, 谨慎使用.



原理


Play给grok.ultra下发了几档offer,同一份 ProductDetails 里就有:




























offerId tag 价格
heavy-p3m-30-may2026 discount $99.99/月 ×3,然后 $299.99
heavy-p3m-30-churn-mar2026 pay1-get2free $299.99 / 3月 (1付3)
空 offerId $299.99/月 (CTA默认)

Grok点「Upgrade to SuperGrok Heavy」时,默认把空 offerId 的基价 p1m token塞进SKU_OFFER_ID_TOKEN_LIST,所以无 hook 时墙是 Starting today $299.99。


Frida 在 getBuyIntentExtraParams 对应的混淆方法里把 list 换成 heavy-p3m-30-may2026 的活 offerIdToken,Play 就按 intro 出墙。


无 token 的官方 self-replace proto 会 unable to change plan,那条是死路。


环境



  • MuMu Android 12,1080×1920

  • Google 已登录 + Grok 登录

  • FClash tun,GLOBAL 美国节点。

  • gsm.operator.iso-country=us

  • Private DNS = off

  • Grok 可 sideload。必须 pm disable ai.x.grok/com.pairip.licensecheck.LicenseActivity,否则约 5s 被打去 Play「Get this app from Play」

  • 设备 frida-server 与 host Frida 同一大版本 (本文 17.15.5)。MuMu 上常见 /data/local/tmp/fs17


出口自检 (Play uid 以机器为准,常见 10045):


adb shell settings put global private_dns_mode off
# 必须 US。不要用 su 0 curl
adb shell su 0 sh -c 'su 10045 /system/bin/sh -c "curl -s --connect-timeout 8 http://208.95.112.1/json"'

应用 uid 若 curl: (6) Could not resolve host,目录会永远转圈,没有 SuperGrok/Plus/Heavy 价格 tab。先修 DNS,再谈 hook。


Hook点


Grok 1.2.19 混淆后:


com.google.android.gms.internal.play_billing.d.d2(
int, String, String, String, android.os.Bundle)

sku == grok.ultra 时:


extra.putStringArrayList("SKU_OFFER_ID_TOKEN_LIST", [introOfferIdToken])

活 token 从 ProductDetails 包装类 mv.s 里读: b() 是 productId,字段 a / f42182a 是原始 JSON,选 offerId=heavy-p3m-30-may2026offerIdToken


禁止在 d.d2Java.choose heap walk 拖死 binder,Play 报 Unable to buy item, Error response code: -1。pick 只能放在 CTA 前 (脚本里 delay 10s)。


避免出现订阅闪退


sideload Grok disable 掉 PairIP LicenseActivity 之后有两条杀进程,只拦 System.exit 不够:



  1. 约 30s: LicenseClient.startPaywallActivity 因 Activity 被 disable 失败,然后 Java System.exit(0)。日志:


ai.x.grok: System.exit called, status: 0
VM exiting with result code 0
WIN DEATH: .../ProxyBillingActivity

必须同时拦:



  • java.lang.System.exit

  • java.lang.Runtime.exit / halt

  • com.android.billingclient.api.ProxyBillingActivity.finish (只拦这一个。全局拦 Activity.finish 会让第二次 billing 起不来)



  1. 更久 (实测约 25 分钟): licensing 服务断开:


LicenseClient.onServiceDisconnected
→ retryOrThrow → handleError → startErrorDialogActivity
ActivityNotFoundException: LicenseActivity
FATAL EXCEPTION: main

必须 no-op startErrorDialogActivity / startPaywallActivity / handleError,并吞掉未捕获异常里带 LicenseActivity / pairip 的。03:18 出墙、03:20 还在,是因为当时还没走到这条;保活进程后来就是这样 FATAL 的。


还有几个死法:



  • Frida 17 的 stdin 一 EOF,session 就没了。挂 hook 的进程 stdin 要保持打开,不要 frida -q,不要脚本跑完就退。

  • CTA 后别 uiautomator dump,会 Purchase cancelled。只 screencap。

  • 别在 d.d2Java.choose

  • 别 no-op Process.killProcess 当保活,Grok 会坏。

  • Close 是 content-desc,不是 text。按 text=Close 永远点不到。


步骤


# 0. frida-server,和 host 同版本
adb shell su 0 sh -c 'setsid /data/local/tmp/fs17 -D >/data/local/tmp/fs17.log 2>&1 < /dev/null &'
frida --version # 17.15.5

# 1. 禁 PairIP + 关 Private DNS
adb shell su 0 pm disable ai.x.grok/com.pairip.licensecheck.LicenseActivity
adb shell settings put global package_verifier_enable 0
adb shell settings put global private_dns_mode off

# 2. 起 Grok attach hook(这条 frida 进程别退,stdin 别关)
adb shell am force-stop ai.x.grok
adb shell am start -n ai.x.grok/.main.GrokActivity
PID=$(adb shell pidof ai.x.grok)
frida -D $SERIAL -p $PID -l inject_p3m99.js

# 3. 等日志 READY tap Heavy CTA
# 4. 点 Try SuperGrok
# 5. 等出 SuperGrok / Plus / Heavy 和美元价格(别在转圈/skeleton 时点)
# 6. Heavy → Upgrade to SuperGrok Heavy
# 7. 停手。只截图,不 dump,不 BACK,不 Subscribe

1080×1920 备用坐标 (仍以 CTA 前 dump 活节点为准):
























控件 坐标
Try SuperGrok 289, 1547
Heavy tab 737, 534
Upgrade to SuperGrok Heavy 540, 1761

日志要对:


★SWAPPED src=live|fallback heavy-p3m-30-may2026
block System.exit 0

墙要对: SuperGrok Heavy + Starting today $99.99/month + 约 +3 个月 $299.99/month。没有 Get this app from Play。


Frida脚本 inject_p3m99.js


'use strict';
function log(m) { send({ t: 'log', m: String(m) }); }

var WANT = 'heavy-p3m-30-may2026';
var FALLBACK = 'AXW7mMuPIEozWT2ukTJE+PRd7/l51pBgfomUPEBu0rukdzfryp0nwkafBsOKevhe/JrwIjnJ0MZi0Wjwzb+9D1iC5RpfT3Gcbie/PHXhf6Sg/JoK';
var liveToken = null;

function field(obj, names) {
var cls = obj.getClass();
while (cls !== null) {
var flds = cls.getDeclaredFields();
for (var i = 0; i < flds.length; i++) {
var n = flds[i].getName();
for (var j = 0; j < names.length; j++) {
if (n === names[j]) {
flds[i].setAccessible(true);
return flds[i].get(obj);
}
}
}
cls = cls.getSuperclass();
}
return null;
}

function pickUltraOffer() {
var JSONObject = Java.use('org.json.JSONObject');
var found = null;
Java.choose('mv.s', {
onMatch: function (inst) {
if (found) return;
var pid = '';
try { pid = String(inst.b()); } catch (e0) {}
if (pid !== 'grok.ultra') return;
var raw = '';
try { raw = String(field(inst, ['a', 'f42182a'])); } catch (e1) {}
if (!raw) return;
try {
var jo = JSONObject.$new(raw);
var arr = jo.optJSONArray('subscriptionOfferDetails');
if (!arr) return;
for (var i = 0; i < arr.length(); i++) {
var o = arr.getJSONObject(i);
var oid = o.optString('offerId', '');
var tok = o.optString('offerIdToken', '');
log('[p3m99] ultra offerId=' + oid + ' tokenLen=' + (tok ? tok.length : 0));
if (oid === WANT && tok) found = tok;
}
} catch (e2) {
log('[p3m99] parse ' + e2);
}
},
onComplete: function () {}
});
return found;
}

Java.perform(function () {
log('[p3m99] start ' + WANT);
try {
var d = Java.use('com.google.android.gms.internal.play_billing.d');
var ov = d.d2.overload('int', 'java.lang.String', 'java.lang.String', 'java.lang.String', 'android.os.Bundle');
ov.implementation = function (api, pkg, sku, type, extra) {
try {
var tok = liveToken || FALLBACK;
if (String(sku) === 'grok.ultra' && extra && tok) {
var ArrayList = Java.use('java.util.ArrayList');
var list = ArrayList.$new();
list.add(tok);
extra.putStringArrayList('SKU_OFFER_ID_TOKEN_LIST', list);
log('[p3m99] ★SWAPPED src=' + (liveToken ? 'live' : 'fallback') + ' ' + WANT);
}
} catch (eD) {
log('[p3m99] d2 wrap ' + eD);
}
return ov.call(this, api, pkg, sku, type, extra);
};
log('[p3m99] hooked d.d2');
} catch (e) {
log('[p3m99] d2 fail ' + e);
}
try {
var Sys = Java.use('java.lang.System');
Sys.exit.implementation = function (code) {
log('[p3m99] block System.exit ' + code);
};
} catch (eS) {}
try {
var RT = Java.use('java.lang.Runtime');
RT.exit.implementation = function (code) { log('[p3m99] block Runtime.exit ' + code); };
RT.halt.implementation = function (code) { log('[p3m99] block Runtime.halt ' + code); };
} catch (eR) {}
try {
var PBA = Java.use('com.android.billingclient.api.ProxyBillingActivity');
PBA.finish.overload().implementation = function () {
log('[p3m99] block ProxyBillingActivity.finish');
};
} catch (eP) {}
try {
var LC = Java.use('com.pairip.licensecheck.LicenseClient');
var names = ['startErrorDialogActivity', 'startPaywallActivity', 'handleError'];
for (var ni = 0; ni < names.length; ni++) {
(function (name) {
if (!LC[name]) return;
var ovs = LC[name].overloads;
for (var oi = 0; oi < ovs.length; oi++) {
ovs[oi].implementation = function () {
log('[p3m99] block LicenseClient.' + name);
};
}
})(names[ni]);
}
} catch (eL) {}
try {
var Thread = Java.use('java.lang.Thread');
var Handler = Java.use('java.lang.Thread$UncaughtExceptionHandler');
var prev = Thread.getDefaultUncaughtExceptionHandler();
var Wrap = Java.registerClass({
name: 'x.p3m99.Ueh',
implements: [Handler],
methods: {
uncaughtException: function (t, e) {
var s = String(e);
if (s.indexOf('LicenseActivity') !== -1 || s.indexOf('pairip') !== -1) {
log('[p3m99] swallow ' + s.substring(0, 160));
return;
}
if (prev) prev.uncaughtException(t, e);
}
}
});
Thread.setDefaultUncaughtExceptionHandler(Wrap.$new());
} catch (eU) {}
log('[p3m99] READY tap Heavy CTA');
});

setTimeout(function () {
Java.perform(function () {
try {
var t = pickUltraOffer();
if (t) liveToken = t;
} catch (e) {}
});
}, 10000);

CTA 后不再 dump。父进程 sleep 保 Frida stdin。需要 ANDROID_SERIALinject_p3m99.js 和本脚本放同一目录。


#!/usr/bin/env python3
from __future__ import annotations
import os, re, signal, subprocess, time
from pathlib import Path

os.environ.setdefault("ADB_SERVER_SOCKET", "tcp:127.0.0.1:5038")
ADB = os.environ.get("ADB", "adb")
HERE = Path(__file__).resolve().parent
CACHE = Path.home() / ".cache/grok-heavy-offer"
SCRIPT = HERE / "inject_p3m99.js"
LICENSE = "ai.x.grok/com.pairip.licensecheck.LicenseActivity"

def serial() -> str:
s = os.environ.get("ANDROID_SERIAL") or os.environ.get("MUMU_SERIAL")
if not s:
raise SystemExit("ANDROID_SERIAL required")
return s

def adb(ser, *a, timeout=20):
r = subprocess.run([ADB, "-s", ser, *a], capture_output=True, timeout=timeout)
return (r.stdout or b"").decode("utf-8", "replace")

def grok_pid(ser):
return (adb(ser, "shell", "pidof", "ai.x.grok").strip().split() or [""])[0]

def dump_xml(ser):
try:
subprocess.run([ADB, "-s", ser, "shell", "uiautomator", "dump", "/sdcard/_p3m99.xml"], timeout=8, capture_output=True)
dest = CACHE / "ui.xml"
subprocess.run([ADB, "-s", ser, "pull", "/sdcard/_p3m99.xml", str(dest)], timeout=8, capture_output=True)
return dest.read_text(encoding="utf-8", errors="replace") if dest.exists() else ""
except Exception as e:
return f"ERR {e}"

def nodes(xml):
out = []
for m in re.finditer(r"<node [^>]*>", xml):
s = m.group(0)
def g(k, s=s):
mm = re.search(k + r'="([^"]*)"', s)
return mm.group(1) if mm else ""
b = g("bounds")
mm = re.match(r"\[(\d+),(\d+)\]\[(\d+),(\d+)\]", b)
cx = cy = y1 = 0
if mm:
x1, y1, x2, y2 = map(int, mm.groups())
cx, cy = (x1 + x2) // 2, (y1 + y2) // 2
out.append({"text": g("text"), "cx": cx, "cy": cy, "y1": y1})
return out

def find_text(xml, label, y_lt=None, exact=False):
for n in nodes(xml):
t = (n["text"] or "").strip()
if exact and t != label:
continue
if not exact and label not in t:
continue
if y_lt is not None and n["y1"] >= y_lt:
continue
return n
return None

def tap(ser, x, y, wait=1.0):
adb(ser, "shell", "input", "tap", str(x), str(y))
time.sleep(wait)

def main():
signal.signal(signal.SIGHUP, signal.SIG_IGN)
ser = serial()
CACHE.mkdir(parents=True, exist_ok=True)
log_path = CACHE / "stay_wall.log"
adb(ser, "shell", "su", "0", "pm", "disable", LICENSE)
adb(ser, "shell", "settings", "put", "global", "package_verifier_enable", "0")
adb(ser, "shell", "settings", "put", "global", "private_dns_mode", "off")
adb(ser, "shell", "am", "force-stop", "ai.x.grok")
time.sleep(0.5)
adb(ser, "shell", "am", "start", "-n", "ai.x.grok/.main.GrokActivity")
pid = ""
for _ in range(24):
time.sleep(0.35)
pid = grok_pid(ser)
if pid:
break
if not pid:
return 2
logf = open(log_path, "w", buffering=1)
proc = subprocess.Popen(
["frida", "-D", ser, "-p", pid, "-l", str(SCRIPT)],
stdin=subprocess.PIPE, stdout=logf, stderr=subprocess.STDOUT, start_new_session=True,
)
t0 = time.time()
while time.time() - t0 < 22:
txt = log_path.read_text(errors="replace")
if "READY tap Heavy CTA" in txt:
break
if proc.poll() is not None:
return 3
time.sleep(0.25)
else:
return 3
for i in range(10):
time.sleep(0.5)
xml = dump_xml(ser)
hit = find_text(xml, "Try SuperGrok")
if hit:
tap(ser, hit["cx"], hit["cy"], 2.0)
break
else:
tap(ser, 289, 1547, 2.0)
xml = ""
for i in range(12):
time.sleep(1.4)
xml = dump_xml(ser)
heavy = find_text(xml, "Heavy", y_lt=900, exact=True)
plus = find_text(xml, "Plus", exact=True)
blob = " ".join(n["text"] for n in nodes(xml))
if heavy and plus and ("$" in blob or "299" in blob):
break
else:
return 5
heavy = find_text(xml, "Heavy", y_lt=900, exact=True)
tap(ser, heavy["cx"] if heavy else 737, heavy["cy"] if heavy else 534, 1.8)
xml = dump_xml(ser)
cta = None
for n in nodes(xml):
if "upgrade to supergrok heavy" in (n["text"] or "").lower():
cta = n
break
tap(ser, cta["cx"] if cta else 540, cta["cy"] if cta else 1761, 0.4)
time.sleep(3.0)
subprocess.run([ADB, "-s", ser, "exec-out", "screencap", "-p"], capture_output=True, timeout=20)
while proc.poll() is None:
time.sleep(15)
return proc.returncode or 0

if __name__ == "__main__":
raise SystemExit(main())

FClash Android VPN 的 DNS 是 172.19.0.2。生成 yaml 里经常 dns.listen: ""。再把 OUTPUT 53 REDIRECT 到 7874 且 7874 没人听 = 所有 hostname 失败。


设备上跑 UDP 转发听 0.0.0.0:7874,上游 8.8.8.8 / 1.1.1.1 / 223.5.5.5。iptables 必须先 RETURN 这几个上游,再 REDIRECT 其余 53,否则转发自己打回环。


dnsfwd.c (aarch64 无 libc):


/* UDP DNS 0.0.0.0:7874 -> 8.8.8.8 / 1.1.1.1 / 223.5.5.5 */
enum {
SYS_write = 64, SYS_close = 57, SYS_exit = 93, SYS_socket = 198, SYS_bind = 200,
SYS_setsockopt = 208, SYS_sendto = 206, SYS_recvfrom = 207, SYS_fcntl = 25,
SYS_clock_nanosleep = 115
};
static long sc(long n, long a, long b, long c, long d, long e, long f) {
register long x8 asm("x8") = n;
register long x0 asm("x0") = a;
register long x1 asm("x1") = b;
register long x2 asm("x2") = c;
register long x3 asm("x3") = d;
register long x4 asm("x4") = e;
register long x5 asm("x5") = f;
asm volatile("svc #0" : "+r"(x0) : "r"(x8), "r"(x1), "r"(x2), "r"(x3), "r"(x4), "r"(x5) : "memory");
return x0;
}
struct sockaddr_in {
unsigned short sin_family;
unsigned short sin_port;
unsigned int sin_addr;
unsigned char sin_zero[8];
};
static unsigned short b2(unsigned short x) { return (unsigned short)((x << 8) | (x >> 8)); }
static unsigned int ip4(unsigned a, unsigned b, unsigned c, unsigned d) {
return a | (b << 8) | (c << 16) | (d << 24);
}
static void wr(const char *s) {
long n = 0; while (s[n]) n++;
sc(SYS_write, 1, (long)s, n, 0, 0, 0);
}
static void nap_ms(long ms) {
struct { long sec; long nsec; } ts;
ts.sec = ms / 1000;
ts.nsec = (ms % 1000) * 1000000L;
sc(SYS_clock_nanosleep, 1, 0, (long)&ts, 0, 0, 0);
}
void _start(void) {
int fd = (int)sc(SYS_socket, 2, 2, 17, 0, 0, 0);
if (fd < 0) { wr("socket fail\n"); sc(SYS_exit, 1, 0, 0, 0, 0, 0); }
int one = 1;
sc(SYS_setsockopt, fd, 1, 2, (long)&one, 4, 0);
struct sockaddr_in loc; long i;
for (i = 0; i < (long)sizeof(loc); i++) ((char *)&loc)[i] = 0;
loc.sin_family = 2; loc.sin_port = b2(7874); loc.sin_addr = 0;
if (sc(SYS_bind, fd, (long)&loc, 16, 0, 0, 0) < 0) {
wr("bind 7874 fail\n"); sc(SYS_exit, 2, 0, 0, 0, 0, 0);
}
unsigned int ups[3] = { ip4(8,8,8,8), ip4(1,1,1,1), ip4(223,5,5,5) };
char buf[2048];
for (;;) {
struct sockaddr_in cli;
for (i = 0; i < (long)sizeof(cli); i++) ((char *)&cli)[i] = 0;
int clen = 16;
long n = sc(SYS_recvfrom, fd, (long)buf, 2048, 0, (long)&cli, (long)&clen);
if (n <= 0) continue;
int ufd = (int)sc(SYS_socket, 2, 2, 17, 0, 0, 0);
if (ufd < 0) continue;
sc(SYS_fcntl, ufd, 4, 2048, 0, 0, 0);
long got = -1; int u;
for (u = 0; u < 3 && got <= 0; u++) {
struct sockaddr_in up;
for (i = 0; i < (long)sizeof(up); i++) ((char *)&up)[i] = 0;
up.sin_family = 2; up.sin_port = b2(53); up.sin_addr = ups[u];
sc(SYS_sendto, ufd, (long)buf, n, 0, (long)&up, 16);
int t;
for (t = 0; t < 40 && got <= 0; t++) {
struct sockaddr_in from; int flen = 16;
got = sc(SYS_recvfrom, ufd, (long)buf, 2048, 0, (long)&from, (long)&flen);
if (got > 0) break;
nap_ms(50);
}
}
sc(SYS_close, ufd, 0, 0, 0, 0, 0);
if (got > 0) sc(SYS_sendto, fd, (long)buf, got, 0, (long)&cli, clen > 0 ? clen : 16);
}
}

编译推送:


aarch64-elf-gcc -nostdlib -static -Os -fno-builtin -fno-stack-protector -o dnsfwd dnsfwd.c
adb push dnsfwd /data/local/tmp/dnsfwd
adb shell su 0 chmod 755 /data/local/tmp/dnsfwd
adb shell su 0 sh -c 'setsid /data/local/tmp/dnsfwd >/data/local/tmp/dnsfwd.log 2>&1 < /dev/null &'

iptables (设备上 su 0):


iptables -t nat -F OUTPUT
iptables -t nat -A OUTPUT -p udp --dport 53 -d 8.8.8.8 -j RETURN
iptables -t nat -A OUTPUT -p udp --dport 53 -d 1.1.1.1 -j RETURN
iptables -t nat -A OUTPUT -p udp --dport 53 -d 223.5.5.5 -j RETURN
iptables -t nat -A OUTPUT -p tcp --dport 53 -d 8.8.8.8 -j RETURN
iptables -t nat -A OUTPUT -p tcp --dport 53 -d 1.1.1.1 -j RETURN
iptables -t nat -A OUTPUT -p udp --dport 53 -j REDIRECT --to-ports 7874
iptables -t nat -A OUTPUT -p tcp --dport 53 -j REDIRECT --to-ports 7874
最新回复 (19)
  • llmw 08-21 04:12
    1

    看上去操作好像有点复杂呀。目前不是刚需,不弄了。

  • baoge418 08-21 04:25
    2

    我确实有点看不懂,头大了,太复杂了

  • apparition 08-21 04:32
    3

    看着手里的两个 supergrok

    算了算了,这次先不跟 ^-^

  • Russell 08-21 05:09
    4

    这个复杂程度高的有点狠了,弄不来,而且这种渠道的应该活不长吧

  • A2401314 08-21 05:45
    5

    两位逆向技术大神的研究,强行把已下架的优惠码用android硬塞进去,长知识了!S佬什么时候更新下GA的工作流呀,期待学习

  • 晦涩男董 08-21 07:15
    6

    持续关注, 坐等下周, 跪求佬 来个号~

  • 嘴硬 08-21 07:32
    7

    佬还是一如既往的权威,刚睡醒又来波大的

  • QQbot 08-21 07:53
    8

    这个还能卡一年吗?谷歌play申诉退款容易吗

  • pangtl 08-21 07:54
    9

    有点厉害了,老马还是差了一点儿,不如直接放开这个优惠吧,普惠大众

  • 长夜 08-21 07:54
    10

    这个本质其实算漏洞攻击,而不算薅羊毛了。做生意或者研究着玩的土豪的可以。自用号还是悠着点,别搞,牢马封得你们飞起来。笑死。




    额外,大佬逆向牛皮(超大声)!~

  • 铁傀儡 08-21 07:55
    11

    很详细啊佬,厉害了,就是怕等下秋后算账钱不给退了

  • ruyu 08-21 07:58
    12

    太细了大佬,去公司就让gpt给我搞起来。先冲个99看看老马反应

  • 轨迹 08-21 08:42
    13

    佬,这是真牛逼。。。。怎么研究出来的?

  • ccicc 08-21 08:51
    14

    太棒了,收藏了准备后面去试一下看看行不行

  • hui xu 08-21 08:52
    15

    google play现在就是畜生,只要过他手的钱,咋都不退

  • jiejiejing 08-21 09:09
    16

    佬真是手把手教学了,真是太细节了,佬的逆向太牛了。

  • Wengfb 08-21 09:31
    17

    看不懂看不懂,只知道佬的逆向太牛了

  • sereno 08-21 09:41
    18

    nb, 菜鸡投来羡慕的目光

    还得是你啊佬

  • starlight312 08-21 10:02
    19

    现在还送cursor ultra吗

* 帖子来源Linux.do
返回