2022-11-21
This past weekend, I participated in SunshineCTF along with ProjectSEKAI. We were able to get full solves on all of the challenges across the board and place 1st!
I wrote about some of these challenges in a previous post. Below are some of the other challenges that I helped to solve.
This challenge is the 2nd in a series of related problems involving a custom CPU architecture and programs written for it. The first one was just an overview of the architecture and how to run sample programs.
You manage to connect to the JTAG interface of the console! However, the debug interface provided over JTAG is partially locked down and only provides non-intrusive debugging capabilities. Maybe that will be enough to bypass the password protection on the device?
Note: There are intentionally no files delivered as part of this challenge. You need to use the interactive debugger to solve it.
We were given a VM we could ssh into that provides pretty nice debugging interface for these Pegasus programs, which we could attach to a separate live instance of the challenge program.
Connect to the PEGASUS session on port 22700 with session ID '059dde68a35a605d'.
EAR debugger
(dbg) help
Available topics (type help <topic> to learn more):
breakpoint -- Setting and modifying breakpoints
running -- Controlling how a program runs and getting info
quit -- Exit the debugger and stop execution
(dbg)
We can set breakpoints at various addresses, step into individual instructions, hexdump data at arbitrary addresses, and more.
Onto the program, we have a fairly simple routine:
Thread state:
(ZERO)R0: 0000 (S1)R8: 0000
(A0)R1: 0000 (S2)R9: 0000
(RV/A1)R2: 0000 (FP/S3)R10: FB00
(RVX/A2)R3: 0000 (SP)R11: FB00
(A3)R4: 0000 (RA)R12: FFFF
(A4)R5: 0000 (RD)R13: FFFF
(A5)R6: EA2A (PC)R14: 0118
(S0)R7: 0000 (DPC)R15: 0000
FLAGS: Zspcvm
(dbg) dis 20
0118.0000: MOV S0, ZERO
011A.0000: RDB S1, (15) <-+
011C.0000: BRR.GE 0xC -----+ |
011F.0000: RDB A0, (0) | |
0121.0000: BRR.GE 0xC ----|-|-+
0124.0000: SUB A0, S1 | | |
0126.0000: ORR S0, A0 | | |
0128.0000: BRR 0xFFEF --|-+ |
012B.0000: MOV S0, S0 <-+ |
012D.0000: BRR.EQ 0x8 <--------+
|----------------------+
|
0130.0000: ADD A0, PC, 0xC |
0135.0000: BRR 0x5 -----------|-+
0138.0000: ADD A0, PC, 0xB | |
013D.0000: FCR 0xFFC0 | |
0140.0000: HLT <------------------+-+
I added some ascii arrows to try and help convey the program flow a bit better. Before describing the routine, I'll give a few brief notes about this architecture:
FLAGS
are set, and NOP
otherwise.RDB
is the "read byte" instruction, which reads from some port number specified like (0)
into the
register operand.BRR
is "branch relative", which will jump to a PC
-relative address. PC
points to the next
instruction.FCR
is "function call relative", which acts like BRR
but saves PC
into R12
(used as the
return address).The above routine reads in a byte one at a time from two places, from port 15 into S1
and from port 0 into A0
. I found out that port 0 is stdin, while port 15 is likely sourced from some file into the program. When RDB
fails to read data, it will set the carry flag C
to 1, and trigger any GE
-conditional instructions.
We can set a breakpoint at 0x121
in the debugger and sent a bunch of garbage data to the program instance and simply read off the values that pop up in S1
.
Reading all of these values and decoding to ASCII results in the flag: sun{d1d_y0u_u53_br34kp01nt5?}
Silicon Bridge is the classic thrilling game of chance! All you need to do is cross the bridge that's made up of two rows of silicon tiles. But be careful! At each step, one of the two silicon tiles will not support your weight! Can you make it to the end?
That's what the description on this game cartridge says, but Silicon Bridge seems impossible. Maybe there's some way to cheat?
We're given a program to run locally with the following setup:
$ echo 'sun{fake_local_flag}' > flag.txt
$ runpeg --plugin peg_cheat_checker.so SiliconBridge.peg [--debug] [--verbose] [--trace]
Upon running the program, we see what appears to be a variant of the Glass Stepping Stones challenge from Squid Game:
$ ./runpeg --plugin peg_cheat_checker.so SiliconBridge.peg
Enter cheat code?
Welcome to the Silicon Bridge game!
_______
|# # # #|
| # # # |
|# # # #|
|_______|
| | |
|[ ]|[ ]|
| | |
|[ ]|[ ]|
| | |
|[ ]|[ ]|
| | |
|[ ]|[ ]|
| | |
|[ ]|[ ]|
| | |
|[ ]|[ ]|
| | |
|[ ]|[ ]|
| | |
|[ ]|[ ]|
| | |
|[ ]|[ ]|
| | |
|[ ]|[ ]|
| | |
|[ ]|[ ]|
| | |
|[ ]|[ ]|
| | |
|[ ]|[ ]|
| | |
|[ ]|[ ]|
| | |
|[ ]|[ ]|
| | |
|___|___|
| @ |
|_______|
Pick a silicon panel to jump forwards to. [L/R]?
The essence of the game is to pick the right path, left or right, at each step successfully until you reach the end. There also seems to be a place to input a cheat code, but we'll get to that shortly.
I ended up running the program several times to determine the correct path, which seemed to be LLLRLRRRRLRLRRL
, but no matter which path you choose at the last step, the game will fail you.
I decided to try and detemine what the cheat code is supposed to be to see what functionality it might unlock.
We can run the Pegasus program with --trace
to print each instruction as they get executed. Doing so pauses the program when it tries to run RDB A3, (0)
at 0x0131
.
In a debugger, after we enter any character, it branches out to another routine:
Thread state:
(ZERO)R0: 0000 (S1)R8: 0000
(A0)R1: FAF4 (S2)R9: 0000
(RV/A1)R2: 000B (FP/S3)R10: FB00
(RVX/A2)R3: 0000 (SP)R11: FAF4
(A3)R4: 0055 (RA)R12: 0489
(A4)R5: 0000 (RD)R13: 0000
(A5)R6: EA2A (PC)R14: 011D
(S0)R7: 0000 (DPC)R15: 0000
FLAGS: zspcvm
(dbg) dis 200
011D.0000: INC A2, 1
011F.0000: STB [A0], A3
0121.0000: INC A0, 1
0123.0000: CMP A3, 0xA
0127.0000: MOV.EQ A1, A2
0129.0000: BRA.EQ RD, RA
012B.0000: CMP A2, A1
012D.0000: MOV.GE A1, A2
012F.0000: BRA.GE RD, RA
0131.0000: RDB A3, (0)
0133.0000: BRR.LT 0xFFE7
0136.0000: MOV A1, A2
0138.0000: BRA RD, RA
013A.0000: PSH {A0, RA-RD}
013D.0000: RDB A0, (15)
013F.0000: WRB.LT (0), A0
0141.0000: BRR.LT 0xFFF9
0144.0000: POP {A0, PC-DPC}
0147.0000: ADD.EQ ZERO, 0xE800
014B.0000: BRR.NE 0xDFA0
This routine essentially reads up to 13 (0xB
) additional characters from the user and
stores them in a buffer starting at 0xFAF4
.
(dbg) hexdump r 0xfaf4 16
Read violation at 0xFB00
faf4: 6865 7920 7468 6572 653a 2900 hey there:).
After reading all of the data, the routine jumps to a comparison routine:
Thread state:
(ZERO)R0: 0000 (S1)R8: 0000
(A0)R1: 043F (S2)R9: 0000
(RV/A1)R2: FAF4 (FP/S3)R10: FB00
(RVX/A2)R3: 000B (SP)R11: FAF4
(A3)R4: 000A (RA)R12: 0493
(A4)R5: 0000 (RD)R13: 0000
(A5)R6: EA2A (PC)R14: 040B
(S0)R7: 0000 (DPC)R15: 0000
FLAGS: zSPCvm
(dbg) dis 20
040B.0000: LDB A2, [A0]
040D.0000: INC A0, 1
040F.0000: LDB A3, [A1]
0411.0000: INC A1, 1
0413.0000: AND ZERO, A2, 0x80
0418.0000: BRR.EQ 0xD
041B.0000: AND A2, 0xFF7F
041F.0000: SUB A2, A3
0421.0000: BRR.EQ 0xFFE7
0424.0000: MOV A1, A2
0426.0000: BRA RD, RA
0428.0000: SUB A1, A2, A3
042B.0000: BRA RD, RA
This routine compares the buffer at 0x043F
with our input at 0xFAF4
. The string
in 0x043F
is what the file format describes as a "length encoded" string. Each byte in a string has its MSB set to 1, and the string continues up to and including a byte with its MSB set to 0.
Running hexdump r 0x043F 16
to get this string, and clearing the MSB yields "UUDDLRLRBA\n"
, which is the Konami Code.
Entering this cheat code into the program displays some new options now:
$ ./runpeg --plugin peg_cheat_checker.so SiliconBridge.peg
Enter cheat code?
UUDDLRLRBA
Address to patch?
abcd
Byte to replace with?
ef
It seems that we can choose an address within the program and patch it to be a different byte value! Given that we're unable to proceed with the stepping challenge at the last level, there is probably some conditional jump near the end that ought to be inverted to let us through. Let's find it!
I decided to run the program with --trace
to see at which point the program branches into a losing situation.
0329.0000: RDB A1, (0)
L <-- my input
032B.0000: HLT.GE
032C.0000: CMP A1, 0x4C <-- check for input 'L' (0x4C)
0330.0000: MOV.EQ A1, ZERO
0332.0000: BRR.EQ 0x9
033E.0000: RDB A2, (0) <-- another read
0340.0000: HLT.GE
0341.0000: CMP A2, 0xA <-- check for '\n'
0345.0000: HLT.NE
0346.0000: ADD A2, PC, 0xFDFF
034B.0000: LDW A2, [A2]
034D.0000: SRU A2, S0
034F.0000: AND A2, 0x1
0353.0000: CMP A2, A1
0355.0000: BRR.NE 0x12
0358.0000: ADD A3, PC, 0x2A3
035D.0000: LDW A4, [A3]
035F.0000: INC A4, 1
0361.0000: STW [A3], A4
0363.0000: INC S0, 1
0365.0000: CMP S0, S1
0367.0000: BRR.LT 0xFFB4
036A.0000: CMP S0, S1
036C.0000: BRR.GT 0xB
036F.0000: ADD A0, PC, 0xFF7F
0374.0000: FCR 0xFD89 <-- appears to be a print routine
0100.0000: MOV A0, A0
0102.0000: BRA.EQ RD, RA
0104.0000: MOV A3, 0x7F
0108.0000: LDB A1, [A0]
010A.0000: INC A0, 1
010C.0000: AND A2, A1, A3
010F.0000: WRB (0), A2 <-- prints some character
/0111.0000: CMP A1, A2 <-- printed '/'
I had to scroll up a lot to find the last character I entered since the Pegasus printing
routines are a bit lengthy, but it seems that the branch that takes us out of the
input checks and into a routine that prints /You Died/
happens at 0x036C
. We would
need to invert the condition of the BRR.GT
instruction to be BRR.LE
to let us pass through.
I had trouble trying to figure out how the opcode BRR.LE
should be formatted, since the provided architecture reference didn't specify the opcode format clearly enough, as trying different byte combinations didn't seem to work.
Looking through the conditions table in the reference showed me that a GE
condition will execute if the flags C=1
and Z=0
. Since I had access to a few other well-formatted BRR
instructions, I could try ones that set C=0
or Z=1
. BRR.EQ
works on Z=1
and BRR.LT
works on C=0
, so I decided to try those.
(dbg) hexdump r 0x0332 1
0332: 15 .
(dbg) c
Enter cheat code?
UUDDLRLRBA
Address to patch?
036C
Byte to replace with?
15
Welcome to the Silicon Bridge game!
// output omitted
You Win!
sun{fake_local_flag}
Success! Using these inputs on the live challenge gives us the flag: sun{l3t5_w4tch_4n_4ct10n_R3pl4y_0f_th4t_G4m3_5h4rk!}
Now you need to connect this console to the internet to see if it can talk to the old game services! Unfortunately, because it's such an old device, you need to help it figure out how to route its way through the Internet (basically a series of tubes) to reach your ISP. Oh, and also, your ISP uses advanced network security that automatically randomizes the path of connection packets every 30 seconds! Can you get this old console connected through all 5 network hops needed to achieve Internet access before the paths are randomized again? It might be difficult with no pixels to look at...
We're only given an endpoint to connect to. Connecting to it displays this information:
||| Challenge level 1/6 |||
You have 30 seconds to solve this level.
Here comes a PEGASUS program (reference id: enominode_yusabubipo) on the next line. Size: 4238 bytes (encoded as hex).
7f504547415355535f454152030001001000....(truncated)
Running now...
It seems that we're going to need to receive the program, run it, and solve it 6 times. Downloading one of the programs and running it shows no visible output, but we can notice a few interesting things in a debugger:
(dbg) context
Thread state:
(ZERO)R0: 0000 (S1)R8: 0000
(A0)R1: 0000 (S2)R9: 0000
(RV/A1)R2: 0000 (FP/S3)R10: FB00
(RVX/A2)R3: 0000 (SP)R11: FB00
(A3)R4: 0000 (RA)R12: FFFF
(A4)R5: 0000 (RD)R13: FFFF
(A5)R6: EA2A (PC)R14: 0100
(S0)R7: 0000 (DPC)R15: 0000
FLAGS: Zspcvm
Next instructions:
0100.0000: MOV DPC, 0x30
0104.0000: MOV PC, RA
0106.0000: MOV PC, RA
0108.0000: MOV PC, RA
010A.0000: MOV PC, RA
(dbg)
The very first instruction moves 0x30
into the DPC
register, which is the "delta
program counter". According to the architecture reference, the DPC
register modifies
how much the PC
register gets incremented by on each instruction execution. After the first instruction, we can get a much more coherent disassembly listing:
0104.0030: MOV A1, 0x4
01C8.0030: MOV A2, 0x0
028C.0030: MOV A3, 0x0
0350.0030: RDB A0, (0) <---------+
03B2.0030: CMP A0, 0x30 | <-- checks for input '0'
0476.0030: BRR.EQ 0x2DF --------+ |
0509.0030: CMP A0, 0x31 | | <-- checks for input '1'
05CD.0030: BRR.EQ 0x498 --------|-+ |
0660.0030: CMP A0, 0xA | | | <-- checks for input '\n'
0724.0030: BRR.EQ 0xFB99 -------|-|-+ <-- re-prompts for input
07B7.0030: HLT | |
07E8.0030: ADD A4, A2, A1 <--+ |
087B.0030: CMP A4, 0x7 | <-- checks A1 + A2 < 7
093F.0030: HLT.GE |
0970.0030: ADD A0, PC, 0xF69F |
0A65.0030: ADD PC, A0, A4 |
0AF8.0030: ADD A4, A3, A1 <----+
0B8B.0030: CMP A4, 0x7 <-- checks A1 + A2 < 7
0C4F.0030: HLT.GE
0C80.0030: MLU A0, A4, 0x7
0D75.0030: ADD A4, PC, 0xF29A
0E6A.0030: ADD PC, A0, A4 <-- onto the next stage
This routine prompts the user to enter a 0
or a 1
. Based on this choice,
the program will make a comparison between 7 and the sum of A1+A2
or A1+A3
respectively. If the sum is less than 7, we can progress to the next challenge
stage.
It seems that the initial values set for A1
,
A2
, and A3
vary between programs, and the values in later stages also vary. A strategy to try and bypass would be to just pick the minimum of A1+A2
and A1+A3
for each branch.
At this point in time, my teammate rik was able to script up a solution to solve the challenge that required much less introspection into the binary: simply bruteforce a solution path locally.
If we imagine the challenge as a binary tree with nodes representing
challenges, paths to children as 0
or 1
choices, and leaves representing
a 'win' or 'fail', we could just perform a depth-first search, and backtrack
when a path yields a failure.
We can implement this easily using pwntools by connecting to the remote instance with remote()
, launch a local process()
in an infinite loop until it returns a solution, then send the solution to the remote. Rinse & repeat 6 times to get the flag: sun{0ur_r0b0t1c_p3g4su5_0v3rl0rd5_w1ll_s4v3_us!pr0b4bly...}
This challenge was released as a "fixed" version of PEG_AUTOREV
due to the
challenge author realizing that there was an unintended bruteforce solution.
(Un)fortunately, this challenge suffered from the same bruteforceable solution.
The only difference in this challenge lies in the input that is required to take the different paths in the program:
0104.0030: MOV A1, 0x3
01C8.0030: MOV A2, 0x0
028C.0030: MOV A3, 0x0
0350.0030: RDB A0, (0)
03B2.0030: HLT.GE
03E3.0030: CMP A0, 0x44 <-- checks for 'D'
04A7.0030: BRR.EQ 0x2DF
053A.0030: CMP A0, 0x4D <-- checks for 'M'
05FE.0030: BRR.EQ 0x498
0691.0030: CMP A0, 0xA
...
Rather than checking for a '0' or '1', the program now checks for random
characters at different stages. Simply reading these values from the
disassembly output of the local process()
and using them for the path
selection was sufficient to solve this challenge, yielding the flag: sun{h0p3fully_th15_0n3_1s_h4rd3r_t0_brut3f0rc3_l0l}