<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>_ = F = _.log</title>
        <link>https://velog.io/</link>
        <description>Developer who loves pixels and makes game :D</description>
        <lastBuildDate>Fri, 27 Oct 2023 11:19:40 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>_ = F = _.log</title>
            <url>https://velog.velcdn.com/images/f_works/profile/ba5e5417-8442-41d5-8320-ec4838045975/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. _ = F = _.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/f_works" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[세이브 파일을 보호하기]]></title>
            <link>https://velog.io/@f_works/%EC%84%B8%EC%9D%B4%EB%B8%8C-%ED%8C%8C%EC%9D%BC%EC%9D%84-%EB%B3%B4%ED%98%B8%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@f_works/%EC%84%B8%EC%9D%B4%EB%B8%8C-%ED%8C%8C%EC%9D%BC%EC%9D%84-%EB%B3%B4%ED%98%B8%ED%95%98%EA%B8%B0</guid>
            <pubDate>Fri, 27 Oct 2023 11:19:40 GMT</pubDate>
            <description><![CDATA[<p>게임에서 <code>세이브 파일</code>은 플레이어의 진행상황을 기록해주기 때문에 정말 중요한 파일입니다. 하지만 누군가가 이 파일에 접근해서 값을 수정해버린다면 손쉽게 아이템을 얻거나 게임의 일부 구간을 건너뛸 수 있습니다. 어쩌면 치명적인 버그가 발생할지도 모르죠.</p>
<p>저의 경우에는 도전과제와 같은 업적 시스템을 넣을 것이었기에 이러한 파일 수정을 막는 방법이 필요했습니다. 단순히 저만의 암호화 알고리즘을 만들어서 적용시킬까 싶기도 했지만 누군가 게임을 뜯어서 코드를 분석하면 금방 들통날 것이기에 해결방법이 되지 않았습니다.</p>
<br/>

<p>일단 <strong>최소한의 보호 장치</strong>를 만들기 위해 <strong>다음과 같은 목표</strong>를 세웠습니다.</p>
<blockquote>
<ol>
<li>사람의 눈으로만 해석이 가능하면 안된다.</li>
<li>파일이 수정되었음을 감지할 방법이 있어야 한다.</li>
<li>세이브 파일의 공유를 막을 수 있어야 한다.</li>
<li>클라우드 기능을 제공할 수 있어야 한다.</li>
</ol>
</blockquote>
<hr>
<h3 id="1-사람의-눈으로만-해석이-가능하면-안된다">1. 사람의 눈으로만 해석이 가능하면 안된다.</h3>
<p>이것은 간단합니다. XOR 암호화나 Shift 암호화만 거쳐도 맨눈으로는 평문을 예측하기 힘들어집니다. 물론 이런 형태의 암호화는 손쉽게 복호화가 이루어집니다.</p>
<blockquote>
<p>그럼 암호화에 의미가 있는가?</p>
</blockquote>
<p>사실 몇 초도 안되서 복호화가 진행되어버리기에 암호화의 의미는 없습니다. 단지 파일을 메모장으로 여는 것만으로 어떤 내용이 담겨있는지 알아차리는 것은 원하지 않았을 뿐입니다. 파일을 뜯어보겠다는 의지를 가진 사람에게 조그만 귀찮음을 줄 순 있겠네요.</p>
<p>그리고 <strong>게임에 save, load 과정에서 많은 비용을 잡아먹는 것을 원하지 않았습니다.</strong> 아이템을 먹거나 맵을 이동하는 순간 매번 렉이 걸린다면 플레이어는 답답함을 느껴버릴 겁니다. 그렇기에 간단한 암호화만으로 처리하였습니다.</p>
<p>나는 아주 조금 더 복잡한 암호화를 원한다! 하시면 Affine 암호화나 Vigenere 암호화를 사용하시길 바랍니다. 물론 이것도 아주 조금의 노력이 들어간다면 금방 뚫립니다.</p>
<p>아니야, 나는 엄청 복잡해서 복호화가 절대 안되는 암호화를 원해!
포기하십시오. 게임의 코드를 뜯으면 어떤 암호화 알고리즘이 사용되었는지, 또는 어떤 암호키가 사용되었는지 바로 들통납니다. 따로 암호키를 관리하는 서버를 만들지 않는 한.. 완벽한 암호화 시스템을 구축하긴 어렵습니다.</p>
<hr>
<h3 id="2-파일이-수정되었음을-감지할-방법이-있어야-한다">2. 파일이 수정되었음을 감지할 방법이 있어야 한다.</h3>
<p>어떤 플레이어가 파일을 복호화하는 방법을 알아내고 수정까지 했다고 가정합시다. 이때 게임에서 파일이 수정되었음을 감지할 수 있다면 발생할 수 있는 문제를 미리 예방할 수 있습니다.</p>
<p>이는 <code>해싱(hashing)</code> 알고리즘을 이용하면 쉽게 구현할 수 있습니다.</p>
<blockquote>
<p>해시 함수(Hash function)
: 임의의 데이터를 고정된 길이의 데이터로 매핑하는 단방향 함수</p>
</blockquote>
<p>해시 함수의 정의는 이 정도로 간단히 이해하고 넘어가시면 되겠네요.</p>
<p>해시 함수의 입력 값이 달라지면 출력되는 hash 값도 달라집니다. 입력되는 값이 아주 조금만 변하더라도 전혀 다른 hash 값이 나오게 되는 것이죠. <strong>세이브 파일의 내용이 달라지면 그 세이브 파일의 hash 값도 달라지는 겁니다.</strong></p>
<p>이 hash 값을 세이브 파일의 맨 뒤에 추가함으로써 게임은 파일이 수정되었음을 감지할 수 있습니다. 파일 내용으로 다시 계산된 hash 값과 세이브 파일에 적힌 hash 값이 서로 동일한지 비교하면 끝입니다. 만일 서로 동일하지 않다면 수정되었다는 것을 알아차릴 수 있죠.</p>
<pre><code>//세이브 파일 데이터를 문자열로 가져오기
var _save_string = json_encode(save_map);

//hash 생성(sha1 해싱 방법을 이용)
//이 hash 값은 길이가 40인 문자열 형태로 반환됨
var _hash = sha1_string_utf8(_save_string);

//문자열 뒤에 해시 값 추가
_save_string += &quot;#&quot; + _hash + &quot;#&quot;;

//이 문자열을 세이브 파일에 저장
var _file = file_text_open_write(filename);
file_text_write_string(_file, _save_string);
file_text_close(_file);</code></pre><pre><code>//세이브 파일 불러오기
var _file = file_text_open_read(filename);
var _save_string = file_text_read_string(_file);
file_text_close(_file);

//기록된 hash 값만 잘라내서 저장
var _expected_hash = string_copy(_save_string, string_length(_save_string)-40, 40);

//hash 값을 제외한 내용만 가져오기
var _hashless_string = string_copy(_save_string, 1, string_length(_save_string)-42);

//세이브 파일 내용을 바탕으로 새로 hash 값을 계산
var _new_hash = sha1_string_utf8(_hashless_string);

//hash 값이 서로 동일한지 확인
if(_expected_hash == _new_hash){
    //세이브 파일은 무사하네요! 어서 세이브 파일을 읽어오도록 합시다.
    load_map = json_decode(_hashless_string);
}
else{
    show_error(&quot;세이브 파일 무결성 검사 실패&quot;, false);
}</code></pre><p>이렇게 게임메이커에서 제공해주는 해시 함수를 이용하여 쉽게 구현할 수 있습니다. 하지만 아직 완벽한 것은 아닙니다. 해커가 해싱 알고리즘을 알아낸다면 몇 분안에 hash 값을 다시 계산하여 세이브 파일을 수정할 수 있죠. 그렇게 되면 게임에서 파일이 수정되었는지 확인할 길은 없습니다.</p>
<blockquote>
<p>HMAC(Hash-based Message Authentication Code)
: 해시 기반 메시지 인증 코드. 해시 함수와 비밀 키를 사용하는 암호화 인증 기술이다.</p>
</blockquote>
<p>바로 HMAC를 이용해서 hash를 암호화할 것입니다. 누군가 파일을 수정해서 hash 값을 계산하면 게임에서 예상하는 hash 값과 다르게 나오게끔 할 수 있습니다.</p>
<p>우리가 사용할 HMAC 알고리즘의 정의는 아래와 같습니다. 물론 이해할 필요는 없습니다.</p>
<pre><code>HMAC-SHA1(Key, Message) = SHA1((Key&#39; ^ OuterPadding) |+| SHA1((Key&#39; ^ InnerPadding) |+| Message))</code></pre><br/>

<p><code>sha1_string_utf8(string)</code> 함수를 <code>sha1_string_utf8_hmac(key, string)</code> 형태로 바꿔서 작성해야 합니다. 키를 바탕으로 hash 값이 계산되게끔 말이죠. 이 스크립트는 아래 프로젝트 파일에서 가져와서 사용하실 수 있습니다. (<a href="https://twitter.com/jujuadams">Juju Adams</a>님께서 올리신 프로젝트 파일입니다.)
<a href="https://www.dropbox.com/s/4yujkgcjdg5pss7/protect%20your%20savefiles.yyz?dl=0">https://www.dropbox.com/s/4yujkgcjdg5pss7/protect%20your%20savefiles.yyz?dl=0</a></p>
<hr>
<h3 id="34-세이브-파일의-공유를-막을-수-있어야-한다-그리고-클라우드-기능을-제공할-수-있어야-한다">3~4. 세이브 파일의 공유를 막을 수 있어야 한다. 그리고 클라우드 기능을 제공할 수 있어야 한다.</h3>
<p>지금까지 파일의 수정은 막을 수 있었지만 공유는 막을 수 없었습니다. 코드에 있는 동일한 키를 바탕으로 계산되기 때문이죠. 그럼 HMAC 알고리즘에 사용되는 secret key를 수정해서 공유를 막으면 되지 않을까? 하고 생각했습니다.</p>
<p>&#39;그럼 컴퓨터에 있는 시스템 정보를 바탕으로 키를 생성하면 되겠구나! 그러면 모든 장치에서 각자 고유의 키를 생성할 수 있겠어!&#39; 하고 생각했습니다. 파일이 생성된 장치에서만 올바른 hash 값을 만들어낼 수 있기에 다른 사용자들간의 파일 공유는 막을 수 있습니다. 하지만 이렇게 되면 클라우드 기능을 제공할 수 없게 됩니다. &#39;왜 같은 계정으로 게임을 즐기는데 기기가 다르다고 세이브를 그대로 이용할 수 없는거야?&#39; 라며 불평을 할 수 있습니다. GMS에서 제공해주는 <code>ds_map_secure_save(id, filename)</code>, <code>ds_map_secure_load(filename)</code> 함수를 이용하지 못하는 이유이기도 합니다.</p>
<p>그래서 키 값을 유저의 id를 이용해서 만드는 것으로 결정하였습니다. 유저의 id는 중복되지 않는 고유의 번호이기 때문이죠. 아직 개발 초기단계라 테스트는 못해보았지만 스팀에서 제공해주는 API같은 것을 이용한다면 충분히 가능할거라고 생각됩니다.</p>
<br/>

<hr>
<p>제 프로젝트에 맞게끔 찾은 정보와 제 생각은 담은 글이므로 모든 게임에 해당되는 내용들은 아니니 참고용으로만 봐주세요!</p>
<hr>
<p>참고자료
<a href="https://gamemaker.io/en/blog/protect-your-savefiles">https://gamemaker.io/en/blog/protect-your-savefiles</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[surface에 관한 고찰]]></title>
            <link>https://velog.io/@f_works/surface%EC%97%90-%EA%B4%80%ED%95%9C-%EA%B3%A0%EC%B0%B0</link>
            <guid>https://velog.io/@f_works/surface%EC%97%90-%EA%B4%80%ED%95%9C-%EA%B3%A0%EC%B0%B0</guid>
            <pubDate>Fri, 11 Aug 2023 14:15:25 GMT</pubDate>
            <description><![CDATA[<h2 id="1-surface_exists-사용했음에도-불구하고-에러가-발생한다">1. surface_exists() 사용했음에도 불구하고 에러가 발생한다..?</h2>
<p>최근 surface를 사용하면서 한 가지 이상한 문제가 발생했습니다.</p>
<pre><code>﻿Trying to set a surface target that does not exist.
(line 64) - surface_set_target(surface)</code></pre><p>surface가 존재하지 않는다는 에러문구였습니다. 하지만 작성한 코드는..</p>
<pre><code>if(!surface_exists(surface)){
  surface = surface_create(width, height)
}
surface_set_target(surface)
...</code></pre><p>surface_exists() 함수를 사용했음에도 이러한 문제가 발생한 것입니다.</p>
<p>저 조건문 하나만으로 surface를 안전하게 사용할 수 있다고 생각했으나.. 그렇지 않았던 겁니다.</p>
<p>if문을 체크 한 다음 운영체제에서 cpu를 다른 프로세스에 넘겨버리고 garbage collection에서 수거해간건지 알 수가 없었습니다.</p>
<p>다시 저 에러를 띄워서 원인이 무엇인가 찾으려고 노력했지만 다시 발생되지 않더군요;</p>
<p>정말 낮은 확률로 발생한 문제였던것 같습니다.</p>
<p>​</p>
<p>이러한 에러가 다시 발생되는 것을 확실히 막아내고 싶었기에, 새로운 방법을 찾아내었습니다.</p>
<pre><code>var _check = true
while(_check){
    try{
        surface_set_target(surface)
            // 임시로 원 하나 그려보기
            draw_clear(c_black)
            draw_set_color(c_yellow)
            draw_circle(mouse_x, mouse_y, 200, false)
        surface_reset_target()
        draw_surface(surface, 0, 0)
        _check = false
    }
    catch(_exception){
        surface = surface_create(room_width, room_height)
        show_debug_message(_exception.message)
    }
}</code></pre><p>try catch 문을 이용하는 것이었습니다.</p>
<p>에러가 발생한다고 해도 다시 surface를 생성하고 그리는 과정을 시도할 수 있게 됩니다. 성공할 때까지 반복하는 것이죠. 물론 에러가 발생하는 횟수만큼 반복하게 되지만 무한루프에 빠질 정도로 에러가 발생하진 않을겁니다.</p>
<p>(코드를 설명하자면 try문의 끝부분에 _check = false를 넣어서 에러가 발생할 수 있는 코드의 수행여부를 확인합니다. _check가 false가 될 때까지 반복하는 것이죠. 에러가 발생해도 catch로 넘어가기에 _check 변수에는 true 값이 유지됩니다.)</p>
<p>​</p>
<p>위 코드가 잘 수행되는지 테스트해보기 위해서 코드 한 줄을 맨 윗줄에 넣었습니다.</p>
<pre><code>if(choose(0,1) == 0) surface_free(surface)</code></pre><p>(대략) 절반의 확률로 surface를 지워보는 것입니다. 테스트 결과는...
<img src="https://velog.velcdn.com/images/f_works/post/3b88d815-37a2-40a6-9be9-9ee25feeab6f/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/f_works/post/4da860b4-380c-473d-91f3-fb848fda160d/image.png" alt=""></p>
<p>네. 아주 잘 그려집니다. Output에서 에러 메시지를 계속 띄우지만 무사히 surface를 성공적으로 그려냅니다.</p>
<p>​
​</p>
<hr>
<p>​
​</p>
<h2 id="2-surface가-사라지는-것을-방지해보자">2. surface가 사라지는 것을 방지해보자</h2>
<p>다들 아시다시피 surface는 휘발성입니다. 언제든지 surface의 내용이 사라질 수 있는 것이죠.</p>
<p>그래서 대부분은 surface의 내용이 사라져도 게임에는 영향이 가지 않게끔 코드를 작성할 것입니다.</p>
<p>하지만 일부는 surface의 내용을 유지해야하는 상황이 발생하기도 합니다. 저 역시 surface가 갑자기 사라지면 곤란했기에, 다음의 방법을 찾았습니다.</p>
<p>​</p>
<p>바로 buffer_get_surface, buffer_set_surface 함수를 사용하는 것</p>
<p>buffer의 내용은 garbage collection의 대상이 아니기에, surface의 내용을 buffer에 저장하는 것입니다.</p>
<p>​</p>
<p>제가 사용한 코드는 다음과 같습니다.</p>
<pre><code>if(!buffer_exists(buffer)){
    buffer = buffer_create(surface_get_width(surface) * 
                           surface_get_height(surface) * 4,
                           buffer_fixed, 1);
}
buffer_get_surface(buffer, surface, 0)</code></pre><p>surface의 크기만큼 buffer를 생성하고, surface의 내용을 담습니다.</p>
<p>(정적 buffer가 아닌 동적 buffer으로 할당하고 싶다면 buffer_fixed 대신 buffer_grow 를 사용하시면 됩니다. 더 자세한 내용은 레퍼런스를 참고해주세요)</p>
<ul>
<li>reference : <a href="https://manual.yoyogames.com/GameMaker_Language/GML_Reference/Buffers/buffer_create.htm">https://manual.yoyogames.com/GameMaker_Language/GML_Reference/Buffers/buffer_create.htm</a></li>
</ul>
<p>반대로 buffer의 내용을 surface에 반영하고 싶다면 아래처럼 작성하시면 됩니다.</p>
<pre><code>buffer_set_surface(buffer, surface, 0)</code></pre><p>이렇게 surface의 내용을 buffer에 저장, buffer에 있는 내용을 surface에 담아낼 수 있기 때문에 사용중인 surface가 사라져도 안심할 수 있습니다.
​
​
​
​
​
​
혹시 잘못된 내용이 있다면 댓글로 알려주시기 바랍니다 :D</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Hello, world!]]></title>
            <link>https://velog.io/@f_works/Hello-world</link>
            <guid>https://velog.io/@f_works/Hello-world</guid>
            <pubDate>Tue, 08 Aug 2023 08:01:30 GMT</pubDate>
            <description><![CDATA[<h4 id="반가워요">반가워요</h4>
]]></description>
        </item>
    </channel>
</rss>