Sekai’s Bank - Signature
Let me introduce you to Sekai Bank!
Static Analysis using Jadx
I started analyze apk file using Jadx GUI. I loaded the apk file and started exploring the code.
package com.sekai.bank;
import android.app.Application;import com.sekai.bank.network.ApiClient;import com.sekai.bank.utils.TokenManager;import com.sekai.bank.utils.delayed_transaction.DelayedTransactionManager;
/* loaded from: classes2.dex */public class SekaiApplication extends Application { private static SekaiApplication instance; private ApiClient apiClient; private TokenManager tokenManager;
@Override // android.app.Application public void onCreate() { super.onCreate(); instance = this; this.tokenManager = new TokenManager(this); this.apiClient = new ApiClient(this.tokenManager); initializeDelayedTransactionMonitoring(); }
private void initializeDelayedTransactionMonitoring() { new DelayedTransactionManager(this).startPeriodicChecking(); }
public static SekaiApplication getInstance() { return instance; }
public ApiClient getApiClient() { return this.apiClient; }
public TokenManager getTokenManager() { return this.tokenManager; }}From the SekaiApplication class, see that it initializes several components for the application, including the
ApiClientTokenManagerDelayedTransactionManager
Navigated to the ApiClient and get important information about baseURL:
public class ApiClient { private static final String BASE_URL = "https://sekaibank-api.chals.sekai.team/api/"; private static final int REFRESH_TIMEOUT_SECONDS = 3; private static final String TAG = "SekaiBank-API"; private static final int TIMEOUT_SECONDS = 30; private static final int TOKEN_TIMEOUT_SECONDS = 2; private final ApiService apiService; private final Retrofit retrofit; private final TokenManager tokenManager;And all endpoints:
public interface ApiService { @PUT("auth/pin/change") Call<ApiResponse<Void>> changePin(@Body PinRequest pinRequest);
@GET("user/search/{username}") Call<ApiResponse<User>> findUserByUsername(@Path("username") String str);
@GET("user/balance") Call<ApiResponse<BalanceResponse>> getBalance();
@POST("flag") Call<String> getFlag(@Body FlagRequest flagRequest);Looking into the FlagRequest class:
public class FlagRequest { private boolean unmask_flag;
public FlagRequest(boolean z) { this.unmask_flag = z; }
public boolean getUnmaskFlag() { return this.unmask_flag; }
public void setUnmaskFlag(boolean z) { this.unmask_flag = z; }}So we have to add unmask_flag to the request body, then craft a request to this endpoint with the appropriate payload to retrieve the flag.
Break down the request
Trying to send the request to /flag endpoint:
curl -X POST https://sekai-bank.ctf/api/flag -H "Content-Type: application/json"Got response: X-Signature is missing. Looking into how requests are handled, I found this class:
private class SignatureInterceptor implements Interceptor { ... @Override public Response intercept(Interceptor.Chain chain) throws IOException { Request request = chain.request(); try { return chain.proceed(request.newBuilder().header("X-Signature", generateSignature(request)).build()); } catch (Exception e) { Log.e(ApiClient.TAG, "Failed to generate signature: " + e.getMessage()); return chain.proceed(request); } }From here, X-Signature is generated using HMAC-SHA256, with:
- The request body
- The app’s signing certificate
as inputs. This value is placed in the header.
Get App Signature
apksigner verify --print-certs SekaiBank.apkSigner #1 certificate DN: C=ID, ST=Bali, L=Indonesia, O=HYPERHUG, OU=Development, CN=Aimar S. AdhityaSigner #1 certificate SHA-256 digest: 3f3cf8830acc96530d5564317fe480ab581dfc55ec8fe55e67dddbe1fdb605beSigner #1 certificate SHA-1 digest: 2c9760ee9615adabdee0e228aed91e3d4ebdebdfSigner #1 certificate MD5 digest: fcab4af1f7411b4ba70ec2fa915dee8eFrom the generateSignature method, SHA256 is used:
if (signingCertificateHistory != null && signingCertificateHistory.length > 0) { MessageDigest messageDigest = MessageDigest.getInstance("SHA-256"); for (Signature signature : signingCertificateHistory) { messageDigest.update(signature.toByteArray()); }Crafting X-Signature Header
The string used in HMAC is:
String str = request.method() + "/api".concat(getEndpointPath(request)) + getRequestBodyAsString(request);So for our case:
POST/api/flag{"unmask_flag":true}
Python script to generate the signature:
import hmac, hashlibkey_hex = "3f3cf8830acc96530d5564317fe480ab581dfc55ec8fe55e67dddbe1fdb605be" # from apksignerkey = bytes.fromhex(key_hex)msg = b'POST/api/flag{"unmask_flag":true}'print(hmac.new(key, msg, hashlib.sha256).hexdigest())Output:
440ba2925730d137259f297fd6fba02af2f7b6c414dd16a1ac336e9047cdb8f5Now final request:
curl -X POST "https://sekaibank-api.chals.sekai.team/api/flag" \ -H "Content-Type: application/json" \ -H "X-Signature: 440ba2925730d137259f297fd6fba02af2f7b6c414dd16a1ac336e9047cdb8f5" \ -d '{"unmask_flag":true}'And the flag is retrieved successfully.