# Reverse - Overall this is a pretty good quality tournament. ## NeuraCall (499 Points/7solved) ### Challenge analysis - It is exactly an ELF 64 file compiled in C/C++ ![image](https://hackmd.io/_uploads/ryOxqZ5RJg.png) - I will analyze it with IDA. - This `main()` functions. ```cpp= // local variable allocation has failed, the output may be wrong! __int64 __fastcall main(__int64 a1, char **a2, char **a3) { __int64 v4; // rcx __int64 v5; // r12 unsigned int v6; // [rsp+5h] [rbp-BBh] BYREF unsigned int v7; // [rsp+Ch] [rbp-B4h] BYREF int v8; // [rsp+10h] [rbp-B0h] BYREF _DWORD v9[5]; // [rsp+14h] [rbp-ACh] BYREF _DWORD v10[14]; // [rsp+28h] [rbp-98h] BYREF int v11; // [rsp+60h] [rbp-60h] BYREF int v12; // [rsp+64h] [rbp-5Ch] BYREF _BYTE v13[14]; // [rsp+68h] [rbp-58h] BYREF char v14; // [rsp+76h] [rbp-4Ah] BYREF _BYTE v15[2]; // [rsp+78h] [rbp-48h] BYREF char v16; // [rsp+7Ah] [rbp-46h] BYREF _BYTE v17[4]; // [rsp+7Ch] [rbp-44h] BYREF char v18; // [rsp+80h] [rbp-40h] BYREF char v19; // [rsp+84h] [rbp-3Ch] BYREF char v20; // [rsp+9Ch] [rbp-24h] BYREF _BYTE v21[24]; // [rsp+A0h] [rbp-20h] BYREF _BYTE var8[12]; // [rsp+B8h] [rbp-8h] OVERLAPPED BYREF *var8 = __readfsqword(0x28u); if ( sub_40147F(a1, a2, a3) ) { puts("Are you trying to cheat? Because I'm trying to cry."); return 1LL; } else { prctl(4, 0LL); sub_4013F2(); v8 = 103; v9[0] = 111; v9[1] = 109; v9[2] = 120; v9[3] = 115; v9[4] = 66; v10[0] = 114; v10[1] = 69; v10[2] = 108; v10[3] = 98; v10[4] = 69; v10[5] = 109; v10[6] = 99; v10[7] = 126; v10[8] = 69; v10[9] = 110; v10[10] = 126; v10[11] = 70; v10[12] = 69; v10[13] = 66; v11 = 118; v12 = 108; *v13 = 122; sub_401E6D(); qword_4051E0 = &v12; qword_4051E8 = &v11; qword_4051F0 = &v13[8]; qword_4051F8 = v13; qword_405200 = &v13[4]; qword_405208 = &v13[8]; qword_405210 = *&var8[4]; qword_405218 = *var8; (loc_40206E)(2LL); qword_4051E0 = &v13[8]; qword_4051E8 = &v16; qword_4051F0 = &v13[10]; qword_4051F8 = v4; qword_405200 = &v14; qword_405208 = &v13[8]; qword_405210 = &v13[8]; qword_405218 = *&v13[6]; sub_401935(6LL, &v13[10], &v13[6]); qword_4051E0 = &v19; qword_4051E8 = &v18; qword_4051F0 = v17; qword_4051F8 = v15; qword_405200 = &v13[12]; qword_405208 = &v13[8]; qword_405210 = *&v13[4]; qword_405218 = *v13; if ( sub_4020CC(5LL) ) { qword_4051E0 = &v13[8]; qword_4051E8 = &v20; qword_4051F0 = v9; qword_4051F8 = &v8; qword_405200 = v17; qword_405208 = v21; qword_405210 = v21; qword_405218 = &v13[4]; v7 = (loc_4019FC)(176LL, v15); v5 = *&var8[-193]; qword_4051E0 = v10; qword_4051E8 = &v6; qword_4051F0 = &v7; qword_4051F8 = &var8[-193]; qword_405200 = v5; qword_405208 = v7; qword_405210 = v6; qword_405218 = v10[0]; sub_402171(13LL); return 0LL; } else { qword_4051E0 = -2LL; qword_4051E8 = -1LL; qword_4051F0 = 0LL; qword_4051F8 = 1LL; qword_405200 = 2LL; qword_405208 = 3LL; qword_405210 = 4LL; qword_405218 = 5LL; sub_402171(10LL); return 1LL; } } } ``` - Here it is anti debug :((( ```cpp= _BOOL8 sub_40147F() { int v1; // [rsp+4h] [rbp-11Ch] FILE *stream; // [rsp+8h] [rbp-118h] char s1[10]; // [rsp+10h] [rbp-110h] BYREF char v4[254]; // [rsp+1Ah] [rbp-106h] BYREF unsigned __int64 v5; // [rsp+118h] [rbp-8h] v5 = __readfsqword(0x28u); stream = fopen("/proc/self/status", "r"); if ( !stream ) return 0LL; while ( fgets(s1, 256, stream) ) { if ( !strncmp(s1, "TracerPid:", 0xAuLL) ) { v1 = atoi(v4); fclose(stream); return v1 != 0; } } fclose(stream); return 0LL; } ``` - Ahh...Here it is checking and returning success or failure message ```cpp= if ( sub_4020CC(5LL) ) { qword_4051E0 = &v13[8]; qword_4051E8 = &v20; qword_4051F0 = v9; qword_4051F8 = &v8; qword_405200 = v17; qword_405208 = v21; qword_405210 = v21; qword_405218 = &v13[4]; v7 = (loc_4019FC)(176LL, v15); v5 = *&var8[-193]; qword_4051E0 = v10; qword_4051E8 = &v6; qword_4051F0 = &v7; qword_4051F8 = &var8[-193]; qword_405200 = v5; qword_405208 = v7; qword_405210 = v6; qword_405218 = v10[0]; sub_402171(13LL); return 0LL; } else { qword_4051E0 = -2LL; qword_4051E8 = -1LL; qword_4051F0 = 0LL; qword_4051F8 = 1LL; qword_405200 = 2LL; qword_405208 = 3LL; qword_405210 = 4LL; qword_405218 = 5LL; sub_402171(10LL); return 1LL; } ``` ```cpp= long double __fastcall sub_402171(int a1, _DWORD *a2) { long double v2; // fst7 __int16 *v3; // rax long double result; // fst7 v3 = a1; __debugbreak(); LODWORD(v3) = *a2; result = *v3 / v2; getpid(); puts("It... worked? OMG it actually worked?! You get the flag, congratz."); return result; } ``` ![image](https://hackmd.io/_uploads/S1aEjW9Akx.png) - But actually in `sub_4020CC(5LL)` I don't see them checking clearly. ```cpp= _BOOL8 __fastcall sub_4020CC(int a1) { long double v1; // fst7 long double v2; // fst7 __int64 v4; // [rsp+0h] [rbp-29Ah] __int64 v5; // [rsp+26Ah] [rbp-30h] __int64 v6; // [rsp+27Ah] [rbp-20h] __int64 v7; // [rsp+28Ah] [rbp-10h] __int128 var8; // [rsp+292h] [rbp-8h] __int64 retaddr; // [rsp+2A2h] [rbp+8h] *&var8 = 0x123456789ABCDEFLL; __debugbreak(); v2 = *a1 / v1; time(a1); qword_4051E0 = *(&var8 + 7); qword_4051E8 = v4; qword_4051F0 = v5; qword_4051F8 = v6; qword_405200 = retaddr; qword_405208 = *(&var8 + 1); qword_405210 = var8; qword_405218 = v7; return sub_401574(14LL, *&v2) == 23; } ``` - But one thing worth noting is that it will return bool with the comparison result return `sub_401574(14LL, *&v2) == 23;` so I think this is probably the counter condition for the flag with length 23 - Looking back at the `main()` function, we can clearly see that there are 23 arguments passed into it, which are the basis for us to decode. - Now we need to know what we are doing with these values. - Pay attention to this function `return sub_401574(14LL, *&v2) == 23;` ```cpp= long double __fastcall sub_401574(int a1) { long double v1; // fst7 long double result; // fst7 __int64 i; // [rsp+10h] [rbp-10h] __debugbreak(); result = *a1 / v1; time(a1); for ( i = 0LL; *(i - 0x5555444433332223LL); ++i ) ; return result; } ``` - At first glance it seems meaningless but when you switch to Assembly it will be different. - It starts here: ```asm= .text:00000000004015CF ; __unwind { .text:00000000004015CF endbr64 .text:00000000004015D3 push rbp .text:00000000004015D4 mov rbp, rsp .text:00000000004015D7 sub rsp, 20h .text:00000000004015DB mov [rbp-14h], edi .text:00000000004015DE mov byte ptr [rbp-3], 57h ; 'W' .text:00000000004015E2 mov byte ptr [rbp-2], 54h ; 'T' .text:00000000004015E6 mov byte ptr [rbp-1], 46h ; 'F' .text:00000000004015EA mov eax, [rbp-14h] .text:00000000004015ED cdqe .text:00000000004015EF mov rdi, rax .text:00000000004015F2 int 3 ; Trap to Debugger .text:00000000004015F3 or [rax], al .text:00000000004015F5 lodsd .text:00000000004015F6 fsubp st, st .text:00000000004015F8 test dl, bh .text:00000000004015F8 ; ----------------------------------------- .text:00000000004015FA dw 0FFFFh, 7D80h, 60FFh .text:0000000000401600 ; --------------------------------------------------------------------------- .text:0000000000401600 jle short loc_401611 .text:0000000000401602 cmp byte ptr [rbp-1], 7Ah ; 'z' .text:0000000000401606 jg short loc_401611 .text:0000000000401608 movzx eax, byte ptr [rbp-1] .text:000000000040160C sub eax, 20h ; ' ' .text:000000000040160F jmp short locret_401615 .text:0000000000401611 ; --------------------------------------------------------------------------- .text:0000000000401611 .text:0000000000401611 loc_401611: ; CODE XREF: .text:0000000000401600↑j .text:0000000000401611 ; .text:0000000000401606↑j .text:0000000000401611 movzx eax, byte ptr [rbp-1] .text:0000000000401615 .text:0000000000401615 locret_401615: ; CODE XREF: .text:000000000040160F↑j .text:0000000000401615 leave .text:0000000000401616 retn .text:0000000000401616 ; } // starts at 4015CF ``` - This snippet simply normalizes lowcase to upcase by subtracting 0x20 for characters <= 'z' - This paragraph is quite important. ```asm= .text:0000000000401617 sub_401617 proc near .text:0000000000401617 .text:0000000000401617 var_14 = dword ptr -14h .text:0000000000401617 var_4 = byte ptr -4 .text:0000000000401617 var_3 = byte ptr -3 .text:0000000000401617 var_2 = byte ptr -2 .text:0000000000401617 var_1 = byte ptr -1 .text:0000000000401617 .text:0000000000401617 ; __unwind { .text:0000000000401617 endbr64 .text:000000000040161B push rbp .text:000000000040161C mov rbp, rsp .text:000000000040161F sub rsp, 20h .text:0000000000401623 mov [rbp+var_14], edi .text:0000000000401626 mov [rbp+var_4], 41h ; 'A' .text:000000000040162A mov [rbp+var_3], 4Ch ; 'L' .text:000000000040162E mov [rbp+var_2], 45h ; 'E' .text:0000000000401632 mov [rbp+var_1], 44h ; 'D' .text:0000000000401636 mov eax, [rbp+var_14] .text:0000000000401639 cdqe .text:000000000040163B mov rdi, rax ; timer .text:000000000040163E int 3 ; Trap to Debugger .text:000000000040163F or [rax], eax .text:0000000000401641 lodsd .text:0000000000401642 fidivr word ptr [rdi+0] .text:0000000000401648 call _time .text:000000000040164D cmp [rbp+var_4], 2Fh ; '/' .text:0000000000401651 jle short loc_401662 .text:0000000000401653 cmp [rbp+var_4], 39h ; '9' .text:0000000000401657 jg short loc_401662 .text:0000000000401659 movsx eax, [rbp+var_4] .text:000000000040165D sub eax, 30h ; '0' .text:0000000000401660 jmp short locret_4016A3 .text:0000000000401662 ; --------------------------------------------------------------------------- .text:0000000000401662 .text:0000000000401662 loc_401662: ; CODE XREF: sub_401617+3A↑j .text:0000000000401662 ; sub_401617+40↑j .text:0000000000401662 cmp [rbp+var_4], 40h ; '@' .text:0000000000401666 jle short loc_401677 .text:0000000000401668 cmp [rbp+var_4], 5Ah ; 'Z' .text:000000000040166C jg short loc_401677 .text:000000000040166E movsx eax, [rbp+var_4] .text:0000000000401672 sub eax, 37h ; '7' .text:0000000000401675 jmp short locret_4016A3 .text:0000000000401677 ; --------------------------------------------------------------------------- .text:0000000000401677 .text:0000000000401677 loc_401677: ; CODE XREF: sub_401617+4F↑j .text:0000000000401677 ; sub_401617+55↑j .text:0000000000401677 cmp [rbp+var_4], 7Bh ; '{' .text:000000000040167B jnz short loc_401684 .text:000000000040167D mov eax, 24h ; '$' .text:0000000000401682 jmp short locret_4016A3 .text:0000000000401684 ; --------------------------------------------------------------------------- .text:0000000000401684 .text:0000000000401684 loc_401684: ; CODE XREF: sub_401617+64↑j .text:0000000000401684 cmp [rbp+var_4], 7Dh ; '}' .text:0000000000401688 jnz short loc_401691 .text:000000000040168A mov eax, 25h ; '%' .text:000000000040168F jmp short locret_4016A3 .text:0000000000401691 ; --------------------------------------------------------------------------- .text:0000000000401691 .text:0000000000401691 loc_401691: ; CODE XREF: sub_401617+71↑j .text:0000000000401691 cmp [rbp+var_4], 5Fh ; '_' .text:0000000000401695 jnz short loc_40169E .text:0000000000401697 mov eax, 26h ; '&' .text:000000000040169C jmp short locret_4016A3 ``` - It is normalizing the character in a special way. > 0-9 -> -= 0x30 > A-Z -> -= 0x37 > { -> $ > } -> % > _ -> & ```asm= .text:00000000004016A5 ; __unwind { .text:00000000004016A5 endbr64 .text:00000000004016A9 push rbp .text:00000000004016AA mov rbp, rsp .text:00000000004016AD sub rsp, 20h .text:00000000004016B1 mov [rbp+var_14], edi .text:00000000004016B4 mov [rbp+var_4], 42h ; 'B' .text:00000000004016BB mov eax, [rbp+var_14] .text:00000000004016BE cdqe .text:00000000004016C0 mov rdi, rax .text:00000000004016C3 int 3 ; Trap to Debugger .text:00000000004016C4 or al, [rax] .text:00000000004016C6 lodsd .text:00000000004016C7 fidivr word ptr [rax+0] .text:00000000004016CD call _getpid .text:00000000004016D2 mov edx, [rbp+var_4] .text:00000000004016D5 mov eax, edx .text:00000000004016D7 shl eax, 3 ; 2^3 .text:00000000004016DA sub eax, edx .text:00000000004016DC add eax, 3 .text:00000000004016DF movsxd rdx, eax .text:00000000004016E2 imul rdx, 0FFFFFFFFD20D20D3h .text:00000000004016E9 shr rdx, 20h .text:00000000004016ED add edx, eax .text:00000000004016EF sar edx, 5 .text:00000000004016F2 mov ecx, eax .text:00000000004016F4 sar ecx, 1Fh .text:00000000004016F7 sub edx, ecx .text:00000000004016F9 imul ecx, edx, 27h ; ''' .text:00000000004016FC sub eax, ecx .text:00000000004016FE mov edx, eax .text:0000000000401700 mov eax, edx .text:0000000000401702 leave .text:0000000000401703 retn ``` - Here they are performing a mathematical transformation. > - Total formula too: result = (a * 8 - a + 3) % 39 - This is the final transformation. ```asm= .text:0000000000401704 ; __unwind { .text:0000000000401704 endbr64 .text:0000000000401708 push rbp .text:0000000000401709 mov rbp, rsp .text:000000000040170C sub rsp, 20h .text:0000000000401710 mov [rbp+var_14], edi .text:0000000000401713 mov [rbp+var_4], 99h .text:000000000040171A mov eax, [rbp+var_14] .text:000000000040171D cdqe .text:000000000040171F mov rdi, rax .text:0000000000401722 int 3 ; Trap to Debugger .text:0000000000401723 or eax, [rax] .text:0000000000401725 lodsd .text:0000000000401726 fidivr word ptr [rax+0] .text:000000000040172C call _getppid .text:0000000000401731 mov eax, [rbp+var_4] .text:0000000000401734 xor eax, 66h .text:0000000000401737 leave .text:0000000000401738 retn .text:0000000000401738 ; } // starts at 401704 ``` - Here they are performing a mathematical transformation. > - Total formula too: result = a ^ 0x66 ### Solution: - And here is the version where I covered the program's encryption algorithm in python ```python= def to_upper(flag): return [ord(c.upper()) for c in flag] def char_to_val(flag): res = [] for c in flag: if 0x30 <= c <= 0x39: # '0'-'9' res.append(c - 0x30) elif 0x41 <= c <= 0x5A: # 'A'-'Z' res.append(c - 0x37) elif c == ord('{'): res.append(0x24) elif c == ord('}'): res.append(0x25) elif c == ord('_'): res.append(0x26) else: res.append(c) return res def apply_mod_math(flag): return [(7 * c + 3) % 39 for c in flag] def xor_with_66(flag): return [c ^ 0x66 for c in flag] def main(): flag = input("Enter flag: ") data = [0x67, 0x6F, 0x6D, 0x78, 0x73, 0x42, 0x72, 0x45, 0x6C, 0x62, 0x45, 0x6D, 0x63, 0x7E, 0x45, 0x6E, 0x7E, 0x46, 0x45, 0x42, 0x76, 0x6C, 0x7A] if len(flag) != len(data): print("Wrong length!") return f = to_upper(flag) f = char_to_val(f) f = apply_mod_math(f) f = xor_with_66(f) if f == data: print("Correct!\n") else: print("Wrong!\n") if __name__ == "__main__": main() ``` - Flag decoding version: ```python= def reverse_flag(data): recovered = [] for val in data: # Step 1: Reverse XOR v = val ^ 0x66 # Step 2: Find x so that (7 * x + 3) % 39 == v found = False for x in range(39): if (7 * x + 3) % 39 == v: found = True break if not found: print("No valid value found!") return None # Step 3: Reverse mapping to characters if 0 <= x <= 9: c = chr(x + 0x30) elif 10 <= x <= 35: c = chr(x + 0x37) elif x == 0x24: c = '{' elif x == 0x25: c = '}' elif x == 0x26: c = '_' else: print(f"Unmapped value: {x}") return None recovered.append(c) return ''.join(recovered) # Data from main function data = [0x67, 0x6F, 0x6D, 0x78, 0x73, 0x42, 0x72, 0x45, 0x6C, 0x62, 0x45, 0x6D, 0x63, 0x7E, 0x45, 0x6E, 0x7E, 0x46, 0x45, 0x42, 0x76, 0x6C, 0x7A] flag = reverse_flag(data) print("Flag:", flag) ``` > FLAG: MCTF{R8_1S_TH3_N3W_RD1} > Thanks for reading ## Samurai - I also rate this challenge as quite good about shell code ### Challenge analysis: ![image](https://hackmd.io/_uploads/Skh47P5Ayx.png) - Looking at DIE it is indeed a PE file but I see no signs of compilation by C/C++ or anything else. - Put it in IDA and analyze what it is doing. ```cpp= int __fastcall main(int argc, const char **argv, const char **envp) { LPVOID v4; // [rsp+28h] [rbp-8h] _main(); if ( argc == 2 ) { puts("Nannnnn je deconne le password ne sert a rien"); puts("[+] Removing System32"); Sleep(0x7D0u); puts("[+] Sending bitcoin to MCTF Admins"); Sleep(0x4B0u); puts("[+] Attempt to install rootkit"); Sleep(0xFA0u); puts("[-] Failed"); puts("[+] Attempt to install rootkit harder"); Sleep(0x1388u); puts("[+] Sucess"); Sleep(0x3E8u); puts("[+] Start connection to C2: https://tinyurl.com/533u5pxs"); v4 = VirtualAlloc(0LL, 0xD1600uLL, 0x1000u, 0x40u); memcpy(v4, &shellcode, 0xD1600uLL); (v4)(); return 0; } else { printf("Usage: %s password\n", *argv); return 1; } } ``` - Woww...Pay attention to the shell code it seems the oscur.exe file only indirectly executes the shell code to check the flag - Try extracting this shell code to see what it is. - This file has a total of 857600 bytes. ![image](https://hackmd.io/_uploads/r1dsNDc0Jl.png) - It is exactly an exe shell code - We will dump this shell code starting from ```4D 5A``` until the 857600th byte. - If you use IDA to analyze, dump it my way to dump the most complete shellcode.exe ![image](https://hackmd.io/_uploads/BJaBLP9Rke.png) ![image](https://hackmd.io/_uploads/HkbuIvqAyg.png) ![image](https://hackmd.io/_uploads/Skfa8P5R1e.png) - Now let's analyze directly on `shellcode.exe` instead of oscur.exe - On the `shellcode.exe` file there seems to be no `main()` function but only `start()` function. ```cpp= __int64 start() { unk_1400230F0 = 1; return sub_140001180(); } ``` - It seems like nothing :(( ![image](https://hackmd.io/_uploads/S1w0wD5AJl.png) - When I look at Strings I see the string ```Nah I'd Win``` at `sub_140001972` ```cpp= __int64 __fastcall sub_140001972(__int64 a1, __int64 a2, __int64 a3, unsigned int a4) { __int64 v4; // rax const CHAR *v5; // rax HKEY hKey; // [rsp+30h] [rbp-10h] BYREF char v8; // [rsp+3Bh] [rbp-5h] BYREF if ( RegOpenKeyExA(HKEY_CURRENT_USER, "I Really Want to Stay at Your House", 0, 0x20019u, &hKey) ) { sub_140001688(a1, a4); } else { v4 = sub_140001898(&v8); v5 = sub_140015A10(v4); MessageBoxA(0LL, v5, "Nah I'd Win", 0x40u); RegCloseKey(hKey); } return 0LL; } ``` - Pay attention to these two small functions because they are in the condition that prints out the success message for you. ```cpp= v4 = sub_140001898(&v8); v5 = sub_140015A10(v4); ``` ```cpp= __int64 sub_140001898() { __int64 v0; // rax _BYTE v2[15]; // [rsp+20h] [rbp-20h] BYREF _BYTE v3[2]; // [rsp+2Fh] [rbp-11h] BYREF __int64 v4; // [rsp+38h] [rbp-8h] v4 = 17LL; qmemcpy(v2, "z2", 2); v2[2] = -113; v2[3] = -71; v2[4] = 82; v2[5] = -113; v2[6] = 33; v2[7] = 98; v2[8] = 84; v2[9] = 60; v2[10] = -76; v2[11] = -111; v2[12] = 111; v2[13] = -117; v2[14] = 4; qmemcpy(v3, "p7", sizeof(v3)); if ( !*sub_14000E810(&unk_140018020) ) { v0 = sub_14000E810(&unk_140018040); sub_140015970(v0, v2); *sub_14000E810(&unk_140018020) = 1; sub_14000E810(&unk_140018040); sub_140016970(sub_1400159D0); } return sub_14000E810(&unk_140018040); } ``` ```cpp= __int64 __fastcall sub_140015920(__int64 a1) { __int64 result; // rax result = *(a1 + 17); if ( result ) { sub_140015A40(a1, 17LL, 0xD53DF29FFDB7137LL); result = a1; *(a1 + 17) = 0; } return result; } ``` ```cpp= unsigned __int64 __fastcall sub_140015A40(__int64 a1, unsigned __int64 a2, unsigned __int64 a3) { unsigned __int64 result; // rax unsigned __int64 i; // [rsp+8h] [rbp-8h] for ( i = 0LL; ; ++i ) { result = i; if ( i >= a2 ) break; *(a1 + i) ^= a3 >> (8 * (i & 7u)); } return result; } ``` - And here are the two main keys to how you can solve it. - Its main logic is when you enter the flag and the shell code will do XOR with the key and compare with the data. - So data is stored at `sub_140001898()` and key is stored at `sub_140015920()`. - Now we just need to perform the reverse XOR to find the flag ### Solution: - Script solve python: ```python= data = [0x7A,0x32,0x8F,0xB9,0x52,0x8F,0x21,0x62,0x54,0x3C,0xB4,0x91,0x6F,0x8B,0x04,0x70 ,0x37] key = [0x37 ,0x71 ,0xdb ,0xff ,0x29 ,0xdf ,0x53 ,0x0d] for i in range(len(data)): flag = data[i] ^ key[i % len(key)] print(chr(flag), end="") ``` > FLAG: MCTF{ProcMonFTW} # Android - I think this Android challenge is pretty cool. ## Bby Neopasswd ### Challenge analysis: ![image](https://hackmd.io/_uploads/Bk7xhv9Ake.png) - ![image](https://hackmd.io/_uploads/H16WnD9Ckg.png) - Normally I would check `MainActivity` first - But it seems there is nothing interesting here :(( ```java= package com.example.neopasswd; import android.os.Bundle; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatDelegate; import androidx.navigation.NavController; import androidx.navigation.Navigation; import androidx.navigation.ui.AppBarConfiguration; import androidx.navigation.ui.NavigationUI; import com.example.neopasswd.databinding.ActivityMainBinding; /* loaded from: classes6.dex */ public class MainActivity extends AppCompatActivity { private ActivityMainBinding binding; @Override // androidx.fragment.app.FragmentActivity, androidx.activity.ComponentActivity, androidx.core.app.ComponentActivity, android.app.Activity protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); AppCompatDelegate.setDefaultNightMode(2); this.binding = ActivityMainBinding.inflate(getLayoutInflater()); setContentView(this.binding.getRoot()); AppBarConfiguration appBarConfiguration = new AppBarConfiguration.Builder(R.id.navigation_home, R.id.navigation_dashboard, R.id.navigation_notifications).build(); NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment_activity_main); NavigationUI.setupActionBarWithNavController(this, navController, appBarConfiguration); NavigationUI.setupWithNavController(this.binding.navView, navController); } } ``` - After rummaging through it for a while, I found something interesting in the `notification` folder. - There is something very interesting in the `NotificationsFragmen` class. ```java= package com.example.neopasswd.ui.notifications; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import androidx.fragment.app.Fragment; import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.LiveData; import androidx.lifecycle.Observer; import androidx.lifecycle.ViewModelProvider; import com.example.neopasswd.databinding.FragmentNotificationsBinding; import java.util.Objects; /* loaded from: classes4.dex */ public class NotificationsFragment extends Fragment { private FragmentNotificationsBinding binding; @Override // androidx.fragment.app.Fragment public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { NotificationsViewModel notificationsViewModel = (NotificationsViewModel) new ViewModelProvider(this).get(NotificationsViewModel.class); this.binding = FragmentNotificationsBinding.inflate(inflater, container, false); View root = this.binding.getRoot(); final TextView textView = this.binding.textNotifications; LiveData<String> text = notificationsViewModel.getText(); LifecycleOwner viewLifecycleOwner = getViewLifecycleOwner(); Objects.requireNonNull(textView); text.observe(viewLifecycleOwner, new Observer() { // from class: com.example.neopasswd.ui.notifications.NotificationsFragment$$ExternalSyntheticLambda0 @Override // androidx.lifecycle.Observer public final void onChanged(Object obj) { textView.setText((String) obj); } }); byte[] bArr = {15, 1, 22, 4, 57, 115, 54, 119, 29, 17, 55, 18, 113, 48, 29, 113, 35, 49, 59, 29, 54, 114, 29, 4, 115, 44, 38, 29, 17, 113, 33, 48, 39, 54, 119, 63}; return root; } @Override // androidx.fragment.app.Fragment public void onDestroyView() { super.onDestroyView(); this.binding = null; } private byte[] encryptNotification(byte[] toEncrypt) { byte[] encrypted = new byte[toEncrypt.length]; for (int i = 0; i < encrypted.length; i++) { encrypted[i] = (byte) (toEncrypt[i] ^ 66); } return encrypted; } } ``` - It is checking the flag by XOR it with the key and checking it with the given data decima. - Let's decode it ### Solution: - Script solve python: ```python= data = [15, 1, 22, 4, 57, 115, 54, 119, 29, 17, 55, 18, 113, 48, 29, 113, 35, 49, 59, 29, 54, 114, 29, 4, 115, 44, 38, 29, 17, 113, 33, 48, 39, 54, 119, 63] key = 66 for i in range(len(data)): flag = data[i] ^ key print(chr(flag), end="") ``` > FLAG: MCTF{1t5_SuP3r_3asy_t0_F1nd_S3cret5} ## Neopasswd2 - I'm impressed with this second Android challenge :)) ### Challenge analysis: ![image](https://hackmd.io/_uploads/H1jdAwqR1l.png) - It is also a similar apk file and I will use JADX to analyze it. - In addition, I also use Androi Studio to run and analyze. - I'll still visit the MainActivity function first to see what's going on. ```java= package com.example.neopasswd2; import android.content.Intent; import android.content.SharedPreferences; import android.database.Cursor; import android.os.Bundle; import android.util.Base64; import android.view.MenuItem; import android.view.View; import android.widget.TextView; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatDelegate; import androidx.core.view.GravityCompat; import androidx.drawerlayout.widget.DrawerLayout; import androidx.navigation.NavController; import androidx.navigation.Navigation; import androidx.navigation.ui.AppBarConfiguration; import androidx.navigation.ui.NavigationUI; import com.example.neopasswd2.UserContract; import com.example.neopasswd2.databinding.ActivityMainBinding; import com.google.android.material.navigation.NavigationView; import com.google.android.material.snackbar.Snackbar; import javax.crypto.Cipher; import javax.crypto.spec.SecretKeySpec; /* loaded from: classes3.dex */ public class MainActivity extends AppCompatActivity { private ActivityMainBinding binding; private boolean isAdmin = false; private AppBarConfiguration mAppBarConfiguration; public native String getObfuscatedString(); @Override // androidx.fragment.app.FragmentActivity, androidx.activity.ComponentActivity, androidx.core.app.ComponentActivity, android.app.Activity protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); AppCompatDelegate.setDefaultNightMode(2); this.binding = ActivityMainBinding.inflate(getLayoutInflater()); setContentView(this.binding.getRoot()); setSupportActionBar(this.binding.appBarMain.toolbar); final SharedPreferences prefs = getSharedPreferences("session", 0); String currentUsername = prefs.getString("current_user", null); if (currentUsername == null) { startActivity(new Intent(this, (Class<?>) LoginActivity.class)); finish(); return; } TextView welcomeText = (TextView) this.binding.appBarMain.getRoot().findViewById(R.id.welcomeText); if (welcomeText != null) { welcomeText.setText("Logged as : " + currentUsername); } Cursor cursor = getContentResolver().query(UserContract.CONTENT_URI, null, "username=?", new String[]{currentUsername}, null); if (cursor != null && cursor.moveToFirst()) { int adminValue = cursor.getInt(cursor.getColumnIndexOrThrow(UserContract.UserEntry.COLUMN_ADMIN)); this.isAdmin = adminValue == 1; cursor.close(); } this.binding.appBarMain.fab.setOnClickListener(new View.OnClickListener() { // from class: com.example.neopasswd2.MainActivity.1 private boolean firstClick = true; @Override // android.view.View.OnClickListener public void onClick(View view) { if (!MainActivity.this.isAdmin) { Snackbar.make(view, "Sorry, only an admin can read messages. :/", 0).setAnchorView(R.id.fab).show(); return; } if (!this.firstClick) { String decrypted = MainActivity.this.tryDecrypt("Mszhl+UnftsTwm7Ule0V28WQMptqd8uoc4AbDSBKavw="); if (decrypted != null) { Snackbar.make(view, "★" + decrypted, 0).setAnchorView(R.id.fab).show(); return; } else { Snackbar.make(view, "Sry bro, you don't have permission to read the notification :(", 0).setAnchorView(R.id.fab).show(); return; } } Snackbar.make(view, "A secret and important notification is about to arrive..", 0).setAnchorView(R.id.fab).show(); this.firstClick = false; } }); final DrawerLayout drawer = this.binding.drawerLayout; NavigationView navigationView = this.binding.navView; this.mAppBarConfiguration = new AppBarConfiguration.Builder(R.id.nav_home, R.id.nav_gallery, R.id.nav_slideshow).setOpenableLayout(drawer).build(); final NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment_content_main); NavigationUI.setupActionBarWithNavController(this, navController, this.mAppBarConfiguration); NavigationUI.setupWithNavController(navigationView, navController); navigationView.setNavigationItemSelectedListener(new NavigationView.OnNavigationItemSelectedListener() { // from class: com.example.neopasswd2.MainActivity.2 @Override // com.google.android.material.navigation.NavigationView.OnNavigationItemSelectedListener public boolean onNavigationItemSelected(MenuItem item) { int id = item.getItemId(); if (id == R.id.nav_logout) { prefs.edit().clear().apply(); Intent i = new Intent(MainActivity.this, (Class<?>) LoginActivity.class); i.setFlags(268468224); MainActivity.this.startActivity(i); MainActivity.this.finish(); return true; } boolean handled = NavigationUI.onNavDestinationSelected(item, navController); if (handled) { drawer.closeDrawer(GravityCompat.START); } return handled; } }); } public int getMaxAllowedLength() { return 3; } /* JADX INFO: Access modifiers changed from: private */ public String tryDecrypt(String base64Data) { try { byte[] encrypted = Base64.decode(base64Data, 0); if (encrypted.length > getMaxAllowedLength()) { return null; } byte[] key = getObfuscatedString().getBytes("UTF-8"); Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); cipher.init(2, new SecretKeySpec(key, "AES")); byte[] decrypted = cipher.doFinal(encrypted); String result = new String(decrypted, "UTF-8").trim(); return result; } catch (Exception e) { return null; } } static { System.loadLibrary("native-lib"); } @Override // androidx.appcompat.app.AppCompatActivity public boolean onSupportNavigateUp() { NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment_content_main); return NavigationUI.navigateUp(navController, this.mAppBarConfiguration) || super.onSupportNavigateUp(); } } ``` - Ahaha...So much fun - I will summarize how it works for you. > 1. Perform account login (but you can bypass it by logging in with a wrong account and then click register, I think this is a bug of the program) > 2. When you click on the button it will decrypt the cipher text string available with the key from the program with AES algorithm but required with Admin rights. > 3. If all the above requirements are correct the program will return the flag ![image](https://hackmd.io/_uploads/S1YD-d50yx.png) - Please enter random username and password then login. ![image](https://hackmd.io/_uploads/SyWTb_cAJl.png) - Then click register. ![image](https://hackmd.io/_uploads/B19JfucAyl.png) - Try pressing it, is there anything? ![image](https://hackmd.io/_uploads/ByDzfdcAyx.png) ![image](https://hackmd.io/_uploads/S1arfO5Ckx.png) ```java= public void onClick(View view) { if (!MainActivity.this.isAdmin) { Snackbar.make(view, "Sorry, only an admin can read messages. :/", 0).setAnchorView(R.id.fab).show(); return; } ``` - I violated this condition because I do not have admin rights. - I'll read through this function to see if there are any conditions that would prevent me from getting the flag. ```java= if (!this.firstClick) { String decrypted = MainActivity.this.tryDecrypt("Mszhl+UnftsTwm7Ule0V28WQMptqd8uoc4AbDSBKavw="); if (decrypted != null) { Snackbar.make(view, "★" + decrypted, 0).setAnchorView(R.id.fab).show(); return; } else { Snackbar.make(view, "Sry bro, you don't have permission to read the notification :(", 0).setAnchorView(R.id.fab).show(); return; } } Snackbar.make(view, "A secret and important notification is about to arrive..", 0).setAnchorView(R.id.fab).show(); this.firstClick = false; ``` - There are two conditions here that I cannot meet. - In summary I have 3 conditions I cannot overcome it: > 1. if (!MainActivity.this.isAdmin) > 2. if (!this.firstClick) > 3. if (decrypted != null) - In the third condition I saw a piece of code that prevented it with a constant 3 because the length of the flag is always greater than 3 so it will always return false. - Hahaah here: ```java= if (encrypted.length > getMaxAllowedLength()) { return null; } ``` ```java= public int getMaxAllowedLength() { return 3; } ``` - Now I will patch this apk file with the above conditions to get the flag - I will perform Decompile apk file on linux using apktool - First I will unzip the apk file and get the MainActivity smali file to patch it. - Command unpack apktool: > apktool d neopasswd2.apk -o app_src - This is the address containing the 2 files `MainActivity.smali` and `MainActivity$1.smali` that need to be patched. > app_src/smali_classes3/com/example/neopasswd2 ### Solution: - First I will bypass this part in `MainActivity.smail` ```java= if (!MainActivity.this.isAdmin) { Snackbar.make(view, "Sorry, only an admin can read messages. :/", 0).setAnchorView(R.id.fab).show(); return; } ``` - It will be equivalent to the code from lines `27-39` in the smali file - And you need to fix the code 345-352 ```smali= .method public constructor <init>()V .locals 1 .line 30 invoke-direct {p0}, Landroidx/appcompat/app/AppCompatActivity;-><init>()V .line 34 const/4 v0, 0x0 iput-boolean v0, p0, Lcom/example/neopasswd2/MainActivity;->isAdmin:Z return-void .end method ``` ```smali= :cond_2 iput-boolean v1, p0, Lcom/example/neopasswd2/MainActivity;->isAdmin:Z .line 71 invoke-interface {v5}, Landroid/database/Cursor;->close()V .line 74 .end local v6 # "adminValue":I ``` - Since this line always returns false with admin privileges, I will bypass it by changing ```const/4 v0, 0x0``` to ```const/4 v0, 0x1``` - You need to add ```const/4 v1, 0x1``` to set the value true for `isAdmin` ```smali= .method public constructor <init>()V .locals 1 .line 30 invoke-direct {p0}, Landroidx/appcompat/app/AppCompatActivity;-><init>()V .line 34 const/4 v0, 0x1 iput-boolean v0, p0, Lcom/example/neopasswd2/MainActivity;->isAdmin:Z return-void .end method ``` ```smali= :cond_2 const/4 v1, 0x1 iput-boolean v1, p0, Lcom/example/neopasswd2/MainActivity;->isAdmin:Z .line 71 invoke-interface {v5}, Landroid/database/Cursor;->close()V .line 74 .end local v6 # "adminValue":I ``` - Next I will deal with this condition. ```java= if (!this.firstClick) ``` - It is in the file ```MainActivity$1.smali``` from lines 81-85 with the jump command being ```if-eqz v0, :cond_1``` ```java= .line 83 :cond_0 iget-boolean v0, p0, Lcom/example/neopasswd2/MainActivity$1;->firstClick:Z if-eqz v0, :cond_1 ``` - Fixed code: ```java= .line 83 :cond_0 iget-boolean v0, p0, Lcom/example/neopasswd2/MainActivity$1;->firstClick:Z goto :cond_1 .line 84 ``` - Next I deal with the last condition. - ```java= if (decrypted != null) { Snackbar.make(view, "★" + decrypted, 0).setAnchorView(R.id.fab).show(); return; } else { Snackbar.make(view, "Sry bro, you don't have permission to read the notification :(", 0).setAnchorView(R.id.fab).show(); return; } ``` ```java= public int getMaxAllowedLength() { return 3; } ``` - Instead of assigning `3` here I will assign` 0xFF` - This code corresponds to lines 160-167 in the file ```MainActivity.smali``` ```java= .method public getMaxAllowedLength()I .locals 1 .line 136 const/4 v0, 0x3 return v0 .end method ``` - Instead of ```const/4 v0, 0x3``` I will change it to ``` const/16 v0, 0xff``` - Everything is ready, I will recompile and run to get the flag. Don't forget to replace these two new smali files back to the original place -.- - I will build a new apk file. > Command: apktool b app_src -o app_patched.apk - Then create a new signature. > Command: keytool -genkeypair -v -keystore my-release-key.keystore -keyalg RSA -keysize 2048 -validity 10000 -alias my-key-alias - Re-sign the apk file > Command: apksigner sign --ks my-release-key.keystore --out app_signed.apk app_patched.apk - Woww... Rerun the new apk file named ```app_signed.apk``` on Android Studio to see if it returns the flag. ![image](https://hackmd.io/_uploads/rkSklK50kl.png) - Yahhhhh...FLag here. > FLAG: Th3_c4k3_1s_4_L13! > Thanks for reading everyone.