Preface
Lately I’ve been scrolling Banciyuan during meals. The Cosplay section is full of great photos, but unfortunately many images have watermarks, and some can’t be downloaded. It’s hard to save them for later, which really ruins the mood.
I also often forward these photos to friends. But with the watermark, everyone immediately figured out I was obsessed with Banciyuan. Annoying. So I decided to bypass these restrictions.
Packet capture analysis
Banciyuan uses HTTPS, and it also uses certificate pinning (HPKP). With the Xposed framework’s JustTrustMe module, we can bypass certificate verification.
So I captured traffic with Burp Suite.

From the response, you can tell that download=false indicates downloading is forbidden. The image URL has an imageMogr2 suffix, which is Qiniu’s advanced image processing feature. Since both of these are hard-coded in the HTTP response, another decent approach would be to hook and modify the response content directly.

Reverse engineering the code path

I opened Banciyuan’s APK with JADX-GUI and found the code is obfuscated. Network requests are implemented with the Volley framework.
According to the official docs, to make a request you set up a RequestQueue, then handle the response in onResponse.
RequestQueue mRequestQueue;
// Instantiate the cache
Cache cache = new DiskBasedCache(getCacheDir(), 1024 * 1024); // 1MB cap
// Set up the network to use HttpURLConnection as the HTTP client.
Network network = new BasicNetwork(new HurlStack());
// Instantiate the RequestQueue with the cache and network.
mRequestQueue = new RequestQueue(cache, network);
// Start the queue
mRequestQueue.start();
String url ="http://www.example.com";
// Formulate the request and handle the response.
StringRequest stringRequest = new StringRequest(Request.Method.GET, url,
new Response.Listener<String>() {
@Override
public void onResponse(String response) {
// Do something with the response
}
},
new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
// Handle error
}
});
// Add the request to the RequestQueue.
mRequestQueue.add(stringRequest);
// ...
Based on what we saw in the captured traffic, we can extract these keywords:
image/postCover
download
multi
path
Using image/postCover from the URL as a keyword and searching globally, it’s wrapped inside the m1750b method. Then we locate the code that calls it.

public void m6896a(DetailType detailType, final C2271b c2271b) {
if (detailType != null) {
final Context context = (Context) this.f6216a.get();
if (context != null) {
// image/postCover 字符串拼接
String str = HttpUtils.f7520b + C0701m.m1750b();
List arrayList = new ArrayList();
arrayList.add(new C2753c("session_key", C0766a.m2185b(context).getToken()));
arrayList.add(new C2753c("id", detailType.getItem_id()));
arrayList.add(new C2753c("type", detailType.getType()));
this.f6217b.add(new C2764l(1, str, HttpUtils.m8132a(arrayList), new Listener<String>(this) {
/* renamed from: c */
final /* synthetic */ C2296a f6186c;
public /* synthetic */ void onResponse(Object obj) {
m6855a((String) obj);
}
/* renamed from: a */
public void m6855a(String str) {
// 判断 status 是否为1,如果为1则执行mo2699a。
if (C2763k.m8211a(str, context).booleanValue()) {
c2271b.mo2699a(str);
} else {
c2271b.mo2700b("");
}
}
}, new ErrorListener(this) {
/* renamed from: b */
final /* synthetic */ C2296a f6188b;
public void onErrorResponse(VolleyError volleyError) {
c2271b.mo2700b("");
}
}));
}
}
}
In com.banciyuan.bcywebview.biz.picshow.ViewPictureActivity2, I found the method named mo2699a, but it’s inside an anonymous inner class (closure), which isn’t convenient to hook directly.
protected void mo2523h() {
new C2296a(this).m6896a(this.f6170i, new C2271b(this) {
/* renamed from: a */
final /* synthetic */ ViewPictureActivity2 f6149a;
{
this.f6149a = r1;
}
/* renamed from: a */
public void mo2699a(String str) {
try {
String string = new JSONObject(str).getString("data");
this.f6149a.f6172k = (OrignPic) new Gson().fromJson(string, OrignPic.class);
this.f6149a.m6848w();
} catch (Exception e) {
this.f6149a.m6847v();
}
}
/* renamed from: b */
public void mo2700b(String str) {
this.f6149a.m6847v();
}
});
}
To make it cleaner, I wanted to hook where downloading actually happens, rather than hooking every request. Scrolling further down, I found the m6849x method.
private void m6849x() {
int i = 0;
while (i < this.f6171j.size()) {
Fragment c2290a = new C2290a();
Bundle bundle = new Bundle();
if (i < this.f6172k.getMultis().size() && m6832c(((Multi) this.f6172k.getMultis().get(i)).getPath()).booleanValue()) {
bundle.putBoolean("is_big", true);
// 可以看到在getMultis方法后面取值然后接上了getPath方法,和抓包过程中的Response层级一致
this.f6171j.set(i, ((Multi) this.f6172k.getMultis().get(i)).getPath());
this.f6164c.put(i, true);
if (i == this.f6178q) {
m6833c(8);
}
}
bundle.putString("path", (String) this.f6171j.get(i));
bundle.putInt("index", i);
bundle.putSerializable("uname", this.f6180s);
bundle.putBoolean("water_mark", this.f6181t);
c2290a.setArguments(bundle);
this.f6169h.add(c2290a);
i++;
}
this.f6165d.setAdapter(new C2282a(this, getSupportFragmentManager()));
this.f6165d.setCurrentItem(this.f6178q);
this.f6179r = this.f6171j.size();
this.f6166e.setText((this.f6178q + 1) + "/" + this.f6179r);
this.f6166e.setVisibility(0);
}
In this method, it doesn’t assign values via something like setPath; instead it first stores things into a Bundle, then assigns a batch of properties later. Hooking here would be ugly because we’d need to add checks for every assignment.
Since there are getMultis and getPath, there must also be a method that checks whether downloading is allowed. I continued searching globally and noticed the app uses the greenDAO ORM framework. In de.greenrobot.daoexample, I found the key methods. If we modify the methods below, we can bypass the restriction.

de.greenrobot.daoexample.model.Multi.getPath()
de.greenrobot.daoexample.model.PostItem.getPath()
de.greenrobot.daoexample.model.OrignPic.isDownload()
Frida hook
First, use Frida to validate whether hooking these methods is feasible. This machine didn’t have Frida installed yet, so install it first.
sudo pip install frida-tools
wget https://github.com/frida/frida/releases/download/12.0.7/frida-server-12.0.7-android-arm64.xz
xz -d frida-server-12.0.7-android-arm64.xz
Run Frida Server on the Android device:
adb push frida-server-12.0.7-android-arm64 /data/local/tmp
adb shell su -c "/data/local/tmp/frida-server-12.0.7-android-arm64"
Use frida-ps to check the process name:
frida-ps aU | grep banciyuan
半次元 com.banciyuan.bcywebview
Write a debug script to hook the methods and change the logic, saved as bcy.js.
setTimeout(function() {
Java.perform(function() {
var pathRe = /\?imageMogr2.*$/g;
var Multi = Java.use("de.greenrobot.daoexample.model.Multi");
Multi.getPath.implementation = function() {
var path = this.getPath().replace(pathRe, '');
console.log(path);
return path;
}
var PostItem = Java.use("de.greenrobot.daoexample.model.PostItem");
PostItem.getPath.implementation = function() {
var path = this.getPath().replace(pathRe, '');
console.log(path);
return path;
}
var OrignPic = Java.use("de.greenrobot.daoexample.model.OrignPic");
OrignPic.isDownload.implementation = function() {
console.log(this.isDownload());
return true;
}
});
}, 0);
Run the debug script:
frida -U -f com.banciyuan.bcywebview -l bcy.js --no-pause
The normal preview still has a watermark, but the original image watermark is removed, the download restriction is bypassed, and the image saved to disk has no watermark.

Xposed module
Code only:
public class Main implements IXposedHookLoadPackage {
@Override
public void handleLoadPackage(final XC_LoadPackage.LoadPackageParam loadPackageParam) throws Throwable {
if (!loadPackageParam.packageName.equals("com.banciyuan.bcywebview"))
return;
findAndHookMethod("de.greenrobot.daoexample.model.Multi", loadPackageParam.classLoader, "getPath", new XC_MethodReplacement() {
@Override
protected Object replaceHookedMethod(MethodHookParam param) throws Throwable {
try {
String path = (String) XposedHelpers.getObjectField(param.thisObject, "path");
int imageMogrIndex = path.indexOf("?");
if (imageMogrIndex > 1) {
path = path.substring(0, imageMogrIndex);
}
return path;
} catch (Throwable t) {
XposedBridge.log(t);
return "";
}
}
});
findAndHookMethod("de.greenrobot.daoexample.model.PostItem", loadPackageParam.classLoader, "getPath", new XC_MethodReplacement() {
@Override
protected Object replaceHookedMethod(MethodHookParam param) throws Throwable {
try {
XposedBridge.log("PostItem");
String path = (String) XposedHelpers.getObjectField(param.thisObject, "path");
int imageMogrIndex = path.indexOf("?");
if (imageMogrIndex > 1) {
path = path.substring(0, imageMogrIndex);
}
return path;
} catch (Throwable t) {
XposedBridge.log(t);
return "";
}
}
});
findAndHookMethod("de.greenrobot.daoexample.model.OrignPic", loadPackageParam.classLoader, "isDownload", new XC_MethodReplacement() {
@Override
protected Object replaceHookedMethod(MethodHookParam param) throws Throwable {
return true;
}
});
}
}
GitHub:
https://github.com/gorgiaxx/bcyhelper