사악한 KT는 어떻게 공유기를 검출하는가




글쓴이 | 레니


서론



KT 공유기 이용제재 조치 개념도

KT 공유기 이용제재 조치 개념도
2005년 KT는 자사의 인터넷 라인에 공유기를 꼽아 여러 대의 IP를 따서 사용하는 유저를 식별할 수 있는 시스템, 즉 공유기 검출 시스템 개발에 성공했다고 발표했습니다.

하지만 이것이 기술적으로 완벽하지 않다는 반론과 함께, 기존 사용자들의 거센 반발로 인해 사용 가능 PC를 2대로 제한한다는 정책으로 선회한 뒤 이 문제는 그대로 묻히나 했는데… 작년부터 KT와 하나로 사용자들을 중심으로 인터넷 연결 중 이상한 현상에 대한 보고가 들어오기 시작합니다. 참고1,참고2


이를 정리하면, KT등의 ISP(인터넷 서비스 공급자, Internet Service Provider) 라인을 사용하는 유저들이 웹브라우저로 특정 사이트에 접속할 때, KT에서는 임의로 공유기 검출을 위해 특정 서버로 먼저 요청을 보낸 다음, 사용자가 요청한 페이지로 다시 이동시킨다는 것입니다.참고3


명목상 이런 과정은 "접속 환경 개선을 위한 속도 테스트"나 "불량 사이트 차단"을 위한 것이라는 설명을 많이 하는데, 실제로 앞에서 얘기한 KT 서버에서 하는 일은 사용자가 공유기를 사용하는지를 체크하는 것입니다. 그럼 이런 과정이 어떻게 일어나는지 한 번 추적해 보겠습니다.


로직 추적



보통의 경우라면 바로 www.jinbo.net으로 요청이 전달되어 진보넷 페이지가 브라우저에 뜨게 됩니다. 하지만 공유기 검출 로직이 실행되는 중이라면 요청한 주소는 다음과 같은 HTML로 치환되어 보내집니다.

<HTML>
<HEAD></HEAD>
<FRAMESET border=0 frameSpacing=0 rows=0,* frameBorder=0>
<FRAME name=dolla src="http://221.147.67.202/headseed.asp">
<FRAME src="http://221.147.67.202/xs_btn/uxcsdo.asp?n=2&u=8825&i=0&y=0&m=0&k=0&t=N&b=78&x=www.jinbo.net&s=600&d=2008-01-01">
</FRAMESET>
</HTML>

화면이 두 개의 프레임으로 나뉘고, 둘 다 특정한 KT 서버에 요청이 갑니다. 이 중 첫 번째 요청(http://221.147.67.202/headseed.asp)은 서버에서 정확히 어떤 일을 하는지 알 수 없고 단지 빈 HTML이 돌아올 뿐입니다. 여기서 주목해야 할 것은 두 번째 요청입니다.


<html>
<frameset rows='0,*' border='0'>
<frame src='/xs_btn/uxcn.asp?n=2&u=8825&i=0&y=0&m=0&k=0&t=N&b=78&s=600&x=www.jinbo.net&d=2008-01-01'>
<frame src='http://www.jinbo.net/?' name='rosea'>
</frameset>
<body oncontextmenu="return false" onselectstart="return false" ondragstart="return false">
</body>
</html>

응답의 결과로 역시 두 개의 프레임을 나눠서 줍니다. 첫 번째 프레임은 hidden 처리되어 특정 코드를 수행하고, 두 번째 프레임은 요청한 주소의 페이지를 보여줍니다. 결과적으로 화면에는 진보넷 페이지가 보이게 되니까 사용자는 크게 문제점을 인식하지 못할 가능성이 높습니다. 하지만 실제로는 사용자가 눈치채지 못하게 첫 번째 프레임의 동작이 실행되는 셈입니다. 그럼 첫 번째 프레임의 요청을 따라가 봅시다.


<input type='hidden' id='PTe' value='http://www.jinbo.net' target='_top'>

<script language='javascript'>
var tcheck = document.cookie;
window.onload = gonogo;

function gonogo() {
if (tcheck == ) {
if (navigator.appName == 'Microsoft Internet Explorer') {
document.all.PTAe.click();
}
if (navigator.appName == 'Netscape') {
self.location.href=document.getElementById('PTe').value;
}
} else {
today = new Date();
var YY, MM, DD;
YY = today.getYear();
MM = today.getMonth()+1;
DD = today.getDate();
self.location.href='uxcn01.asp?mode=SSL3H&n=2&ha='+YY+'&ba='+MM+'&ya='+DD+'&x=www.jinbo.net&i=0&y=0&m=0&k=0&t=N&b=78&u=8825&d=2008-01-01';
}
}
</script>
<html>
<head>
<title></title>
<meta http-equiv="Content-Type" content="text/html; charset=euc-kr">
</head>
<body oncontextmenu="return false" onselectstart="return false" ondragstart="return false">
</body>
</html>


특정한 스크립트가 응답으로 날아옵니다. 페이지가 로드되면서 스크립트를 실행시켜 쿠키가 있는 경우 날짜와 기타 정보들을 파라미터로 심고 특정 사이트로 다시 보내버립니다. 다시 페이지로 따라가 보겠습니다.


<SCRIPT LANGUAGE="JavaScript">
</SCRIPT>
<input type='hidden' id='PT' value='http://www.jinbo.net' target='_top'>

<script language='javascript'>
if (navigator.appName == 'Microsoft Internet Explorer') {
window.onload = function() {
document.all.PTA.click();
}
}
if (navigator.appName == 'Netscape') {
window.onload = function() {
self.location.href=document.getElementById('PT').value;
}
}
</script>


역시 다른 페이지로 보내버립니다. 또 따라가 봅시다.


<HTML><HEAD><TITLE>Untitled Document</TITLE>
<META http-equiv=Content-Type content="text/html; charset=euc-kr">
<SCRIPT language=javascript>
<!–
//오늘은 더이상 팝업 윈도우를 띄우지 않습니다
function GetCookie( name )
{
var nameOfCookie = name + "=";
var x = 0;
while ( x <= document.cookie.length )
{
var y = (x+nameOfCookie.length);
if ( document.cookie.substring( x, y ) == nameOfCookie ) {
if ( (endOfCookie=document.cookie.indexOf( ";", y )) == -1 )
endOfCookie = document.cookie.length;
return unescape( document.cookie.substring( y, endOfCookie ) );
}
x = document.cookie.indexOf( " ", x ) + 1;
if ( x == 0 )
break;
}
return "";
}

//**********************************************************************************************************************

popup();

function popup()
{
var g1 = "78";
var n_type = "2";
var u_idx = "8825";
var cnt_g = "0";
var cnt_i = "0";
var msgUrl = "";
var width = "";
var height = "";
var msgUrl2 = "";
var width2 = "";
var height2 = "";
var total = "0";
var xt = "www.google.com";
var dsa = "2008-01-01";
var mainsvIP = ""; //임시 (공유기 서비스가입)

var tmpwin = "";
var tmpwin2 = "";
var param = "";
param = "g1="+g1+"&u_idx="+u_idx+"&cnt_g="+cnt_g+"&cnt_i="+cnt_i+"&total="+total+"&xt="+xt+"&mainsvIP="+mainsvIP+"&dsa="+dsa;

if (n_type != 2 && msgUrl != "") // 유도
{
url = "evsol.asp?n_type="+n_type+"&"+param+"&msgUrl="+msgUrl;
tmpwin=window.open(url,"pop1","width="+width+",height="+height+",top=0,left=0");
}

if (msgUrl2 != "") // 일반
{
//오늘은 더이상 팝업 윈도우를 띄우지 않습니다 if 문
var erer = GetCookie("Notice");
if ( erer != "done" )
{
url2 = "evsol.asp?n_type=1&"+param+"&msgUrl="+msgUrl2;
tmpwin2=window.open(url2,"pop2","width="+width2+",height="+height2+",top=0,left=370");
//tmpwin2.focus();
}
}

if (n_type == 2)
{
var strHTML = "<APPLET CODE='fnqlxmfhvl.class' WIDTH='1' HEIGHT='1' name='Internet_Loading'>";
strHTML += "<PARAM NAME='URL' VALUE='dotori_app.asp?b=78&n=2&u=8825&i=0&y=0&t=0&x=www.daum.net&re_ip='>";
strHTML += "<PARAM NAME='TARGET' VALUE='_self'>";
strHTML += "</APPLET>";
document.write (strHTML);
}
} // popup() end

//–>
</SCRIPT>
</HEAD>
<BODY oncontextmenu="return false" onselectstart="return false" ondragstart="return false"></BODY></HTML>


드디어 정말 하고 싶었던 일을 하는 페이지가 나왔습니다. 이 복잡한 과정의 목적은 결국 공유기 사용자에게 경고 팝업을 띄우고, fnqlxmfhvl.class란 java 애플릿을 실행시키는 것입니다. 일단 fnqlxmfhvl.class가 무엇을 하는지 다운받아 리버스 컴파일해 보았습니다.



  • fnqlxmfhvl.java
import java.applet.Applet;
import java.applet.AppletContext;
import java.net.*;

public class fnqlxmfhvl extends Applet
{
public fnqlxmfhvl()
{
flema = "";
mkbro = "unknown";
fx0073 = "";
}

public void init()
{
mkbro = rusianxx();
if(getParameter("URL") != null)
{
fx0073 = getParameter("URL");
fx0073 = fx0073 + mkbro;
if(getParameter("TARGET") != null)
flema = getParameter("TARGET");
}
}

public void start()
{
try
{
URL url = new URL(getDocumentBase(), fx0073);
getAppletContext().showDocument(url, flema);
}
catch(Exception exception) { }
}

private String rusianxx()
{
String s = "";
String s1 = getDocumentBase().getHost();
int i = 80;
if(getDocumentBase().getPort() != -1)
i = getDocumentBase().getPort();
try
{
s = (new Socket(s1, i)).getLocalAddress().getHostAddress();
}
catch(Exception exception) { }
return s;
}

String flema;
String mkbro;
String fx0073;
}


이 프로그램이 하는 일은 매우 간단합니다. 사용자의 내부 IP를 알아낸 다음, 이 IP를 dotori_app.asp로 전송해 줍니다. dotori_app.asp로 요청을 보냈을 때 특별한 응답은 날아오지 않았지만, 짐작으로는 아마 dotori_app.asp에서 사용자의 내부 IP를 받아 공유기를 사용한 IP인지, ISP에서 정상적으로 발급한 IP인지를 판단할 것입니다.


로직 설명


일반적으로 KT나 하나로텔레콤 등의 ISP는 사용자에게 유동 IP를 발급합니다. 유동 IP는 모뎀이 ISP에 접속할 때마다 일정 범위 내에 있는 IP를 발급해 주는 IP입니다. 사용자가 ISP에서 제공하는 라인을 컴퓨터에 바로 꽂아 쓰는 경우에는 사용자 PC의 IP는 외부에서나 내부에서나 ISP가 발급한 유동 IP로 동일하게 됩니다.


하지만 공유기를 사용하게 되면 약간 다른 상황이 발생합니다. 공유기는 하나의 외부 IP를 여러 개의 내부 IP로 분할해 사용 가능하게 해 줍니다. 이 경우 유동 IP는 공유기가 갖게 되고 사용자의 PC는 공유기가 발급한 내부 IP를 사용하게 됩니다. 따라서 사용자 PC의 IP는 10.0.0.2나 192.168.0.2 같은 형태의 내부 IP가 됩니다. 하지만 외부에서 볼 때 사용자 PC의 IP는 공유기가 가지고 있는 IP, 즉 ISP가 발급한 외부 IP로 보이게 되죠. 따라서 이 경우는 외부 IP와 내부 IP가 달라지는 현상이 발생합니다.


어쨌든 ISP는 사용자들이 공유기를 사용하여 여러 대의 PC가 한 개의 라인에 물려있는지 판단할 수 있는 작업을 사용자 몰래 하고 있는 셈입니다. 현실적으로 집에서 2~3개의 PC를 공유기로 사용하는 경우는 크게 문제를 삼고 있지 않지만, 사무실 등에서 10개 이상의 PC가 한 개의 공유기를 통해 사용하는 경우는 적발하여 추가적으로 라인을 구매하도록 유도하고 있다고 합니다. 이런 경우를 알 수 있는 것도 이러한 공유기 검출 시스템이 있기 때문이죠. 아마 사용자의 약관 어딘가에 사용자 동의 없이 하는 이런 작업을 정당화 하는 문구도 넣어놨으리라 생각됩니다.


결론


현실적으로 ISP에서 제공하는 망을 사용하는 이상, 개인 PC에 침입하여 벌어지는 이런 일들을 막기가 매우 어렵습니다. 어떻게 보면 인터넷 세상의 진정한 지배자는 ISP가 아닐까요…

사악한 KT는 어떻게 공유기를 검출하는가

댓글 남기기

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다