2017년 10월 18일 수요일

Wifi WPA2 "KRACK" 공격 상세 분석


핵심 요약


일단 시간이 없는 사람들을 위해 KRACK 공격의 핵심을 요약하면 다음과 같다.

ㆍ키가 재사용 되면 패킷이 해독될 위험이 있다.
ㆍKRACK은 키를 재사용 하도록 할 수 있다.

내용이 쉽지는 않기 때문에 왜 재사용 되면 안되는지

어떻게 재사용 하도록 하는지는 아래에서 자세히 설명한다.



선행 학습 - 요약


일단 공격을 이해하기 전에 선행 학습 해야 할 내용이 있는데,

참고 자료들도 적은 양은 아니므로 중요 내용만 요약하면 다음과 같다.

ㆍWPA 는 EAPOL 포맷의 4-Way Handshake를 사용하며,
한번 사용된 PTK는 다시 사용되지 않아야 한다.

ㆍWPA에서 동일한 키로 자꾸 암호화 하면 해독 될 가능성이 높다.
특히 Context 內 영어 등 자연어가 있으면 거의 된다고 봐야 함.

ㆍJammer와 RogueAP, 다양한 채널을 이용하면 Wifi도 MitM 할 수 있다.

ㆍPMK 는 Pre-shared password (와이파이 패스워드)로 부터 나온다.

ㆍPTK 는 PMK, ANonce, SNonce, MAC( AP, Client) 주소로 부터 나온다.
실제로 사용되는 것은 PTK 로 부터 나온 KCK, KEK, TEK, TK( CCMP ) 등 이지만,
아래부터는 PTK로 작성한다.

  • KCK is used to construct MAC in EAPOL packets 2,3 and 4.
  • KEK is used to encrypt some data sent to client(for example GTK).
  • TEK is used for encrypting traffic between client and AP, later during session.

KRACK 내용 분석


일단 두가지를 먼저 짚고 넘어가자.

1. wpa_supplicant 2.4, 2.5 버전에는 특별한 버그가 있다.

재전송된 Msg3 을 받았을 때, TK 를 0으로 채워서 인스톨 한다는 점이다.

이것은 당시에는 심각하게 받아들여지지 않았는데,

Side Effect 를 일으킬 수 있으므로 분리 패치가 필요한 부분이다.


2. iOS 와 Window 도 안전하지 않다.

Msg3 재전송을 허용하지 않기 때문에 ( 근데 이거 802.11 표준 위반이다 )

Key reinstallation Attack 에는 취약하지 않지만, Group Key Handshake 공격에는 취약하다.


자 이제 논문의 그림을 보면서 하나씩 이해하려고 노력해 보자.



PHASE 1. 공격자는 메시지를 보다가 Msg4 가로채고 보내지 않는다
            (이걸 대체 어떻게 한건지 디테일은 최하단 내용 참조)

PHASE 2. 클라이언트는 Msg4 보낸 직후 PTK & GTK 설치하고
            설치한 PTK 데이터를 암호화해서 보낸다.

PHASE 3. AP Msg4 오지 않으므로 클라이언트가 Msg3 받은걸로 생각하고
             다시 MSG4 를 설치했던 PTK 암호화해서 보낸다. 공격자는 이걸 그대로 전달한다.
             클라이언트는 Msg3 받고 nonce replay 카운터를 초기화 한다.

PHASE 4. AP 사이드에 연결을 성립시켜서 PTK 설치하게 한다.
             생각해보면 AP Msg4 받지 않아 아직 PTK 설치 되어 있지 않기 때문에
             클라이언트가 보낸 암호화된 Msg4 reject 것이.
             그러나 802.11 표준을 유심히 살펴보면 마지막 counter 뿐만 아니라 
             4-way handshake 사용된 모든 replay counter 받아들이도록 되어 있어서 문제 없다.
             즉, 어떤 AP는 r+1을 받아들이고 어떤 AP는 이전에 암호화된 Msg4 를 받아들임
       

PHASE 5. 클라이언트가 데이터를 다시 동일한 Nonce PTK 암호화해서 보낸다.


결국 요약하면 


WPA는 일반적으로 키를 자주 Refresh 하므로

복호화 하기 매우 어렵다.

하지만 쓰던 키를 재사용 하도록 공격 해서 데이터를 모은 다음,

데이터가 쌓이면 결국 복호화 할 수 있다 라는 말이 된다.


위험도

4-Way Handshake 가 나온 이래 14년간 누구도 생각지 못한 엄청난 공격이다.

그래서 와이파이 세상이 망한 것 처럼 화제가 되고 있지만

글쎄, 전통적인 WPA Crack 에 쉽게 깨지는 패스워드를 사용하고 있는 AP가

이 세상에 얼마나 많은데

굳이 AP 와 Jammer 를 사고 펌웨어를 수정해서 이 공격을 할까?

게다가 HTTPS 를 깨는 것이 아니라서,

주의심이 깊은 사용자라면 당하지 않을 수도 있다고 생각한다.



복호화

사실상 암호화는 PTK 로 직접 하는 것이 아니라, PTK 로 Derive 된 임시키를 
OTP (One Time Pad)로 사용하게 된다.
참고로 이 임시키는 매 패킷마다 시퀀스 번호를 참조하여 derive 되는데...

ㆍ가장 궁금해지는 부분이다. 키를 알지 못하는 상태에서,
단지 키를 재사용한다는 이유만으로 어떻게 복호화가 가능한가?

아래 그림을 참고해 보면 이해가 쉽다.



KRACK 의 주장은, 거의 모든 패킷에 항상 영어 단어 등이 있을 것이므로
Message 1, 2 를 갖고 있을 때 분리가 가능하다는 것이다.

Crip-drag 공격 참조
http://travisdazell.blogspot.kr/2012/11/many-time-pad-attack-crib-drag.html

Two Time Pad를 통해 자연어를 자동으로 해독하는 방안에 대해서는
아래 참고 논문을 참조하면 되겠다.
https://crypto.stackexchange.com/questions/2249/how-does-one-attack-a-two-time-pad-i-e-one-time-pad-with-key-reuse/2250#2250

디테일


ㆍ논문에서는 다양한 공격들과 디테일을 더 다룬다. 논문을 직접 보는게 더 좋다.

ㆍTKIP 나 GCMP 를 쓰는 경우 복호화 뿐만 아니라 패킷을 Inject 할 수도 있고,

GCMP의 경우 Key 를 복구해 낼 수도 있다. (https://www.krackattacks.com/)


ㆍ이 공격을 성공 시키려면 PMK 를 모르는 상태에서 Wifi 를 MitM 해야하고

그러기 위해서는 Channel Based MitM 을 먼저 시도해야 한다.
(https://people.cs.kuleuven.be/~mathy.vanhoef/papers/acsac2014.pdf)

이때 Rogue AP 2개와 Jammer 가 필요하며 Latency 가 두배 가까이 늘어나게 된다.

위 논문에 의하면 MitM시 Latency는 3.82±10.4 ms. -> 7.45±12.9 ms

100MB 파일 다운로드 속도는  18.6 Mbps -> 8 Mbps 로 변했다고 한다.

즉, 평소보다 다운로드 속도가 많이 느리다면 

공격의 징후로 볼 수도 있다.



참고 자료


위 논문은 좀 더 자세하게 다루고 싶은 내용이라 요약하자면, 

4 way handshake 때문에 MitM 위치 잡는게 사실상 어렵다
세션키가 MAC주소에 기반하기 때문에 AP 동일한 MAC 주소를 가져야 하는데
AP 클라이언트가 통신 해야 하기 때문에 불가능했다.

ㆍ그래서 두개의 AP 만들어 하나는 실제 AP 같은 채널에
다른 하나는 AP와 다른 채널에 둔다.
다른 채널에 있더라도 물리적으로 가깝게 있기 때문에 서로 프레임을 수신 있어서 
무한 루프에 빠질 있는데 이를 막기 위해 최근 전달된 프레임을 
시퀀스 번호로 추적해 프레임만 전달하도록 했다.

ㆍ이 Rouge AP 102.4 ms 마다 뿌림
다만, 뿌린다고 해서 클라이언트가 바로 채널을 변경하는 것이 아니다.

ㆍ비컨을 선택적 Jamming 했더니 반드시 프레임은 손실되었다
특히 authentication message 34바이트밖에 안되서 
거의 선택적 Jamming 없었고 클라이언트를 Rogue AP 붙일 없다.
그래서 실제 AP 지속적으로 Jamming 해서 클라이언트가 Rogue AP 붙게 했다.

ㆍ다른 채널에 AP를 복제하는 것은 간단하지만
실제 AP 채널에 있는 장치는 모든 클라이언트 MAC 주소를 Listen 하고 있다가 보내야 하므로,
Attacker 드라이버 펌웨어를 수정해서 Sequence Number 덮어쓰는걸 방지했다.

이렇게 하면 와이파이 패스워드를 몰라도 MitM 위치에서 선택적으로 패킷을 버리거나 다시 보내거나 있다.

물론 그렇다고 해도 2014년 당시 이 논문으로는 패킷을 decrypt  수 없었다
(하지만 3년후 아저씨는 결국 대박을 치게 된다)


2017년 10월 12일 목요일

Node.js 에서 Redis Session 사용하기


Node.js 에서 Redis Session을 사용하는 예제


Node.js 에서 Redis Session을 사용하는 단순한 예제. Hello SyntaxHighlighter

Use Redis Session in node.js


var express   = require('express');
var router          = express.Router();
var path   = require('path');
var favicon   = require('serve-favicon');
var cookieParser  = require('cookie-parser');
var bodyParser  = require('body-parser');
var http    = require('http');
var expressSession  = require('express-session');
var fs     = require('fs');
var RedisStore      = require('connect-redis')(expressSession);
var redis           = require('redis');
var app = express();
//http////////////////////////////////
var httpServer=http.createServer(app);
httpServer.listen(80, function(){
 console.log("http server listening on port " + 80);
});
////////////////////////////////
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cookieParser());

var redisConfig = {
    "host": "localhost",
    "port": 6379,
    "prefix": "session:",
    "db": 0,
    "client": redis.createClient(6379,"localhost")
}
const session = expressSession({
    secret : new Date().getMilliseconds()+"brokim",
    resave : false,
    store  : new RedisStore(redisConfig),
    saveUninitialized:true,
    cookie : {
        maxAge : 1000 * 60 * 3
    }
});
app.use(session);
app.use(favicon(path.join(__dirname, 'public', 'favicon.ico')));
app.use(express.static(path.join(__dirname, 'public')));
app.get('/',function(req,res){
  fs.readFile('./public/login/index.html', function(error, data) {
        if(error != undefined) {
            console.log(error);
            res.writeHead(404);
            res.end();
        } else {
            res.writeHead(200, {'Content-Type': 'text/html'});
            res.end(data);
        }
    });
});

//////////////////////////////////////////////////////////////////////////////////
/////////////////RESTFUL APIS/////////////////////////////////////////////////////
//LOGIN///////////////////////////////////////////////////////////////////////////

app.route('/login')
    .post(function login_post(req,res){
        try{
        //query는 따로 구현하면 됨.
        //var query = require('./query.js');
        query.login([
            req.body.id
            ,salted(req.body.pw)],
            function loginAPI(ret){
            if(ret.fail==0){
                req.session.privilege=ret.result.privilege;
                req.session.uid=ret.result.id;
                req.session.name=ret.result.name;
            }
            res.send(ret);
        });
        } catch(error){
        }
    });

app.get('/logout', function logout_get(req,res){
    req.session.destroy(function logout_destroy(err){
        if(err!==null){
            RESULT.GENERAL_FAIL.result="";
            res.send(RESULT.GENERAL_FAIL);
        } else {
            RESULT.GENERAL_SUCCESS.result="";
            res.send(RESULT.GENERAL_SUCCESS);
        }
    });
});

module.exports = app;