Shabak challenge — Baby RISC

Anarta Poashan
4 min readJun 25, 2021

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 and PRINTDX to print the value of a register in decimal or hexadecimal formats, respectively.
  • PRINTNL to print a newline.

Stack operations:

  • PUSH and POP.
  • PUSHCTX and POPCTX.

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 and ADDI
  • AND and ANDI
  • DIV and DIVI
  • 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.

Instructions defined in header file asm_instructions.h

Similarly, registers are defined in asm_processor_state

Registers defined in asm_processor_state.h
Memory allocation in asm_processor_state.c

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

Writing to zero register returns an error

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.

Code snippet in definition of push instruction

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

No responses yet

Write a response