패킷 변조 대비하기

# 학습 과정 소개

서버와 클라이언트가 패킷을 주고 받을 때 데이터 변조가 발생해 크리에이터의 월드에 불편을 초래할 수 있습니다. 패킷 변조를 예방해 쾌적한 월드를 만들기 위해 패킷의 개념과 예방 방법을 알아봅시다.

##### 참고 가이드

  * [WorldConfig](/docs/?postId=1101)
  * [예시로 알아보는 서버 검증](/docs?postId=1084)



# 패킷 알아보기

패킷은 서버와 클라이언트가 주고받는 데이터의 단위를 말합니다. 메이플스토리 월드에서는 함수의 실행 공간에 따라 클라이언트와 서버가 통신해 데이터를 주고받을 수 있습니다. 이때 클라이언트와 서버는 데이터를 단순하게 주고받는 것이 아니라, 이를 담은 패킷으로 서로 통신합니다. 예를 들어 클라이언트에서 실행제어가 server인 함수를 호출한다고 생각해 봅시다. 이때 함수를 어떤 엔티티, 컴포넌트, 함수, 인자로 실행할지에 관한 정보를 패킷에 담아 서버로 전달합니다. 서버는 전달받은 정보대로 함수를 실행시키게 됩니다.

# 패킷 변조 알아보기

패킷 변조란 네트워크 통신 중 악의적으로 패킷을 변경하는 행위를 뜻합니다. 패킷 변조는 보통 네트워크로 전송된 패킷을 분석해 구조를 파악하고, 실제 데이터 부분을 변조하는 방식으로 이루어집니다.

메이플스토리 월드에서는 패킷의 엔티티, 컴포넌트, 함수, 인자, 대상 클라이언트 정보 등의 변조를 시도할 수 있습니다. 스크립트를 작성할 때 클라이언트 변조나 패킷 변조에 대비해 서버는 받은 데이터를 검증한 뒤 실제 데이터를 변경하도록 해야 합니다.

자주 발생하는 패킷 변조 방식은 패킷에서 특정 데이터의 값을 찾아 클라이언트에서 서버로 패킷을 전송할 때 값을 변경하는 것입니다. 이 패킷 변조 방식은 게임 재화 및 아이템 가치를 변동시켜 게임 생태계에 악의적인 영향을 끼칠 수 있습니다. 아래 그림과 같이 몬스터를 처지하면 보상으로 크리에이터가 설정한 메소, 경험치, 아이템을 얻게 만들었다고 생각해 봅시다. 이때 실제로 유저가 처치한 몬스터로부터 얻을 보상은 100 메소, 1000 경험치, Normal 아이템이지만 패킷 변조를 통해 그보다 훨씬 좋은 보상을 얻을 수 있습니다.

[png](1706080639140c27ad828ae0a49aca8cec1d0a1e9da34)

# 패킷 데이터 변조 1

패킷 변조 예방을 위해 패킷에 실제 골드, 경험치, 아이템 이름 데이터를 담아 보내는 방식은 지양하는 것이 좋습니다. 서버에 요청만 보내고 실제 데이터는 서버에서 결정하는 방식을 권장합니다. 이때 서버는 받은 패킷을 다시 한 번 검증한 뒤 적용하는 것도 중요합니다. 패킷 데이터 변조를 예방하는 자세한 방식은 [예시로 알아보는 서버 검증](/docs?postId=1084) 가이드를 참고합니다.

예시를 통해 엔티티와 컴포넌트를 변조하는 경우와 대처 방식을 소개합니다. 먼저 패킷 하나에 4가지 데이터를 담아 클라이언트에서 서버로 전송한다고 가정합니다.

  * 엔티티 이름: PlayerEntity
  * 컴포넌트 이름: MyPlayerDataComponent
  * 함수 이름: RequestGetCoin
  * 함수 인수: 500



[png](17116124998769b237ee7005a4bd7a9d464cf615f5e4c)

이때, 데이터를 악의적으로 변조하는 대표적인 2가지 예시입니다.

  1. RequestGetCoin 패킷을 반복 전송해 퀘스트를 완료하지 않아도 골드를 얻습니다.
     * Server에 지급 요청이 왔을 때 골드 획득 조건에 부합하는지 검증합니다. Client에서 온 요청을 검증 없이 반영하지 않습니다.
  2. 얻을 골드 수치에 해당하는 인수 값을 변경해 다량의 골드를 한 번에 얻습니다.
     * 실제 얻을 골드의 양을 클라이언트에서 보내지 않습니다. 서버에서 클라이언트로부터 온 요청과 맞는 값의 골드를 지급합니다.



#### 중요한 함수 server only 지정

클라이언트에서 서버로 패킷을 보낼 때 내용을 그대로 전송하게 되면, 변조 위험률이 높아집니다. 그러므로 중요한 함수는 실행제어를 server only로 설정해 패킷 반복 전송, 인자 값 변경 시도 등을 예방하는 것이 좋습니다. 또한 서버 함수는 클라이언트로부터 온 정보가 변조되지 않았는지 확인해야 합니다. 예를 들어 보상을 지급하는 함수를 작성해야 한다면 client → server → server only 함수 순서로 호출되도록 작성하는 것을 권장합니다. 이때 실제 보상을 지급하는 함수는 server only 함수여야 합니다.

100 메소 획득이란 내용 자체를 클라이언트에서 서버로 보낸다면 클라이언트를 변조해 더 많은 메소를 얻거나, 다른 클라이언트에게 지급할 수 있습니다. 패킷 변조를 예방하기 위해선 보상 지급 요청 패킷을 서버로 보내게 작성하고, 실제 지급 함수는 server only로 작성해야 합니다.
    
    
    @Component
    script RewardProcessor extends Component
        @ExecSpace("ClientOnly")
        method void ClickGetRewardButton()
        self:RewardProcess()
        end
    
        @ExecSpace("ServerOnly")
        method void RewardProcess()
        -- SendCoin() 함수를 호출하기 전, 반드시 올바른 요청이 맞는지 검증해야 합니다.
        self:SendCoin()
        end
    
        @ExecSpace("ServerOnly")
        method void SendCoin()
        -- 실제 지급 로직 
        end
    end
    

> Tip. 모든 로직의 실행제어를 server로 설정하는 것은 서버에 부하를 줄 수 있습니다. 애니메이션 재생과 같은 렌더링 처리는 client에서 처리하고, 재화나 아이템 같은 중요한 처리는 서버에서 하는 것을 권장합니다.

# 패킷 데이터 변조 2

또 다른 패킷 변조 방식으로는 플레이어 엔티티 이름 변조가 있습니다. 이 방식으로 실제로 특정 행동을 하지 않은 유저가 특정 행동을 완료하고 보상을 받을 수 있게 됩니다. 예를 들어 PlayerEntityA와 B 중 실제 플레이한 사람은 A이지만, 보상 지급은 B에게 될 수 있습니다. 플레이어 엔티티 이름이 변조되어 실제로 퀘스트를 수행한 플레이어가 아닌 다른 플레이어 엔티티에게 골드를 지급했기 때문입니다. 이를 예방하기 위해서는 로컬 클라이언트 외 다른 클라이언트에서 플레이어 엔티티의 서버 함수를 호출하지 못하도록 해야 합니다.

[png](17116125320499f54fab802ea4f0cbdbb81827d3955f7)

#### PlayerEntityAuthorityCheck 활용

WorldCofig의 PlayerEntityAuthorityCheck는 플레이어 엔티티에 속한 컴포넌트들의 서버 함수들을 로컬 클라이언트에만 호출하도록 합니다. 다른 클라이언트에서 서버 함수를 호출할 수 없기 때문에 다른 플레이어 엔티티에게 골드 지급하도록 패킷 변조를 할 수 없습니다. 만약 다른 플레이어 엔티티의 서버 함수 호출을 시도하면 아래와 같은 에러가 발생합니다.

[png](169571933009917b0b6c1d9994fe9a9e71fa7ff2dacbd)

> Tip. 로컬 플레이어 엔티티 여부는 클라이언트와 서버 양쪽에서 검증합니다.

PlayerEntityAuthorityCheck를 비활성화할 경우 실제로 값을 보내야 하는 플레이어 엔티티가 아닌 다른 플레이어 엔티티에 값을 전송해 보상이 다른 플레이어에게 갈 수 있습니다. 만약 PlayerEntityA가 몬스터를 사냥했다면 PlayerEntityA에게 메소를 추가하는 것이 정상 동작입니다. 하지만, 패킷의 엔티티 정보를 PlayerEntityB로 변조하면, PlayerEntityB의 서버 함수가 호출되어 PlayerEntityB에게 메소가 추가될 수 있습니다.

[png](17116125839800b14b16908dc40c3bdb3eb44b3777e0d)

#### Server 함수 호출자 senderUserId

senderUserId는 서버 함수의 인자로 서버 함수를 호출한 클라이언트의 userId 정보를 가지고 있습니다. senderUserId는 서버에서 부여하는 값으로 클라이언트에서 변조되지 않습니다. 예를 들어 월드의 유저A가 자신의 인벤토리에서 특정 아이템을 파기할 때는 아래와 같이 스크립트를 작성할 수 있습니다. 이 스크립트의 경우 다른 클라이언트에서 `RequestDropItem()` 함수를 호출하면, 유저A의 아이템이 삭제될 수 있습니다.
    
    
    @Component
    script DropRequestValidator extends Component
        @ExecSpace("ServerOnly")
        method void RequestDropItem(Entity targetItem)
        if self:HasItemInInventory(targetItem) then
            return
        end
        end
    end
    

특정 유저가 아이템을 파기할 때 다른 유저들의 아이템을 안전하게 지키기 위해선 senderUserId를 활용합니다. `RequestDropItem()` 함수를 호출한 유저와 인벤토리에서 아이템이 삭제될 유저가 일치하는지 알 수 있습니다. `RequestDropItem()` 함수를 로컬 클라이언트가 아닌 다른 클라이언트에서 호출할 경우 인벤토리에서 아이템을 삭제할 수 없도록 아래와 같이 작성할 수 있습니다.
    
    
    @Component
    script PlayerItemDropper extends Component
        @ExecSpace("ServerOnly")
        method void RequestDropItem(Entity targetItem)
        if senderUserId ~= self.Entity.PlayerComponent.UserId then
            return
        end
    
        if self:HasItemInInventory(targetItem) then
            self:RemoveItemInInventory(targetItem)
        end
        end
    end
    

# ServiceAuthorityCheck 활용

메이플스토리 월드는 크리에이터의 제작 편의성을 위해 Service 함수의 실행제어를 변경할 수 있도록 지원합니다. WorldConfig의 ServiceAuthorityCheck는 Service의 모든 서버 함수를 server only 함수로 동작하도록 변경할 수 있습니다. 예를 들어 TeleportService의 함수는 서버 함수로 플레이어 엔티티가 특정 플레이어 엔티티를 텔레포트 시킬 수 있습니다. 그러나 ServiceAuthorityCheck를 활성화하면 Teleportservice 함수는 server only로 변경되므로 클라이언트에서 호출할 수 없습니다. 그러므로 의도하지 않은 플레이어 엔티티가 텔레포트 되는 현상을 대비할 수 있습니다.

# RestrictedPlayerEntitySync 활용

WorldConfig의 RestrictedPlayerEntitySync를 활성화해 플레이어 엔티티에 속한 네이티브 컴포넌트의 Sync 프로퍼티가 클라이언트에서 변경된 값을 서버로 동기화하지 않도록 할 수 있습니다. 기본적으로 네이티브 컴포넌트의 Sync 프로퍼티는 서버에서 값이 변경될 때마다 모든 클라이언트로 동기화합니다. 반대로 클라이언트에서 변경된 값은 서버로 동기화하지 않습니다. 그러나 네이티브 컴포넌트가 플레이어 엔티티에 속한 경우 예외적으로 클라이언트에서 변경된 값을 서버로 동기화합니다. 이는 유저 입력이 있을 때 클라이언트에서 상태를 즉시 변경하기 위함입니다. 유저의 입력을 서버에서 먼저 처리하고, 결과를 클라이언트로 보내줄 때 유저는 입력 반응이 지연되는 것을 느낄 수 있기 때문입니다. 그러나 이 동기화 방식을 이용해 플레이어 엔티티의 데이터를 변조할 가능성이 있습니다. 예를 들어 채팅 UI가 없는 월드임에도 클라이언트에서 ChatBalloonComponent의 프로퍼티 값을 변조해 아바타에 말풍선을 띄울 수 있습니다.

> Tip. 아래 컴포넌트들은 플레이어 엔티티에 포함되더라도 입력 반응성을 위해 RestrictedPlayerEntitySync 영향을 받지 않습니다. 활성화 여부와 무관하게 플레이어 엔티티에 포함된 경우 항상 클라이언트에서 서버로 동기화가 이루어집니다.
> 
>   * MovementComponent
>   * PlayerControllerComponent
>   * StateComponent
>   * TransformComponent
>