The ARC6969 is an old and forgotten architecture used in a military computers during Cold War. Although we don't have the computers anymore, we got CPU manual and a few programs.
Solution
The idea behind this challenge is the same as that of the previous RISC 8bit CPU challenge. The extra difficulty stems from the increased number of instructions, I/O handling, and a more complex flag register.
Comparison
There are now 4 flags that can be set by the comparison instructions.
The first three are pretty straightforward. Note that for signed numbers, the MSB determines the sign (1 for negative, 0 for positive).
if opcode ==4or opcode ==5: Rx =int(curr_instruction[8:13], 2) Ry_Imm8 =int(curr_instruction[16:], 2)# print(f"COMPARISON; opcode: {opcode}; Rx: {Rx}; Ry/Imm8: {Ry_Imm8}") fr &=0b0000if opcode ==4:if registers[Rx]== registers[Ry_Imm8]: fr |=0b0001if registers[Ry_Imm8]> registers[Rx]: fr |=0b0010if (registers[Rx]- registers[Ry_Imm8]) &0b10000000: fr |=0b0100# Rx and Ry have the same sign, but Rx - Ry has a different signif registers[Rx]&0b10000000== registers[Ry_Imm8]&0b10000000and\ (registers[Rx]- registers[Ry_Imm8]) &0b10000000!= registers[Rx]&0b10000000: fr |=0b1000else:if registers[Rx]== Ry_Imm8: fr |=0b0001if Ry_Imm8 > registers[Rx]: fr |=0b0010if (registers[Rx]- Ry_Imm8) &0b10000000: fr |=0b0100# Rx and Ry have the same sign, but Rx - Ry has a different signif registers[Rx]&0b10000000== Ry_Imm8 &0b10000000and\ (registers[Rx]- Ry_Imm8) &0b10000000!= registers[Rx]&0b10000000: fr |=0b1000
I/O Devices
There are two I/O interfaces - a GPU and a serial interface.
It is given that the GPU display is 64 x 32. This is represented by a 2D array, where each element represents a pixel. The GPU is not yet needed for this challenge, so we'll touch up the display_screen() function in the next challenge. The serial I/O is pretty standard - we'll accept user input one byte at a time.
gpu = [[0for _ inrange(64)] for _ inrange(32)]gpu_x =0gpu_y =0...# IO Device Communicationelif opcode ==21: Rx =int(curr_instruction[8:13], 2) Imm3 =int(curr_instruction[13:], 2)# print(f"IO; Rx: {Rx}; Imm3: {Imm3}")if Imm3 ==1: gpu_x = registers[Rx]elif Imm3 ==2: gpu_y = registers[Rx]elif Imm3 ==3: gpu[gpu_y][gpu_x] = registers[Rx]elif Imm3 ==4:# Draw bufferdisplay_screen(gpu)elif Imm3 ==5:# Return the number of bytes currently in serial buffer registers[Rx]=int(input("Number of bytes: "))elif Imm3 ==6:# Read 1 byte from serial buffer registers[Rx]=int(input("Byte: "))elif Imm3 ==7:# Write 1 byte to serial outprint(chr(registers[Rx]), end='')
One last detail - since there are subtraction instructions, we have to handle the cases where the result becomes negative. Taking the lower 8 bits is the same as subtracting the result from 256.
# 8 bitfor i inrange(len(registers)):while registers[i]>=256: registers[i]-=256while registers[i]<0: registers[i]=256-abs(registers[i])
Putting everything together, the full emulator script is as follows:
#!/usr/bin/python3ROM ="58 84 00 ... 45 F7 A0"registers = [0for _ inrange(32)]pc =0# Program Counterfr =0# Flag Registerrom = [int(x, 16)for x in ROM.split()]memory = rom + [0for _ inrange(0xffff+1-len(rom))]gpu = [[0for _ inrange(64)] for _ inrange(32)]gpu_x =0gpu_y =0defdisplay_screen(gpu):print(gpu)done =Falsewhilenot done: curr_instruction =bin(memory[pc])[2:].zfill(8) opcode =int(curr_instruction[:5], 2)# 24 bits per instructionif opcode !=21: curr_instruction = memory[pc:pc+3] pc +=3# 16 bits per instructionelse: curr_instruction = memory[pc:pc+2] pc +=2 curr_instruction =''.join([bin(x)[2:].zfill(8) for x in curr_instruction])# Comparisonif opcode ==4or opcode ==5: Rx =int(curr_instruction[8:13], 2) Ry_Imm8 =int(curr_instruction[16:], 2)# print(f"COMPARISON; opcode: {opcode}; Rx: {Rx}; Ry/Imm8: {Ry_Imm8}") fr &=0b0000if opcode ==4:if registers[Rx]== registers[Ry_Imm8]: fr |=0b0001if registers[Ry_Imm8]> registers[Rx]: fr |=0b0010if (registers[Rx]- registers[Ry_Imm8]) &0b10000000: fr |=0b0100# Rx and Ry have the same sign, but Rx - Ry has a different signif registers[Rx]&0b10000000== registers[Ry_Imm8]&0b10000000and\ (registers[Rx]- registers[Ry_Imm8]) &0b10000000!= registers[Rx]&0b10000000: fr |=0b1000else:if registers[Rx]== Ry_Imm8: fr |=0b0001if Ry_Imm8 > registers[Rx]: fr |=0b0010if (registers[Rx]- Ry_Imm8) &0b10000000: fr |=0b0100# Rx and Ry have the same sign, but Rx - Ry has a different signif registers[Rx]&0b10000000== Ry_Imm8 &0b10000000and\ (registers[Rx]- Ry_Imm8) &0b10000000!= registers[Rx]&0b10000000: fr |=0b1000# Arithmeticelif opcode <=13: Rx =int(curr_instruction[6:11], 2) Ry =int(curr_instruction[11:16], 2) Rz_Imm8 =int(curr_instruction[16:], 2)# print(f"ARITHMETIC; opcode: {opcode}; Rx: {Rx}; Ry: {Ry}; Rz/Imm8: {Rz_Imm8}")if opcode ==0: registers[Rx]= registers[Ry]+ registers[Rz_Imm8]elif opcode ==1: registers[Rx]= registers[Ry]+ Rz_Imm8elif opcode ==2: registers[Rx]= registers[Ry]- registers[Rz_Imm8]elif opcode ==3: registers[Rx]= registers[Ry]- Rz_Imm8elif opcode ==6: registers[Rx]= registers[Ry]| registers[Rz_Imm8]elif opcode ==7: registers[Rx]= registers[Ry]| Rz_Imm8elif opcode ==8: registers[Rx]= registers[Ry]^ registers[Rz_Imm8]elif opcode ==9: registers[Rx]= registers[Ry]^ Rz_Imm8elif opcode ==10: registers[Rx]= registers[Ry]& registers[Rz_Imm8]elif opcode ==11: registers[Rx]= registers[Ry]& Rz_Imm8elif opcode ==12: registers[Rx]= registers[Ry]<< registers[Rz_Imm8]elif opcode ==13: registers[Rx]= registers[Ry]>> registers[Rz_Imm8]# Memory Operationselif opcode ==14or opcode ==15: Rx =int(curr_instruction[6:11], 2) Ry =int(curr_instruction[11:16], 2) Rz =int(curr_instruction[19:], 2)# print(f"MEMORY OPERATION; opcode: {opcode}; Rx: {Rx}; Ry: {Ry}; Rz: {Rz}") addr =256* registers[Ry]+ registers[Rz]if opcode ==14: registers[Rx]= memory[addr]elif opcode ==15: memory[addr]= registers[Rx]# IO Device Communicationelif opcode ==21: Rx =int(curr_instruction[8:13], 2) Imm3 =int(curr_instruction[13:], 2)# print(f"IO; Rx: {Rx}; Imm3: {Imm3}")if Imm3 ==1: gpu_x = registers[Rx]elif Imm3 ==2: gpu_y = registers[Rx]elif Imm3 ==3: gpu[gpu_y][gpu_x] = registers[Rx]elif Imm3 ==4:# Draw bufferdisplay_screen(gpu)elif Imm3 ==5:# Return the number of bytes currently in serial buffer registers[Rx]=int(input("Number of bytes: "))elif Imm3 ==6:# Read 1 byte from serial buffer registers[Rx]=int(input("Byte: "))elif Imm3 ==7:# Write 1 byte to serial outprint(chr(registers[Rx]), end='')elif opcode ==23:# print("HLT") done =Trueelse: Imm16 =int(curr_instruction[8:], 2)# print(f"JUMP; opcode: {opcode}; Imm16: {Imm16}")if opcode ==24: registers[31]= pc pc = Imm16elif opcode ==25: pc = registers[31]elif opcode ==16: pc = Imm16elif opcode ==17:if fr &0b0001: pc = Imm16elif opcode ==18:if (fr &0b0001) ==0: pc = Imm16elif opcode ==19:if fr &0b0010: pc = Imm16elif opcode ==20:if (fr &0b0100>>2) != (fr &0b1000>>3): pc = Imm16elif opcode ==26:if (fr &0b0001) ==0and (fr &0b0100>>2) == (fr &0b1000>>3): pc = Imm16elif opcode ==27:if (fr &0b0010) ==0and (fr &0b0001) ==0: pc = Imm16else:raiseValueError("Unknown opcode", opcode)# 8 bitfor i inrange(len(registers)):while registers[i]>=256: registers[i]-=256while registers[i]<0: registers[i]=256-abs(registers[i])
Running the program, we're prompted for 3 bytes of user input (the "key").
➜ ARC6969 python3 arc6969.py
Hello fellow komrade.
Wanna capture the flag?
Enter the key:
After entering the key, some scrambled text is printed.
➜ ARC6969 python3 arc6969.py
Hello fellow komrade.
Wanna capture the flag?
Enter the key: Number of bytes: 3
Byte: 0
Byte: 0
Byte: 0
dL@CLòaï*õ99ýNO;ý8NüÿN:üQQý(
This probably means our key is wrong. Hmm... let's reverse engineer the program to understand what's going on. Running the program again, this time uncommenting all the print statements, we can track the program's execution.
First, R4 is cleared by AND-ing with 0. Then, the three bytes of user input are stored in R1, R0 and R2 respectively.
We can see that the 3-byte key is essentially used to perform the addition, XOR, and subtration operations on each byte of the flag. The loop continues until all 31 bytes of the flag are printed.
R6 = R6 + R1
R6 = R6 ^ R0
R6 = R6 - R2
Entering the key 0, 0, 0 gives us the "original" values. We can then bruteforce the key:
import itertoolskey_chars = [x for x inrange(256)]possible =list(itertools.product(key_chars, repeat=3))flag = [100, 76, 64, 67, 76, 242, 97, 239, 42, 245, 2, 57, 57, 253, 78, 79, 59, 253, 56, 78, 252, 4, 255, 4, 78, 58, 252, 81, 81, 253, 40]
i =0for key in possible: result =''for flag_char in flag: decoded = (((((flag_char + key[0]) %256) ^ key[1]) %256) - key[2]) %256 result +=chr(decoded)if'yauzactf'in result.lower():print(result) i +=1if i %100000==0:print("Progress:", i /len(possible))
The flag is YauzaCTF{H3ll0_fr0m_1969_k1dd0}.
The overflow flag can be a bit tricky - one trick is to check the sign of the compared operands and the result. If both Rx and Ry have the same sign, but Rx−Ry yields a different sign, then an overflow has occurred.