Skip to content

issue #11 ランキング登録の通信パケット改ざんの対策 #12

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@

public class SampleSceneManager : MonoBehaviour
{
// メモリを書き換えてハイスコアを改ざんするハッキングが存在します。
// unity-simple-ranking 内で取り回しているスコアのメモリは書き換えを検知するようにしていますが、
// このクラスの score は検知していません。
// Webアプリであれば書き換えは難しい気がしますが、PC/iOS/Androidなどであれば、暗号化・もしくは書き換えを
// 検知する仕組みにすると安全です。

public Text scoreText;
[NonSerialized] int score = 0;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ public class RankingLoader : MonoBehaviour
/// </summary>
[SerializeField] public RankingBoards RankingBoards;

/// <summary>
/// ハッシュ値による検証有効
/// ・検証無効であってもハッシュ値の計算・登録は行われます
/// </summary>
[SerializeField] public bool EnabledVerifyHash;

/// <summary>
/// 表示対象のボード
/// </summary>
Expand Down
65 changes: 65 additions & 0 deletions Assets/naichilab/unity-simple-ranking/Scripts/RankingRecord.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;


namespace naichilab
{
/// <summary>
/// ランキングのレコード
/// ・主にハッシュ値を作成する目的で用意しました
/// </summary>
public class RankingRecord
{
public string Name { get; set; }
public IScore Score { get; private set; }
public string Hash { get; private set; }

/// <summary>
/// コンストラクタ
/// ・DBから復元する場合はhashの引数があるものを使用します
/// </summary>
/// <param name="name"></param>
/// <param name="score"></param>
/// <param name="hash"></param>
public RankingRecord(string name, IScore score)
{
Name = name;
Score = score;
Hash = CalcHash();
}
public RankingRecord(string name, IScore score, string hash)
{
Name = name;
Score = score;
Hash = hash;
}

/// <summary>
/// ハッシュ計算
/// ・計算が推測できないよう、通信パケットに含まれていないクライアントキーを混ぜ合わせています
/// </summary>
/// <returns></returns>
string CalcHash()
{
return (Name + Score.TextForSave + NCMB.NCMBSettings.ClientKey).ToHMACSHA256();
}

/// <summary>
/// ハッシュ値の検証
/// </summary>
/// <returns>true:OK、false:NG</returns>
public bool VerifyHash()
{
return CalcHash() == Hash;
}

/// <summary>
/// ハッシュ再計算
/// </summary>
public void RefreshHash()
{
Hash = CalcHash();
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,25 @@
using NCMB;
using NCMB.Extensions;


namespace naichilab
{
public class RankingSceneManager : MonoBehaviour
{
private const string OBJECT_ID = "objectId";
private const string COLUMN_SCORE = "score";
private const string COLUMN_NAME = "name";
private const string COLUMN_HASH = "hash";

// 表示するランキングデータの行数
private const int MAX_VIEW_ROW = 30;

// 読み込むランキングデータの行数
// ・ハッシュ値による検証有効時のみ使用します。
// 通信パケット改ざんが行われた場合、サーバ側ではじかない限りデータが登録されることは避けられません。
// 表示行数分だけ読み込むと、不正なデータがはじかれた結果、表示に必要なデータが足りなくなってしまいます。
// 仕方なく読み込み行数を増やしておくことで対応しておきます。
private const int MAX_READ_ROW = 60;

[SerializeField] Text captionLabel;
[SerializeField] Text scoreLabel;
Expand All @@ -38,17 +49,27 @@ private string ObjectID
PlayerPrefs.SetString(BoardIdPlayerPrefsKey, _objectid = value);
}
}
private void CrearObjectID()
{
PlayerPrefs.SetString(BoardIdPlayerPrefsKey, null);
}


private string BoardIdPlayerPrefsKey
{
get { return string.Format("{0}_{1}_{2}", "board", _board.ClassName, OBJECT_ID); }
}

private RankingInfo _board;
private IScore _lastScore;
private RankingRecord _lastScoreRecord; // メモリ書き換えを警戒してハッシュ値を含むレコードで保持しています

private NCMBObject _ncmbRecord;

private bool EnabledVerifyHash
{
get { return RankingLoader.Instance.EnabledVerifyHash; }
}

/// <summary>
/// 入力した名前
/// </summary>
Expand All @@ -70,7 +91,7 @@ void Start()
{
sendScoreButton.interactable = false;
_board = RankingLoader.Instance.CurrentRanking;
_lastScore = RankingLoader.Instance.LastScore;
_lastScoreRecord = new RankingRecord("default", RankingLoader.Instance.LastScore); // 後で書き換えるので名前は適当に登録しています

Debug.Log(BoardIdPlayerPrefsKey + "=" + PlayerPrefs.GetString(BoardIdPlayerPrefsKey, null));

Expand All @@ -79,10 +100,11 @@ void Start()

IEnumerator GetHighScoreAndRankingBoard()
{
scoreLabel.text = _lastScore.TextForDisplay;
scoreLabel.text = _lastScoreRecord.Score.TextForDisplay;
captionLabel.text = string.Format("{0}ランキング", _board.BoardName);

//ハイスコア取得
RankingRecord hiScoreRecord = null;
{
highScoreLabel.text = "取得中...";

Expand All @@ -96,8 +118,16 @@ IEnumerator GetHighScoreAndRankingBoard()
_ncmbRecord = hiScoreCheck.Result.First();

var s = _board.BuildScore(_ncmbRecord[COLUMN_SCORE].ToString());
highScoreLabel.text = s != null ? s.TextForDisplay : "エラー";

if (s != null) {
hiScoreRecord = new RankingRecord(_ncmbRecord[COLUMN_NAME].ToString(),
s,
_ncmbRecord.ContainsKey(COLUMN_HASH) ? _ncmbRecord[COLUMN_HASH].ToString() : "");
if (EnabledVerifyHash && !hiScoreRecord.VerifyHash()) {
hiScoreRecord = null;
}
}

highScoreLabel.text = hiScoreRecord != null ? hiScoreRecord.Score.TextForDisplay : "エラー";
nameInputField.text = _ncmbRecord[COLUMN_NAME].ToString();
}
else
Expand All @@ -111,26 +141,24 @@ IEnumerator GetHighScoreAndRankingBoard()
yield return StartCoroutine(LoadRankingBoard());

//スコア更新している場合、ボタン有効化
if (_ncmbRecord == null)
if (hiScoreRecord == null)
{
sendScoreButton.interactable = true;
}
else
{
var highScore = _board.BuildScore(_ncmbRecord[COLUMN_SCORE].ToString());

if (_board.Order == ScoreOrder.OrderByAscending)
{
//数値が低い方が高スコア
sendScoreButton.interactable = _lastScore.Value < highScore.Value;
sendScoreButton.interactable = _lastScoreRecord.Score.Value < hiScoreRecord.Score.Value;
}
else
{
//数値が高い方が高スコア
sendScoreButton.interactable = highScore.Value < _lastScore.Value;
sendScoreButton.interactable = hiScoreRecord.Score.Value < _lastScoreRecord.Score.Value;
}

Debug.Log(string.Format("登録済みスコア:{0} 今回スコア:{1} ハイスコア更新:{2}", highScore.Value, _lastScore.Value,
Debug.Log(string.Format("登録済みスコア:{0} 今回スコア:{1} ハイスコア更新:{2}", hiScoreRecord.Score.Value, _lastScoreRecord.Score.Value,
sendScoreButton.interactable));
}
}
Expand All @@ -143,6 +171,11 @@ public void SendScore()

private IEnumerator SendScoreEnumerator()
{
if (EnabledVerifyHash && !_lastScoreRecord.VerifyHash()) { // メモリが書き換えられてないかのチェック
highScoreLabel.text = "検証エラー";
yield break;
}

sendScoreButton.interactable = false;
highScoreLabel.text = "送信中...";

Expand All @@ -154,7 +187,10 @@ private IEnumerator SendScoreEnumerator()
}

_ncmbRecord[COLUMN_NAME] = InputtedNameForSave;
_ncmbRecord[COLUMN_SCORE] = _lastScore.Value;
_ncmbRecord[COLUMN_SCORE] = _lastScoreRecord.Score.Value;
_lastScoreRecord.Name = InputtedNameForSave; // 名前変更
_lastScoreRecord.RefreshHash(); // ハッシュ再計算
_ncmbRecord[COLUMN_HASH] = _lastScoreRecord.Hash;
NCMBException errorResult = null;

yield return _ncmbRecord.YieldableSaveAsync(error => errorResult = error);
Expand All @@ -163,13 +199,14 @@ private IEnumerator SendScoreEnumerator()
{
//NCMBのコンソールから直接削除した場合に、該当のobjectIdが無いので発生する(らしい)
_ncmbRecord.ObjectId = null;
CrearObjectID();
yield return _ncmbRecord.YieldableSaveAsync(error => errorResult = error); //新規として送信
}

//ObjectIDを保存して次に備える
ObjectID = _ncmbRecord.ObjectId;

highScoreLabel.text = _lastScore.TextForDisplay;
highScoreLabel.text = _lastScoreRecord.Score.TextForDisplay;

yield return StartCoroutine(LoadRankingBoard());
}
Expand All @@ -193,7 +230,7 @@ private IEnumerator LoadRankingBoard()
MaskOffOn();

var so = new YieldableNcmbQuery<NCMBObject>(_board.ClassName);
so.Limit = 30;
so.Limit = EnabledVerifyHash ? MAX_READ_ROW : MAX_VIEW_ROW;
if (_board.Order == ScoreOrder.OrderByAscending)
{
so.OrderByAscending(COLUMN_SCORE);
Expand All @@ -217,6 +254,17 @@ private IEnumerator LoadRankingBoard()
int rank = 0;
foreach (var r in so.Result)
{
if (rank >= MAX_VIEW_ROW) break;

IScore highScore = _board.BuildScore(r[COLUMN_SCORE].ToString());
if (highScore == null) continue;

// ハッシュ検証
RankingRecord highScoreRecord = new RankingRecord(r[COLUMN_NAME].ToString(),
highScore,
r.ContainsKey(COLUMN_HASH) ? r[COLUMN_HASH].ToString() : "");
if (EnabledVerifyHash && !highScoreRecord.VerifyHash()) continue;

var n = Instantiate(rankingNodePrefab, scrollViewContent);
var rankNode = n.GetComponent<RankingNode>();
rankNode.NoText.text = (++rank).ToString();
Expand Down
47 changes: 47 additions & 0 deletions Assets/naichilab/unity-simple-ranking/Scripts/StringExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Security.Cryptography;
using System.Text;

namespace naichilab
{
// <summary>
/// string型の拡張メソッド
/// </summary>
public static class StringExtensions
{
/// <summary>
/// 空チェック
/// </summary>
/// <returns></returns>
public static bool IsNullOrEmpty(this string self)
{
return string.IsNullOrEmpty(self);
}

/// <summary>
/// ハッシュ化(HMACSHA256)
/// 参考:http://hensa40.cutegirl.jp/archives/4066
/// 環境的に SHA256CryptoServiceProvider が存在しないので HMACSHA256 を使用
/// </summary>
/// <returns></returns>
public static string ToHMACSHA256(this string self)
{
// パスワードをUTF-8エンコードでバイト配列として取り出す
byte[] byteValues = System.Text.Encoding.UTF8.GetBytes(self);

// HMACSHA256のハッシュ値を計算する
HMACSHA256 crypto256 = new HMACSHA256(byteValues);
byte[] hash256Value = crypto256.ComputeHash(byteValues);

// HMACSHA256の計算結果をUTF8で文字列として取り出す
StringBuilder hashedText = new StringBuilder();
for (int i = 0; i < hash256Value.Length; i++) {
// 16進の数値を文字列として取り出す
hashedText.AppendFormat("{0:X2}", hash256Value[i]);
}
return hashedText.ToString();
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.