ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [ARP spoofing] 3. Websocket protocol 파헤치기
    해킹/network 2023. 5. 31. 15:53

    이번 글은 ARP spoofing과는 큰 연관은 없기 때문에 websocket을 사용하지 않는 경우 참고하지 않아도 된다.

     


    Web Socket

    우리 프로젝트는 React와 Spring간 websocket을 활용해서 실시간 채팅을 구현하였다.

     

    사용 라이브러리는 Stomp이며, 굳이 Stomp를 사용하지 않아도 된다.

     

    우선 apic이라는 크롬 extension을 사용해서 websocket 테스트를 해보자.

    https://docs.apic.app/tester/test-websocket

     

    Test Websocket - apic docs

    To data/messages to ant exchange, click on the Send button in the Messages panel, specify the Destination exchange and message to be sent and click Send.

    docs.apic.app

     

     

    websocket 은 http로 connection을 맺은 후, websocket만의 프로토콜로 통신하게 된다.

     

    아래는 http connection을 하는 과정을 캡쳐한 것이다.

     

     

    이후 apic을 이용해 websocket 통신을 해보면 아래와 같이 캡쳐가 된다.

     

     

     

    여기서 websocket에 중요한 점 두 가지가 나온다.

    1. masking
    2. per-message compressed

     

    websocket은 payload를 암호화해서 통신한다. (정확히 말하면 암호화는 아니다. 이후 이유를 설명하겠다)

    클라이언트에서 서버로 가는 패킷은 반드시 masking을 해야하고, 반대의 경우 할 수도 있고 안할 수도 있다.

     

    패킷 내용을 보면 Mask : True라고 되어 있는데, 이 bit가 1로 되어 있다면 masking을 한다는 것이다.

    masking bit가 1로 되어있으면, masking key를 전달해준다. 위에서는 283er7114로 되어 있는 것을 볼 수 있다.

     

     

     

    또한 websocket은 데이터를 압축해서 보낼 수 있다. 압축을 하는 방법은 http connection을 맺을 때 전달된다. 

    위에서 캡쳐한 http 패킷을 보면, Sec-websocket-extensions : permessage-deflate ...라고 되어 있다.

    이렇게 클라이언트에서 전달하면 아래와 같이 서버에서 응답이 온다.

    살펴보면, sec-websocket-extensions: permessage-deflate, client_max_window_bits=15라고 나온다.

     

    순서를 보면, 클라이언트가 서버에게 압축 방법을 제시하고, 서버는 클라이언트에게 가능한 압축 방법을 확정지어 준다.

     

     

     

    이렇게 websocket은 압축과 masking을 사용해서, 내용을 보면 알아볼 수 없게 되어 있다..

     

    난독화된 데이터

     

    하지만 어째서인지 wireshark는 암호화된 패킷을 제대로 알아볼 수 있게 해석해준다.

     

    해독된 데이터

    이 이유는 masking은 암호화가 아니기 때문이다.

    정확히 말하면 난독화라고 말할 수 있다.

    위에서 websocket 헤더에 masking key가 존재하는 것을 확인해 보았을 것이다.

    이를 이용해서 난독화를 풀어 해석할 수 있다.

     

     

    Web socket unmask

     

     

    int unmask_data(u_char *buf, const u_int buf_len) {
        u_char payload_length = *(buf + 1) & 0x7f;
        u_char mask[4];
        int payload_start;
    
        if (payload_length < 126) {
            for (int i = 0; i < 4; i++) {
                mask[i] = *(buf + i + 2);
            }
            payload_start = 6;
        } else if (payload_length == 126) {
            printf("길이 너무 김..\n");
            return -1;
        } else {
            printf("길이 너무 김..\n");
            return -1;
        }
    
        // unmask payload
        for (int i = 0; i < payload_length; i++) {
            buf[i + payload_start] = buf[i + payload_start] ^ mask[i % 4];
        }
    
        return 1;
    }

     

    위에서 unmask payload하는 부분을 보자. masking key는 4byte로 이루어져 있으며, 한 바이트씩 payload와 xor 연산을 통해 masking된 데이터를 unmask한다.

     

    payload_length를 126 기준으로 나눈 이유는, websocket header를 자세히 보면 이해할 수 있다.

     

    여기서 기본적으로 payload len은 7bit가 할당된다. 만약 payload len이 126이라면, payload length는 확장된다. 127이라면, payload length는 더욱 확장된다.

    이렇게 확장된 payload length 뒤에 masking key가 오게 된다. (mask bit == 1일 때만)

    payload length에 따라 어느 위치에 masking key가 위치하는지 달라지게 되니, 길이를 잘 보고 코드를 작성하면 되겠다.

     

     

     

     

    Web socket decompress

    http connection을 할 때, permessage-deflate 헤더가 있었던 걸 기억해보자.

    이 헤더는 websocket의 payload를 어떻게 압축할 것인지 방법론을 제시한다.

    permessage-deflate는 압축하는 알고리즘의 일종이며, 대부분의 websocket은 이 압축방식을 이용한다.

    위에서 unmask하는 과정을 통해 데이터를 해독했지만, 아직 읽을 수 없는 데이터로 보인다.

    이는 데이터가 압축된 생태이기 때문인데, 이를 압축 해제할 필요가 있다.

     

     

    여기서 엄청난 삽질을 하게 됐는데..

     

    결론부터 말하면 Stomp방식의 websocket 통신은 압축을 해제하는 데에 실패했다.

    원본 데이터를 per-message deflate 방식으로 압축을 먼저 시도해보았다.

    하지만 구글에 나와있는 코드를 사용한 결과와, 실제로 wireshark에서 캡쳐되는 압축 결과가 달랐다.

    미묘하게 비슷한 부분이 있었는데, 그대로 압축 해제하는 코드를 적용시켜보니, 압축 데이터가 잘못 되었다는 오류를 뱉어낼 뿐이었다..

     

    stomp가 압축 방식이 다른걸까 생각이 들어 apic을 이용해서 기본 websocket으로 통신을 한 것을 캡쳐해보았다.

    기본 websocket의 원본 데이터를 압축해보니, 결과가 정말 비슷하게 나왔다.

    하지만 아직도 조금 다른 부분이 있었고, 차이가 나는 부분을 수정해보았다.

    아래 코드처럼 window bit를 설정하고 첫 byte에 1을 더하고, 마지막으로 payload의 끝에 0xffffffff를 더해보았다.

    // decompress websocket payload
    u_char compr_payload[payload_length + 6];
    u_char decompr_payload[payload_length + 6];
    int decomp_len = 0;
    memcpy(compr_payload, unmasked + 6, payload_length);
    compr_payload[0] = compr_payload[0] + 1;
    compr_payload[payload_length] = 0x00;
    compr_payload[payload_length + 1] = 0x00;
    compr_payload[payload_length + 2] = 0xff;
    compr_payload[payload_length + 3] = 0xff;
    compr_payload[payload_length + 4] = 0xff;
    compr_payload[payload_length + 5] = 0xff;
    
    decompress_data(compr_payload, payload_length + 6, decompr_payload, decomp_len);

    이 상태로 decompress를 해보니, 성공적으로 압축 해제가 되었다!

     

     

    하지만 이게 끝은 아니었다..

     

    첫 번째 데이터는 잘 압축해제가 되었지만, 두 번째 통신부터는 정상적으로 해제가 되지 않았다.

    왜 이러는걸까 생각을 해봤는데, 웹 소켓 통신은 stream으로 이어진다고 들었다.

    문득 비디오 압축 방법인 Spatial or Temporal Coding이 생각이 났는데, 하나의 stream으로 이어지는 통신이니까, 기존 데이터가 압축하는 데에 영향을 주지 않을까라는 생각이 들었다.

     

    아니나 다를까 똑같은 데이터를 연속해서 세 번 전달해보았는데, 점점 압축된 데이터가 작아지는 것을 확인할 수 있었다.

    즉, 압축을 해제할 때도 단순히 unmasked data를 가지고 압축 해제할 것이 아니라, 기존에 가지고 있던 데이터를 통해 압축 해제를 해야한다는 것이다.

    파일 압축에 대해 알아볼수록 내용이 너무 깊어서, 여기까지 알아보도록 하기로 했다.

    프로젝트 마무리 할 시간이 필요한데, 이미 삽질한 시간이 너무 많아 다음으로 넘어가기로 했다.

     

    위 내용은 삽질을 하면서 추론한 내용으로, 정확하지 않을 수 있다.

    따라서 잘못된 내용이나 문제가 있으면 적극적으로 알려주길 바란다.

     

     

    결론

    websocket을 unmask하는 것까진 성공했지만, 압축을 해제하는 부분은 부분적으로만 성공했다.

    아쉬운 부분이 많았으며 나중에 기회가 되면 stream 전체를 압축 해제하는 것까지 도전해보고 싶다.

    우선 arp project 가 중요하기 때문에 여기까지 하고 다음부터는 전체 코드를 살펴보며 어떻게 공격이 수행됐는지 알아보도록 하자.

    댓글

Designed by Tistory.