티스토리 뷰

728x90
반응형

문제 첫 화면

문제 개요

Javascript Debugging and Deobfuscating Source Code

 

문제 풀이

우선 개발자도구로 디버깅하는 것을 막는 것이던지 개발자도구로 디버깅을 할 수 없었습니다. 그래서 일단 주어진 html 소스코드로 분석을 진행하였는데요.

// Prevent setting breakpoints €from the dev console UI directly€ by defining the function as string
var code = `\x60
  console.log({flag}); 
  for (i=0; i<100; i++) setTimeout('debugger');
  if ("\x24\x7B\x22   .?  K 7 hA  [Cdml<U}9P  @dBpM) -$A%!X5[ '% U(!_ (]c 4zp$RpUi(mv!u4!D%i%6!D'Af$Iu8HuCP>qH.*(Nex.)X&{I'$ ~Y0mDPL1 U08<2G{ ~ _:h\ys! K A( f.'0 p!s    fD] (  H  E < 9Gf.' XH,V1 P * -P\x22\x7D" != ("\x24\x7B\x22" + checksum(code) + "\x22\x7D")) while(1);
  flag = flag.split('');
  i‍ = 1337;
  pool = 'c_3L9zKw_l1HusWN_b_U0c3d5_1'.split('');
  while (pool.length > 0) if(flag.shift() != pool.splice((i = (i || 1) * 16807 % 2147483647)%pool.length, 1)[0]) return false;
  return true;
\x60`;
setTimeout("x = Function('flag', " + code + ")");  

// Check password and decode €encrypted€ data from localstorage
function open_safe() {
  keyhole.disabled = true;
  password = /^CTF{([0-9a-zA-Z_@!?-]+)}$/.exec(keyhole.value);
  document.body.className = (password && x(password[1])) ? 'granted' : 'denied';
  if (document.body.className == 'denied') return;
  password = Array.from(password[1]).map(c => c.charCodeAt());
  encrypted = JSON.parse(localStorage.content || '');
  content.value = encrypted.map((c,i) => c ^ password[i % password.length]).map(String.fromCharCode).join('')
}

// €Encrypt€ and save data
function save() {
  plaintext = Array.from(content.value).map(c => c.charCodeAt());
  localStorage.content = JSON.stringify(plaintext.map((c,i) => c ^ password[i % password.length]));
}

우선 문제의 첫 페이지의 입력값에 들어갈 Flag 값인 password 값을 연산하는 코드는 위와 같습니다. x 라는 이름의 함수에 CTF{...} 포맷의 값인 flag 값을 넘겨주고 함수 내용물로는 code 변수에 정의된 내용을 함수가 호출되었을 때 실행될 내용으로 정의해주고 있습니다. 이 때 code는 backtick 을 사용해서 hex 인코딩 되어 있는 ${...} 의 내용을 수행해주게 되는데요. 거기엔 checksum(code) 라는 함수가 실행되게 되는 것을 볼 수 있습니다. 그 checksum 함수는 아래와 같습니다.

// Utilities
// Proprietary €military grade€ checksum function (to use on objects, stringify argument first, e.g. object + ' ')
checksum  =  s => {                                                                                                       
  let result = '';                                                                                                        
  let x = 0;                                                                                                              
  try {                                                                                                                   
    for (let x = 0; x+3 <= s.length; x+=3) {                                                                              
      let next =                                                                                                          
        (s.charCodeAt(x)%2)*64 +                                                                                          
        (s.charCodeAt(x+1)%8)*8 +                                                                                         
        s.charCodeAt(x+2)%8;                                                                                              
      next = Math.min(Math.max(0x20, next), 0x7E);                                                                        
      result += String.fromCharCode(next);                                                                                
    }                                                                                                                     
    return result;                                                                                                        
  } catch(_) {                                                                                                            
    throw ChecksumError(s, result, x);                                                                                    
  }                                                                                                                       
}

s 파라미터로 들어온 문자열을 위와 같이 연산해주고 그 결과 값으로 문자열을 반환합니다.

그렇게 checksum(code) 함수가 반환하는 문자열과 합쳐진 code 문자열은 최종적으로 x라는 이름의 함수에 정의됩니다.

 

그리고 문제의 첫 페이지에서 input form 에서 flag 값을 입력하게 되면 open_safe 라는 함수가 실행되고, 이 함수에서는 password 가 CTF{어쩌구} 정규식과 일치하는지 확인한 뒤 바로 x(password[1]) 로 방금 정의한 x 함수에 password[1] 즉 flag 값의 CTF{ 와 } 사이에 있는 값을 파라미터로 넘겨주어 함수를 실행하여 password 가 올바른 지 확인하는 과정을 수행합니다.

 

password 가 올바른 값인지 체크하는 과정은 단순하여 쉽게 리버싱할 수 있었습니다. 그러기 위해서는 code 변수에 정의된 함수의 내용을 분석해봐야 하는데요. 육안으로 봤을 때는 i 값이 1337로 정의되어지는 것 같은데 알고보면 코드 상에 적힌 i 는 유니코드로 알파벳 i과는 다른 i 였는데 벌써부터 페이크가 시작되는구나라고 생각되었습니다.

 

밑의 코드는 code 에 정의된 내용의 backtick 을 제거한, 즉 ${...} 에 정의된 내용을 모두 실행한 결과의 코드입니다.

  console.log({flag}); 
  for (i=0; i<100; i++) setTimeout('debugger');
  if ("   .?  K 7 hA  [Cdml<U}9P  @dBpM) -$A%!X5[ '% U(!_ (]c 4zp$RpUi(mv!u4!D%i%6!D'Af$Iu8HuCP>qH.*(Nex.)X&{I'$ ~Y0mDPL1 U08<2G{ ~ _:hys! K A( f.'0 p!s    fD] (  H  E < 9Gf.' XH,V1 P * -P" != ("   .?  K 7 hA  [Cdml<U}9P    8 @@\di "),i+=(x+"").length+12513|1,!("X  HA 8 @ (  D 7 1    @# C+]CeC QG AZ  (!s! K hA_P `G{ ~ _:h\ys! K A( f.`0 p!s    fD] (  H  E < 9Gf.` XH,V1 P * -P")) while(1);
  flag = flag.split('');
  i‍ = 1337;
  pool = 'c_3L9zKw_l1HusWN_b_U0c3d5_1'.split('');
  while (pool.length > 0) if(flag.shift() != pool.splice((i = (i || 1) * 16807 % 2147483647)%pool.length, 1)[0]) return false;
  return true;

보면 알겠지만, checksum(code) 의 반환 값으로 아래와 같이 나온 것을 볼 수 있습니다.

i+=(x+"").length+12513|1

즉 여기에 있는 i 는 진짜 알파벳 i 로 결국에는 i 의 값이 1337이 아니라 13337이 된다는 것을 알 수 있게 해줍니다.

그럼 결론적으로 flag 값은 아래와 같이 연산해보면 구할 수 있게 됩니다.

var index = 13337;
pool = "c_3L9zKw_l1HusWN_b_U0c3d5_1".split("");
var result = "";
while (pool.length > 0)
  result =
    result +
    pool.splice(
      (index = ((index || 1) * 16807) % 2147483647) % pool.length,
      1
    )[0];
console.log(result); //'W0w_5ucH_N1c3_d3bU9_sK1lLz_'

 

이렇게 Flag를 획득하는 것처럼 보이나 Flag 형태가 조금 이상합니다. _로 끝난다는 것은 아직 일부가 남았다는 것으로 보이는데요. 실제로 이 상태로 제출해봤지만 Flag 가 아니라고 합니다.

 

그래서 정말 오랫동안 소스코드를 보면서 대체 나머지 Flag 가 있을 법한 곳이 어디일까 생각하면서 엄청 분석했는데요.

그러던 중에 code 변수 안에 정의되어 있는 if 문에서 비교를 하는 것에 있어서 어떤 문자열과 checksum(code) 함수의 반환 문자열값과 비교하는 코드가 보였습니다. 그러고 보니 생각해보니깐 이 부분 때문에 올바른 password 값을 입력하여도 Access Denied 가 뜬다는 것을 생각해보았습니다. 뭔가 아직 되지 않았다는 뜻이구나라고 짐작하게 해줍니다. 특히 여기서 if 문에서 A != checksum(code) 간에 비교하는 거에 있어서 A 문자열의 값도 결국에는 checksum(어떤코드)의 반환값일 것이라고 생각하여서 이 부분을 어떻게든 분석해보려고 하였는데, checksum 로직 상 암호화된 것을 다시 복호화하기에 적합하지 않게끔 설계되어 있어서(너무나도 많은 변수가 생기기 때문), 조금만 보다가 말았습니다.

 

그러다가 Visual Studio Code 에서 여기가 뭔가 좀 이상하다라고 알려줍니다.

이상하게 주석문에서 document.docuementElement.outerHTML.length 와 setTimeout 이 주석이 아니라 소스코드로 읽혀지고 있는 것이 보였습니다.

 

그래서 이 부분만 따로 다른 파일에 가져와서 열어보니깐 "비정상적인 줄 종결자"라고 합니다.

줄 종결자? 제가 아는 줄 종결자는 \n 와 같은 개행문자인데, 그럼 저걸 개행문자로 인식한다는 걸까요?

그래서 개행문자로 모두 치환을 해보았더니 아래와 같아졌습니다.

checksum(" " + checksum) 함수의 반환 문자열 값이 "..." 문자열 값과 일치하는 지 확인하고 있는 것을 볼 수 있습니다.

대체 저 반환 문자열 값이 뭐길래 싶어서 확인해보려고 개발자도구를 못여니깐 URL 입력창에다가 아래와 같이 입력해서 확인해보았습니다.

javascript:alert(checksum(" " + checksum));

그러더니 나머지의 Flag 문자열이 등장하는 것을 알 수 있었습니다. 위 코드를 깔끔하게 해보면 아래와 같이 생겨먹었습니다.

pA: Object.defineProperty(document /*  `+d({*/.body, "className", {
  get() {
    /*       `  */ return this.getAttribute("class" /*   @*/) || "";
  },
  set(x) {
    this.setAttribute(
      /*   7 , @@X(tw  Y */ "class",
      x != "granted" /*   ,5 @*/ ||
        /*                               s |L Q4 */ /*                             s |L M  */ /*                              Se` h@*/ /*                             ( ) N) H  5! =X*/ /*                      +d   v=A (( */ /*                        (*/ (/^CTF{([0-9a-zA-Z_@!?-]+)}$/.exec(
          /*   * ]#*/ keyhole.value
        ) || x)[1].endsWith(
          /*    9 */ "Br0w53R_Bu9s_C4Nt_s70p_Y0u"
        ) /*   ? [mRP`+d X*/
        ? x
        : "denied"
    );
  },
}); /*          */ //

친절하게도 Flag 값이 이렇게 끝난다라고 알려주는 듯 하네요. 결국 이 코드 때문에 처음에 password 값의 앞부분을 구했더라도 Access Denied 가 뜨는 것이었습니다.

 

그렇게 완성형 Flag 값을 input form 에 입력해주게 되면 아래와 같이 Access Granted 가 되면서 기분 좋은 초록색 글자가 보이게 됩니다.

 

- 끝 -

 

728x90
반응형
댓글