Posts Decompetition CTF - batsounds
Post
Cancel

Decompetition CTF - batsounds

In this post we will be constructing the source code for batsounds

1
2
3
4
5
6
7
8
9
10
11
12
13
.text:004E34B1    lea     rax, aTcp       ; "tcp"
.text:004E34B8    mov     [rsp+80h+var_80], rax
.text:004E34BC    mov     [rsp+80h+var_78], 3
.text:004E34C5    lea     rax, a20080     ; ":20080"
.text:004E34CC    mov     [rsp+80h+var_70], rax
.text:004E34D1    mov     [rsp+80h+var_68], 6
.text:004E34DA    call    net_Listen
.text:004E34DF    mov     rax, [rsp+80h+var_58] ; listener.data
.text:004E34E4    mov     [rsp+80h+var_28], rax
.text:004E34E9    mov     rcx, [rsp+80h+var_60] ; listener.type
.text:004E34EE    mov     [rsp+80h+var_40], rcx
.text:004E34F3    cmp     [rsp+80h+var_50], 0   ; error.type
.text:004E34F9    jnz     loc_4E3673

net.Listen is called specifying a TCP socket that will listen on port 20080. The Listener instance is stored in var_28 and it’s type is in var_40. If Listen fails, then log.Fatalln is called.

1
2
3
4
svr, err := net.Listen("tcp", ":20080")
if err != nil {
    log.Fatalln("Unable to bind to port")
}
1
2
3
4
5
6
7
8
9
10
.text:004E34FF    lea     rdx, off_553C80
.text:004E3506    cmp     rcx, rdx
.text:004E3509    jnz     loc_4E36C2
.text:004E36C2 loc_4E36C2:
.text:004E36C2    mov     [rsp+80h+var_80], rcx
.text:004E36C6    lea     rax, stru_51EE20
.text:004E36CD    mov     [rsp+80h+var_78], rax
.text:004E36D2    lea     rax, stru_50A140
.text:004E36D9    mov     [rsp+80h+var_70], rax
.text:004E36DE    call    runtime_panicdottypeI

rcx contains the type of the Listener returned by the Listen function. If the type does not match the expected type of the listener (which is present at off_553C80), runtime_panicdottypeI is called. But panicdottypeE is called when doing an e.(T) conversion and the conversion fails. So, we have

1
listener := svr.(T)

Where T is the type at off_553C80

1
2
3
4
5
6
7
.rodata:00553C80 off_553C80
.rodata:00553C80    dq offset stru_50A140   ; interfaceType
.rodata:00553C88    dq offset stru_51EE20   ; underlying type
.rodata:00553C90    dq 0A353F449h           ; hash
.rodata:00553C98    dq offset net__ptr_TCPListener_Accept
.rodata:00553CA0    dq offset net__ptr_TCPListener_Addr
.rodata:00553CA8    dq offset net__ptr_TCPListener_Close

Can you recognize this structure? Yes, it’s an itab

So, we are looking at an interface that implements TCPListener.Accept, TCPListener.Addr and TCPListener.Close. Can you guess the underlying type? It’s *TCPListener

1
listener := svr.(*net.TCPListener)
1
2
3
4
5
6
7
8
9
10
11
12
.text:004E350F    call    time_Now
.text:004E3514    mov     [rsp+80h+var_68], 100000000
.text:004E351D    call    time_Time_Add
.text:004E3522    mov     rax, [rsp+80h+var_60]
.text:004E3527    mov     rcx, [rsp+80h+var_58]
.text:004E352C    mov     rdx, [rsp+80h+var_50]
.text:004E3531    mov     rbx, [rsp+80h+var_28]
.text:004E3536    mov     [rsp+80h+var_80], rbx
.text:004E353A    mov     [rsp+80h+var_78], rax
.text:004E353F    mov     [rsp+80h+var_70], rcx
.text:004E3544    mov     [rsp+80h+var_68], rdx
.text:004E3549    call    net__ptr_TCPListener_SetDeadline

time.Now() returns a Time instance.

1
2
3
4
5
type Time struct {
    wall uint64
    ext int64
    loc *Location
}

time.Add adds 1e8 ns (0.1s) to current time and uses it as deadline for listener

1
2
listener.SetDeadline(time.Now().Add(100*time.Millisecond))
log.Println("Listening on 0.0.0.0:20080")
1
2
3
4
5
6
7
8
9
10
11
12
.text:004E358E    mov     rax, [rsp+80h+var_40] ; type of listener (itab)
.text:004E3593    mov     rax, [rax+18h]        ; first method
.text:004E3597    mov     rcx, [rsp+80h+var_28] ; instance to be used
.text:004E359C    mov     [rsp+80h+var_80], rcx
.text:004E35A0    call    rax
.text:004E35A2    mov     rax, [rsp+80h+var_78] ; Conn.type
.text:004E35A7    mov     [rsp+80h+var_38], rax
.text:004E35AC    mov     rcx, [rsp+80h+var_70] ; Conn.data
.text:004E35B1    mov     [rsp+80h+var_20], rcx
.text:004E35B6    mov     rdx, [rsp+80h+var_68] ; error.type
.text:004E35BB    mov     [rsp+80h+var_30], rdx
.text:004E35C0    cmp     rdx, 0

var_40 points to the listener’s itab, and the method at offset +18h is net__ptr_TCPListener_Accept. The cmp instruction is useless here, maybe the optimizer missed it

1
2
conn, err := listener.Accept()
log.Println("Received connection...")

Wait! Should we call Accept on listener which has the type *net.TCPListener? If we do, Go knows that we are calling accept on a concrete type, it calls TCPListener.Accept directly. But we need to make an indirect call, and that is only possible through an interface type, not a concrete type. So, we must use

1
2
conn, err := svr.Accept()
log.Println("Received connection...")
1
2
3
4
5
6
7
8
.text:004E3604    mov     rax, [rsp+80h+var_30]
.text:004E3609    cmp     rax, 0
.text:004E360D    jnz     short loc_4E3631  ; error is not nil
.text:004E360F    mov     rax, [rsp+80h+var_38]
.text:004E3614    mov     [rsp+80h+var_80], rax
.text:004E3618    mov     rax, [rsp+80h+var_20]
.text:004E361D    mov     [rsp+80h+var_78], rax
.text:004E3622    call    main_echo

Nothing to explain here..

1
2
3
4
if err != nil {
    log.Fatalln("Unable to accept connection")
}
echo(conn)

Lets summarize main function:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func main() {
    svr, err := net.Listen("tcp", ":20080")
    if err != nil {
        log.Fatalln("Unable to bind to port")
    }
    listener := svr.(*net.TCPListener)
    listener.SetDeadline(time.Now().Add(100*time.Millisecond))
    log.Println("Listening on 0.0.0.0:20080")
    conn, err := svr.Accept()
    log.Println("Received connection...")
    if err != nil {
        log.Fatalln("Unable to accept connection")
    }
    echo(conn)
}

Moving on to echo

1
2
3
4
5
6
7
8
9
10
.text:004E311F    mov     rax, [rsp+0D0h+arg_0]
.text:004E3127    test    [rax], al
.text:004E3129    mov     [rsp+0D0h+var_90], 18h
.text:004E3131    lea     rcx, [rax+18h]    ; first method of Conn
.text:004E3135    mov     [rsp+0D0h+var_78], rcx
.text:004E313A    mov     rcx, [rsp+0D0h+arg_8]
.text:004E3142    mov     [rsp+0D0h+var_60], rcx
.text:004E3147    lea     rdx, [rsp+0D0h+var_90]
.text:004E314C    mov     [rsp+0D0h+var_D0], rdx
.text:004E3150    call    runtime_deferprocStack

The test instruction is useless. I don’t understand why it’s present. deferprocStack queues a new deferred function with a _defer record on the stack.

1
2
3
4
5
6
7
8
9
10
type _defer struct {
    siz     int32       // +00
    started bool        // +04
    heap    bool        // +05
    openDefer bool      // +06
    sp        uintptr   // +08
    pc        uintptr   // +10
    fn        *funcvals // +18
    // ...
}

fn is at offset +18h and it contains rax+18h, the first method of Conn interface (sorted alphabetically by name), which is Close(). So this entire block is

1
2
3
4
func echo(conn net.Conn) {
    defer conn.Close()
    // ...
}

A slice of 20 bytes is allocated (var_40) and a loop is executed

1
2
3
4
5
6
7
8
9
10
11
12
13
14
.text:004E3196    mov     rcx, [rsp+0D0h+arg_0] ; type Conn
.text:004E319E    mov     rdx, [rcx+28h]        ; third method of Conn
.text:004E31A2    mov     rbx, [rsp+0D0h+arg_8] ; Conn instance
.text:004E31AA    mov     [rsp+0D0h+var_D0], rbx
.text:004E31AE    mov     [rsp+0D0h+var_C8], rax
.text:004E31B3    mov     [rsp+0D0h+var_C0], 14h
.text:004E31BC    mov     [rsp+0D0h+var_B8], 14h
.text:004E31C5    call    rdx
.text:004E31C7    mov     rcx, [rsp+0D0h+var_B0]
.text:004E31CC    mov     [rsp+0D0h+var_98], rcx
.text:004E31D1    mov     rax, [rsp+0D0h+var_A0]
.text:004E31D6    mov     rdx, [rsp+0D0h+var_A8]
.text:004E31DB    cmp     cs:error_type_EOF, rdx
.text:004E31E2    jz      loc_4E33E8
1
2
3
4
5
6
7
8
9
10
type Conn interface {
	Read(b []byte) (n int, err error)
	Write(b []byte) (n int, err error)
	Close() error
	LocalAddr() Addr
	RemoteAddr() Addr
	SetDeadline(t time.Time) error
	SetReadDeadline(t time.Time) error
	SetWriteDeadline(t time.Time) error
}

Let’s sort them by their names…

1
2
3
4
5
6
7
8
9
10
type Conn interface {
	Close() error
	LocalAddr() Addr
	Read(b []byte) (n int, err error)
	RemoteAddr() Addr
	SetDeadline(t time.Time) error
	SetReadDeadline(t time.Time) error
	SetWriteDeadline(t time.Time) error
	Write(b []byte) (n int, err error)
}

.text:004E31C5 calls Read() on the conn instance using the buffer of 20 bytes. The number of bytes read is stored in var_98. .text:004E31DB checks if the error returned by read is EOF. Comparing the type pointers is a weak check, which if fails, runtime.ifaceeq is used to ensure that the error is really a EOF.

1
2
3
4
5
6
7
8
9
10
11
12
13
func echo(conn net.Conn) {
    defer conn.Close()
    buf := make([]byte, 20)
    for {
        n, err := conn.Read(buf)
        if err == io.EOF {
            log.Println("Client disconnected")
            break
        }
        // ...
    }
    // ...
}
1
2
3
4
5
.text:004E31E8    cmp     rcx, 14h
.text:004E31EC    ja      loc_4E347A
.text:004E347A loc_4E347A:
.text:004E347A    mov     edx, 14h
.text:004E347F    call    runtime_panicSliceAcap

Anything weird? runtime_panicSliceAcap is present to ensure that a slice never goes out of bounds, followed by two log.Printf’s

Till now, we have got

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func echo(conn net.Conn) {
    defer conn.Close()
    buf := make([]byte, 20)
    for {
        n, err := conn.Read(buf)
        if err == io.EOF {
            log.Println("Client disconnected")
            break
        }
        packet := buf[:n]
        log.Printf("Received %d bytes: %s", n, string(packet))
        log.Printf("Writing %d bytes of data", n)
        // ...
    }
    // ...
}
1
2
3
4
5
6
7
8
9
10
11
12
.text:004E3340    mov     rax, [rsp+0D0h+arg_0]     ; type Conn
.text:004E3348    mov     rcx, [rax+50h]            ; 8th method of Conn
.text:004E334C    mov     rdx, [rsp+0D0h+arg_8]
.text:004E3354    mov     [rsp+0D0h+var_D0], rdx
.text:004E3358    mov     rbx, [rsp+0D0h+var_40]
.text:004E3360    mov     [rsp+0D0h+var_C8], rbx
.text:004E3365    mov     rsi, [rsp+0D0h+var_98]
.text:004E336A    mov     [rsp+0D0h+var_C0], rsi
.text:004E336F    mov     [rsp+0D0h+var_B8], 14h
.text:004E3378    call    rcx
.text:004E337A    cmp     [rsp+0D0h+var_A8], 0      ;  error type is nil?
.text:004E3380    jz      loc_4E318E

Recall that the 8th method of Conn is Write(). Now we have

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
func echo(conn net.Conn) {
    defer conn.Close()
    buf := make([]byte, 20)
    for {
        n, err := conn.Read(buf)
        if err == io.EOF {
            log.Println("Client disconnected")
            break
        }
        packet := buf[:n]
        log.Printf("Received %d bytes: %s", n, string(packet))
        log.Printf("Writing %d bytes of data", n)
        n, err = conn.Write(packet)
        if err != nil {
            log.Println("Unable to write data")
            break
        }
    }
}

func main() {
    svr, err := net.Listen("tcp", ":20080")
    if err != nil {
        log.Fatalln("Unable to bind to port")
    }
    listener := svr.(*net.TCPListener)
    listener.SetDeadline(time.Now().Add(100*time.Millisecond))
    log.Println("Listening on 0.0.0.0:20080")
    conn, err := svr.Accept()
    log.Println("Received connection...")
    if err != nil {
        log.Fatalln("Unable to accept connection")
    }
    echo(conn)
}

Let’s submit it. For echo, we get the diff

1
2
3
4
5
6
7
8
9
10
   call    runtime.slicebytetostring
-  mov     rax, [rsp+0x20]
-  mov     rcx, [rsp+0x28]
-  mov     [rsp], rax
-  mov     [rsp+8], rcx
+  mov     rax, [rsp+0x28]
+  mov     rcx, [rsp+0x20]
+  mov     [rsp], rcx
+  mov     [rsp+8], rax
   call    runtime.convTstring

WTF? Register swap! Lets try removing the temporary variable packet

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
func echo(conn net.Conn) {
    defer conn.Close()
    buf := make([]byte, 20)
    for {
        n, err := conn.Read(buf)
        if err == io.EOF {
            log.Println("Client disconnected")
            break
        }
        log.Printf("Received %d bytes: %s", n, string(buf[:n]))
        log.Printf("Writing %d bytes of data", n)
        n, err = conn.Write(buf[:n])
        if err != nil {
            log.Println("Unable to write data")
            break
        }
    }
}

func main() {
    svr, err := net.Listen("tcp", ":20080")
    if err != nil {
        log.Fatalln("Unable to bind to port")
    }
    listener := svr.(*net.TCPListener)
    listener.SetDeadline(time.Now().Add(100*time.Millisecond))
    log.Println("Listening on 0.0.0.0:20080")
    conn, err := svr.Accept()
    log.Println("Received connection...")
    if err != nil {
        log.Fatalln("Unable to accept connection")
    }
    echo(conn)
}

Superb! Now we have an exact match for echo

Final Code

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