안드로이드 해킹 실습 (2)

2013년 11월 22일 금요일

이 문서는 어디까지나 스터디의 목적으로 작성되었습니다. 악용하였을때 받는 불이익 및 피해는 책임지지 않습니다.

1부에서 살펴보았던 소스 기반으로 실제 통신 할 수 있는 외부 프로그램을 구성해보자.

실제 사용되는 모듈은 base64, md5, http, json같이 간단한 상황이니 쉽게 테스트 할 수 있는 스크립트 언어로 작성하는게 편할것이다.

해서 난 php스크립트를 이용해서 작성했음. 멀로 해도 상관은 없..

먼저 간단한 암복호화와 http통신을 위한 함수이다.

<?php
$xor_table = array(
        1, 0x62, 0x36, 0x6a, 0x6a, 0x38, 0x75, 15, 0x4c, 0x40, 0x4c, 0x6a, 0x3d, 0x3f, 0x36, 0x57,
        0x23, 0x44, 0x2f, 0x71, 1, 0x6d, 0x4c, 110, 0x61, 0x57, 1, 0x62, 0x65, 0x6d, 6, 0x7c,
        0x6a, 0x61, 0x65, 0x5b, 0x63, 0x2b, 0x4d, 0x24, 0x67, 0x49, 0x24, 11, 0x1a, 0x3e, 0x2a, 0x39,
        0x43, 100, 0x24, 0x49, 0x4a, 0x6f, 0x5f, 60, 0x11, 0x2f, 0x3b, 0x7b, 0x5c, 0x23, 0x53, 0x4f,
        0x1a, 0x24, 0x24, 4, 10, 0x63, 120, 0x73, 0x4c, 0x56, 100, 0x40, 13, 0x6f, 0x7f, 0x7b,
        0x70, 0x39, 0x7b, 15, 0x4f, 0x13, 0x13, 0x27, 0x6a, 2, 0x27, 0x1f, 0x29, 0x56, 0x27, 11,
        0x5b, 0x1c, 0x31, 0x60
);

// 암호화
function encode($param) {
    global $xor_table;
    $encode = $param;
    for ($i=0; $i<strlen($param); $i++) {
        $key = $xor_table[$i%count($xor_table)];
        $encode[$i] = chr($key^ord($param[$i]));
    }
    return md5($param).base64_encode($encode);
}

// 복호화
function decode($encode) {
    global $xor_table;
    $decode = base64_decode($encode);
    for ($i=0; $i<strlen($decode); $i++) {
        $key = $xor_table[$i%count($xor_table)];
        $decode[$i] = chr($key^ord($decode[$i]));
    }
    return $decode;
}

// http통신
function push($host, $path, $post) {
    $header = array();
    $header[] = "POST ".$path." HTTP/1.1";
    $header[] = "Content-Type: application/x-www-form-urlencoded";
    $header[] = "User-Agent: Dalvik/1.6.0 (Linux; U; Android 4.1.2; SHV-E210S Build/JZO54K)";
    $header[] = "Host: monster-server.netmarble.net";
    $header[] = "Connection: Keep-Alive";
    $header[] = "Accept-Encoding: gzip";
    $header[] = "Content-Length: ".strlen($post);

    $out = join("rn", $header)."rnrn";

    if($post) $out .= $post."rnrn";
    $body = "";
    $head = "";
    $fp = fsockopen($host, 80, $errno, $errstr, 30);
    usleep(50);
    if($fp) {
        socket_set_timeout($fp, 30);
        fwrite($fp, $out);
        $is_body = false;
        $is_chunk = false;
        $chunk_size = 0;
        $body_size = 0;
        while(!feof($fp)) {
            $buffer = fgets($fp, 128);
            if ($is_body) {
                if ($is_chunked) {
                    if ($chunk_size) {
                        $body .= $buffer;
                        $current_size += strlen($buffer);
                        if ($current_size >= $chunk_size) {
                            $chunk_size = hexdec($buffer);
                            $current_size = 0;
                            next;
                        }
                    }
                } else {
                    $body .= $buffer;
                }
            } else {
                if($buffer=="rn") {
                    $is_body = 1;
                    $current_size = 0;
                    if ($is_chunked) {
                        $chunk_size = hexdec($buffer);
                    }
                } else {
                    $is_chunked = (strpos($head, "chunked")!==false);
                    $head .= $buffer;
                }
            }
        }
        fclose($fp);
    } else {
        echo "error";
    }

    return array($head, $body);
}

// json문자열에서 값추출
function jsonval($json, $key) {
    $pos1 = strpos($json, ""$key":");
    $val = false;
    if ($pos1!==false) {
        $pos1 += strlen(""$key":");
        $pos2 = strpos($json, ",", $pos1);
        $val = substr($json, $pos1, $pos2-$pos1);
        if ($val[0]=='"') {
            $val = substr($val, 1, strlen($val)-2);
        }
    }
    return $val;
}
?>

encode, decode는 원래 소스의 암복호함수를 그대로 포팅한거고 push는 http post요청과 응답을 받기위한 함수이다. 쓸데없이 긴데 php의 http api를 써도 무방하다. 여타 스크립트도 어지간하면 http client기능은 구현되어있으니 그냥 편할걸로 쓰면되겠다.

jsonval도 단순하게 json문자열에서 값을 가져오는 함수이다. 역시 json api를 써서 가져와도 상관없겠다.

이 함수들을 이용하여 간단하게 로그인 요청을 하고 응답값을 받는 코드를 작성해보자.

<?php
//login
$path = "/SR/login";
$param = "userid=884567053453453&img_url=http://th-p0.talk.kakao.co.kr/th/talkp/23f23f23f/weff223f2/f23f32f323f.jpg&ver=1.25&platform=Android&cookie=527c2196_7f3036e69a2ce3ce4c33b1681d33061c_8d2401853135d705ab5c63cc7668be28";
$post = "epp=".urlencode(encode($param));

list($head, $body) = push("xxxxx-server.xxxxxxxx.net", $path, $post);
echo $head;
$output = decode($body);
echo $output;
?>

당연히 param안에들어간값은다들다를것이다.param 안에 들어간값은 다들 다를것이다. 저 post에 들어가는 값이 1부에서 request값이다. url decoding 해서 decode했었으니 여기선 거꾸로 encode후 url encoding.

unity3d 소스에서 요롷코름 암호화해서 서버로 보내는것이겠다. $output이 정상적으로 출력되면 일단 로그인은 된것이다.

$param의 변수값을 일일히 분석하지 않아도 변수명만 봐도 대충 짐작이 가능할 것이다.

userid는 아이디겠고 (카톡게임이니 카톡아이디겠다..) img_url은 직접 브라우저에 띄워보면 알겠지만 카톡이미지다. ver는 빌드버전. platform은 Android이다.

cookie라는게 조금 복잡한데.. 이것은 추후에 자세히 설펴보도록 하자.

---------------------------참고용 node.js 소스추가

var util = require('util');
var querystring = require('querystring');
var crypto = require('crypto');
var http = require('http');

var xor_table = [
    1, 0x62, 0x36, 0x6a, 0x6a, 0x38, 0x75, 15, 0x4c, 0x40, 0x4c, 0x6a, 0x3d, 0x3f, 0x36, 0x57,
    0x23, 0x44, 0x2f, 0x71, 1, 0x6d, 0x4c, 110, 0x61, 0x57, 1, 0x62, 0x65, 0x6d, 6, 0x7c,
    0x6a, 0x61, 0x65, 0x5b, 0x63, 0x2b, 0x4d, 0x24, 0x67, 0x49, 0x24, 11, 0x1a, 0x3e, 0x2a, 0x39,
    0x43, 100, 0x24, 0x49, 0x4a, 0x6f, 0x5f, 60, 0x11, 0x2f, 0x3b, 0x7b, 0x5c, 0x23, 0x53, 0x4f,
    0x1a, 0x24, 0x24, 4, 10, 0x63, 120, 0x73, 0x4c, 0x56, 100, 0x40, 13, 0x6f, 0x7f, 0x7b,
    0x70, 0x39, 0x7b, 15, 0x4f, 0x13, 0x13, 0x27, 0x6a, 2, 0x27, 0x1f, 0x29, 0x56, 0x27, 11,
    0x5b, 0x1c, 0x31, 0x60
];

function base64_encode(param) {
    var b = new Buffer(param);
    return b.toString('base64');
}

function base64_decode(param) {
    var b = new Buffer(param, 'base64');
    return b.toString();
}

function md5(param) {
    var md5hash = crypto.createHash('md5');
    md5hash.update(param);
    return md5hash.digest('hex');
}

function encode(param) {
    var buf = new Buffer(param.length);
    for (var i=0; i<param.length; i++) {
        key = xor_table[i % xor_table.length];
        buf[i] = param.charCodeAt(i);
        buf[i] = key ^ buf[i];
    }
    return md5(param)+base64_encode(buf.toString());
}

function decode(param) {
    var decode = base64_decode(param);
    var buf = new Buffer(decode.length);
    for (var i=0; i<decode.length; i++) {
        key = xor_table[i % xor_table.length];
        buf[i] = decode.charCodeAt(i);
        buf[i] = key ^ buf[i];
    }
    return buf.toString();
}

function server_push(host, path, post, callback) {
    var options = {
        hostname: host,
        port: 80,
        path: path,
        method: 'POST',
        headers: {
            'Content-Type': 'application/x-www-form-urlencoded',
            'Content-Length': post.length,
            'User-Agent': 'Dalvik/1.6.0 (Linux; U; Android 4.1.2; SHV-E210S Build/JZO54K)',
            'Host': 'monster-server.netmarble.net',
            'Connection': 'Keep-Alive',
            'Accept-Encoding': 'gzip'
        }
    };

    var data = '';
    var req = http.request(options, function(res) {
        res.setEncoding('utf8');
        res.on('data', function (chunk) {
            data += chunk;
        });
        res.on('end', function() {
            callback(false, data);
        });
    });

    req.on('error', function(e) {
        console.log('problem with request: ' + e.message);
        callback(true, e.message);
    });

    req.write(post);
    req.end();
}

var srlib = {
    debug: false,
    server: function(action, param, callback) {
        var path = '/SR/' + action;
        var query = querystring.stringify(param);
        var post = "epp="+encodeURIComponent(encode(query));
        if (srlib.debug) console.log(query);
        server_push("xxxxx-server.xxxxxxxx.net", path, post, function(error, result) {
            if (error) {
                callback(false, {error:result});
                return;
            }

            var retval = '';
            var retobj = {};
            try {
                retval = decode(result);
                retobj = JSON.parse(retval);
            } catch (e) {
                console.log('server-push-exception');
                console.log(e.toString());
                console.log(result);
                callback(false, {err:-2, reason:'script-exception'});
                return;
            }
            if (!retobj) {
                callback(false, {err:-2, reason:'connect-error'});
                return;
            }
            if (retobj.err) {
                callback(false, retobj);
                return;
            }

            callback(true, retobj);
        });
    },
    login: function(param, callback) {
        srlib.server('login', param, callback);
    },
    startgame: function(param, callback) {
        srlib.server('startgame', param, callback);
    },
    gameover: function(param, callback) {
        srlib.server('gameover', param, callback);
    },
    getdropitem: function(param, callback) {
        srlib.server('getdropitem', param, callback);
    },
    getdungeoncleartime: function(param, callback) {
        srlib.server('getdungeoncleartime', param, callback);
    },
};

module.exports = srlib;