Shabak challenge — Baby RISC
We are provided with c++ files which emulate a RISC processor. Main is as follows

The main program loads the user payload and the admin payload to be executed.
The payload consists of instructions defined in asm_instructions.c
Output operations:
PRINTC
to print the lower byte of a register as a character.PRINTDD
andPRINTDX
to print the value of a register in decimal or hexadecimal formats, respectively.PRINTNL
to print a newline.
Stack operations:
PUSH
andPOP
.PUSHCTX
andPOPCTX
.
Flow-control operations:
RET
, to terminate execution unconditionally.RETNZ
, to terminate execution if the given register is not zero.RETZ
, to terminate execution if the given register is zero.
Arithmetic operations
ADD
andADDI
AND
andANDI
DIV
andDIVI
- and so on..
Difference between instruction and instruction+i is that the latter is used to perform operations directly on values instead of using registers.
For eg: SUB R0, R1, R1 will calculate R1 - R1 and store the result in R0. as compared to SUBI R0, R1, 42 will calculate R1 - 42 and store the result in R0.

Similarly, registers are defined in asm_processor_state


So our method to read the flag can be found in the following snippet
tatic int generate_admin_code(uint8_t * payload, size_t max_size, size_t * payload_size_out)
{
int ret = E_SUCCESS;
char flag_string[MAX_FLAG_SIZE] = { 0 };
FILE * payload_fp = NULL;ret = read_flag(flag_string, sizeof(flag_string));
if (ret != E_SUCCESS)
{
printf("Failed to read flag.\n");
goto cleanup;
}payload_fp = fmemopen(payload, max_size, "w");
if (payload_fp == NULL)
{
ret = E_FOPEN;
goto cleanup;
}// Write admin shellcode to payload buffer
// (Because E_SUCCESS == 0, we just OR all the return values, to check for error when we finish).
ret = E_SUCCESS;// Pad out with newlines
for (size_t i = 0; i < 8; ++i)
{
ret |= file_write_opcode(payload_fp, PRINTNL);
}// If the user sets R0 so (R0 * 42) == 1 (impossible!), she deserves to read the flag
ret |= file_write_opcode_imm32(payload_fp, ADDI, ASM_REGISTER_R1, ASM_REGISTER_ZERO, 42);
ret |= file_write_opcode3(payload_fp, MUL, ASM_REGISTER_R2, ASM_REGISTER_R0, ASM_REGISTER_R1);
ret |= file_write_opcode_imm32(payload_fp, SUBI, ASM_REGISTER_R2, ASM_REGISTER_R2, 1);
ret |= file_write_opcode1(payload_fp, RETNZ, ASM_REGISTER_R2);// Print each 4-bytes of the flag as 4-characters
// (We might print some trailing null-characters if the flag length is not divisible by 4)
int32_t * flag_start = (int32_t *)flag_string;
int32_t * flag_end = (int32_t *)((char *)flag_string + strlen(flag_string));
for (int32_t * p = flag_start; p <= flag_end; ++p)
{
int32_t dword = *p;ret |= file_write_opcode_imm32(payload_fp, ADDI, ASM_REGISTER_R1, ASM_REGISTER_ZERO, dword);
for (size_t j = 0; j < 4; j++)
{
ret |= file_write_opcode1(payload_fp, PRINTC, ASM_REGISTER_R1);
ret |= file_write_opcode_imm32(payload_fp, ROR, ASM_REGISTER_R1, ASM_REGISTER_R1, 8);
}
}ret |= file_write_opcode(payload_fp, PRINTNL);
ret |= file_write_opcode(payload_fp, RET);// Check if some error (other than E_SUCCESS) was recieved during the admin code generation
if (ret != E_SUCCESS)
{
ret = E_ADMIN_CODE_ERR;
goto cleanup;
}// Success
long offset = ftell(payload_fp);
if (offset == -1)
{
ret = E_FTELL;
goto cleanup;
}
*payload_size_out = (size_t)offset;cleanup:
if (payload_fp != NULL)
{
fclose(payload_fp);
}
return ret;
}
As stated in comments, register R0 needst to be set so that R0 * 42 = 1
However, writing 1/42 is impossible(all registers are of type enum), so we will have to look for another way.
The basic assembly language version of the admin code is as follows
ADDI R1, ZERO, 42
MUL R2, R0, R1
SUBI R2, R2, 1
RETNZ R2
Notice the strange way values must be loaded into registers due to lack of an equivalent of MOV
.
Another flaw in the architecture is that ZERO
is a register. However, it is prohibited to write to zero

As we have noticed before, the memory for stack and registers are allocated in consecutive statemenets is asm_processor_state. So pusing an appropriate context will Set the ZERO register to our preferred value.
