Official Write Up Timeline
For the paths not explored by the participants, the question setters will come forward to provide an official interpretation.
Android Author:bin233
Tangshan Normal University/Senior Year/Android Score 610 TOP2
Question 1: Breaking LEM
First, drag the apk into JEB for decompilation, go to the entry class and find the click event function.
It was observed that the Java layer is only responsible for passing input content to the native layer, so we can directly analyze the so file’s Java_com_didictf_guesskey2019lorenz_MainActivity_stringFromJNI function.
This function first compares the input content with the string “ddctf-android-lorenz-“. If the input length is insufficient, it fails directly; otherwise, it performs a truncation operation (e.g., input ddctf-android-lorenz-XXX will be truncated to XXX).
Then it performs character-by-character verification on “XXX”; characters must belong to the string “ABCDEFGHIJKLMNOPQRSTUVWXYZ123456”. Next is the Lorenz encryption, and I found the implementation of this algorithm on GitHub, discovering that the encryption and decryption functions are the same. Therefore, we only need to obtain the ciphertext and let the apk run once to get the plaintext. Unsurprisingly, the Lorenz encryption only encrypts “XXX” (let’s denote the encrypted result as “YYY”). Then, it performs a sha256 operation on “YYY”.
Analysis reveals that it is encrypted using a five-layer sha256 algorithm, which is then compared with shaCorrect. By searching for cross-references, one can find the real string of shaCorrect (initialized in init_array).
The next task is to brute-force crack the sha256. That night of the competition, I completed all strings of 7 characters or fewer. In the end, I got a hint that it was an 8-character string and was told the first two characters. Therefore, by completing it to an 8-character string, we can ensure that the first two characters of the ciphertext after Lorenz encryption remain unchanged; we only need to brute-force the last six characters. Eventually, I was incredibly lucky and cracked it in a minute.
After cracking, I concatenated the result with ddctf-android-lorenz- and let the apk automatically decrypt the plaintext for us.
Question 2: Have Fun
First, drag it into JEB and find that the identifiers have been obfuscated into invisible characters. Since the file is small, I manually renamed it to deobfuscate.
It was easy to trace to the function that encrypts the input content for the first time; the o() and p() functions will release the dex file from Assets to a hidden folder, while also secretly modifying the bytecode.
The apk uses first-generation protection technology, utilizing DEXClassLoader to hot-load the dex file, so I continued to follow up in the dexLoader function.
To obtain the dex file faster and more accurately, I used IDA dynamic debugging on the dex, which allowed me to directly obtain the dex file path and the dex file about to be loaded (the algorithm for obtaining the dex file directly from assets was incorrect).
The correct algorithm implementation is as follows:
Next, the program will delete that dex file and finally call the so layer function. The so file underwent section encryption, but static analysis was enough. From JNI_Onload, I obtained the dynamically registered triplet and found the specific function location.
The program will convert the input content into hexadecimal and compare it with fixed data in memory, as shown in the following image.
The decryption script is as follows:
Question 3: A Different Service
The question setter hopes that participants can find multiple control points of the distracting branches, distinguishing the correct branch from the logic of the control conditions. This time, there were fewer distracting branches, so the brute force method used by participants was also feasible; if there were more distracting branches, it would be time-consuming.
————————————————————-
This question uses control flow flattening, and the visuals are stunning, forcing obfuscation debugging. First, the JAVA layer will start a service to participate in the verification of input content, which has no critical logic, focusing on the so layer. From JNI_OnLoad, I found the dynamically registered functions as follows:
It is easy to find the function for anti-debug detection, which we will not concern ourselves with for now.
Next, pay attention to the handling function of Parcel, create a structure for easier analysis, and focus on the call to readString during dynamic debugging.
Step-by-step tracking reveals that the following function will use the readString function (offset 0x1DB50).
Following the position shown in the image above (offset 0x10458), I finally obtained the input content from the java layer, and then entered the sendInput1 function (offset 0x1B0D4).
Here, the program uses a socket to send the input content, then enters the recvResult function (offset 0x1470C).
It was found that the received data was actually different from the sent data, and after multiple debugging attempts, it was discovered that the content received each time was also different; I temporarily set this issue aside. Next, the received data was analyzed, and the program would compare this content with fixed memory data (referred to as enFlag) (offset 0x8540).
Later, I thought of another service process and started debugging that service process. I traced to the validate function and found that if the input length was 32, it would return the dd string (and there was also a verification of whether the result of recv was ddd in the main process, otherwise that strange content would not be received).
First encryption operation: Step-by-step tracking reveals that this is implemented in Python as follows:
Second encryption operation: It will first save the first two elements, and then every two subsequent elements will be XORed; after processing, the previously saved elements are placed at the end. The pseudocode is as follows:
The third encryption operation: It will XOR with a certain memory data (referred to as key) bit by bit, and finally send it out. This is the reason why the data received in the main thread differs from the data sent (the main process and service process communicate via socket, hence IDA could only control the main process space).
XORing the key with enFlag completes one decryption, but the last two elements are clearly not in the ASCII table, so it is inferred that I obtained an incorrect key (which confirms the previous phenomenon of receiving multiple different results).
Therefore, I first input a random 32-character string, self-implement the first and second encryption operations, and then XOR it with the data received from the main process; this way, I obtained multiple keys, one of which must be real.
Continuing to XOR these keys with enFlag, one of the key XOR results is shown in the image below.
68 corresponds to the character ‘D’, and 69 is exactly due to the addition of index 1 during the first encryption, hence it is also ‘D’ (isn’t that just like DDCTF? It can be inferred that I have obtained the correct data). The next issue is to crack the “second encryption”; directly brute-forcing is unrealistic, so here are two decryption methods:
1. Reverse Guessing Method:
It can be inferred that the last element data is “}”, so after the “first encryption”, it is “} + 31 = 156”. Therefore, we only need to guess the second last element and reverse XOR it. The specific script is as follows:
def myPrint(res):
ret=[]
for i in range(32):
ret+=chr(res[i]-i)
print "".join(ret)
for j in range(160):
ispass=0
flag=[ 1 , 18 , 15 , 215 , 22 , 254 , 12 , 9 , 42 , 21 , 20 , 50 , 232 , 22 , 242 , 204 , 1 , 248 , 2 , 246 , 244 , 248 , 4 , 251 , 221 , 202 , 22 , 3 , 27 , 210 , 68 , 69 ]
flag[30]=j
flag[31]=156
for i in range(1,31):
flag[30-i] = flag[32-i]^flag[30-i]
if(flag[30-i]<33 or flag[30-i]>160):
ispass=1
break
if(ispass==0):
flag[0]=68
flag[1]=69
myPrint(flag)
2. Forward XOR Method:
Since we have already seen the “DD” string, the following must be “CTF”; XORing it again directly gives the specific script as follows:
flag=[ 1 , 18 , 15 , 215 , 22 , 254 , 12 , 9 , 42 , 21 , 20 , 50 , 232 , 22 , 242 , 204 , 1 , 248 , 2 , 246 , 244 , 248 , 4 , 251 , 221 , 202 , 22 , 3 , 27 , 210 , 68 , 69 ]
tmp=[]
tmp.append(flag.pop(30))
tmp.append(flag.pop(30))
tmp+=flag
tmp[2]=ord('C')+2
tmp[3]=ord('T')+3
for i in range(0,30):
tmp[i+2]= tmp[i] ^ flag[i]
for i in range(32):
tmp[i]-=i
print "".join(map(lambda x:chr(x),tmp))
————— End —————
Further Reading
The official questions are still open for access; click “Read the original text” to go.
About Vulnerabilities
Please submit any vulnerabilities related to Didi Chuxing to
http://sec.didichuxing.com/
Leave a Comment
Your email address will not be published. Required fields are marked *