본문 바로가기

REVERSING

PINTOOL for CTF / codegate 2020 prequal simple machine

워게임이나 실시간 CTF에서 가끔 "input check" 문제가 등장한다. 사용자가 flag를 입력하면 "OK" 등의 문자열을 출력하는 프로그램이다. 시간 제약이 있는 실시간 CTF에서 이러한 문제에 시간을 적게 들이고 풀 수 있다면 좋을 것 같았다. PINTOOL을 이용하여 실행되는 명령어 수를 측정하여 input check 문제를 side channel attck으로 풀어보았다. 


COMPILE inscount.so

 

pin은 아래 링크에서 다운로드할 수 있다.

https://software.intel.com/en-us/articles/pin-a-binary-instrumentation-tool-downloads

 

리눅스용 압축파일을 다운로드 후 압축해제를 하면 아래와 같은 파일들이 나온다. pin은 pin 바이너리이다. 

"source/tools/"에 pintools 예시 코드들이 있다. 

이 중에서 적당한 폴더에 들어간 다음 "make"를 입력하면 pintools 라이브러리가 컴파일된다. 이제 side channel attack에 필요한 라이브러를 만들어보자. 

 

링크에 inscount.cpp를 보면 프로그램에서 실행되는 명령어 수를 측정하는 프로그램임을 알 수 있다. 다만 문제점이 있다. 실행된 명령어의 수가 파일을 형식으로 출력된다는 것이다. 이는 나중에서 side channel attack을 위해 python code를 작성할 때 thread를 이용하기 번거롭게 한다. 따라서 실행한 명령어 수를 파일이 아닌 표준 출력으로 쓰게 하였다. ("using namespace std"가 없어서 에러가 발생했다. 이 부분도 추가했다. )

https://software.intel.com/sites/landingpage/pintool/docs/81205/Pin/html/

 

// inscount0.cpp

#include <iostream>
#include <fstream>
#include "pin.H"

using namespace std;
ofstream OutFile;

static UINT64 icount = 0;
VOID docount() { icount++; }

VOID Instruction(INS ins, VOID *v)
{
        // Insert a call to docount before every instruction, no arguments are passed
        INS_InsertCall(ins, IPOINT_BEFORE, (AFUNPTR)docount, IARG_END);
}
KNOB<string> KnobOutputFile(KNOB_MODE_WRITEONCE, "pintool",
	    "o", "inscount.out", "specify output file name");
// This function is called when the application exits

VOID Fini(INT32 code, VOID *v)
{
	    cout << icount << endl;
}

/* ===================================================================== */
/* Print Help Message                                                    */
/* ===================================================================== */

INT32 Usage()
{
        cerr << "This tool counts the number of dynamic instructions executed" << endl;
	    cerr << endl << KNOB_BASE::StringKnobSummary() << endl;
	        return -1;
}

/* ===================================================================== */
/* Main                                                                  */
/* ===================================================================== */
/*   argc, argv are the entire command line: pin -t <toolname> -- ...    */
/* ===================================================================== */

int main(int argc, char * argv[])
{
        // Initialize pin
        if (PIN_Init(argc, argv)) return Usage();
	        // Register Instruction to be called to instrument instructions
	        INS_AddInstrumentFunction(Instruction, 0);
		    // Register Fini to be called when the application exits
		    PIN_AddFiniFunction(Fini, 0);
			PIN_StartProgram();
			return 0;

}

"make"로 컴파일 한 후 테스트 해보았다. 성공적으로 작동한다. 


실제 CTF 문제를 풀기 전에 아주 간단한 문제를 만들고 풀어보았다. flag가 눈으로 보이지만, side channel attack이 유효한지 보기 위한 예시이니 신경쓰지 말자. 

#include<stdio.h>
#include<string.h>
#include<stdlib.h>


int obfuscation(char flag, int idx){
    char * test="flag{flag_is_flag}";
    if( ! (flag^test[idx])){
	return 1;
    };
    return 0;
}


void main(){
    char flag[0x20];
    int end=read(0,flag,0x20);
    flag[end]='\x00';
    for(int i =0;i<strlen("flag{flag_is_flag}");i++){
	if(obfuscation(flag[i],i)==0){
	    exit(1);
	};
    }
    printf("OK!!");

}

 

 

PINTOOLS을 자동화하기 위해 python을 이용했다. 간단하게 만든 템플릿은 아래와 같다. 3가지 모드가 있다.

 

  • len : 입력값의 길이를 변화시키며 실행되는 명령어 수 측정 
  • crack : 입력값을 한 바이트 씩 변화시키며 실행되는 명령어 수 측정
  • check : 보통 CTF에서는 flag 형식을 주는 경우가 많다. 이때 flag 형식이 ide channel attack이 유효한지 파악하는데 될 수도 있다. 
from subprocess import PIPE, Popen
import string
import sys
import os
import getopt
from threading import Thread


def cmdline(command):
    process = Popen(args=command,stdout=PIPE,shell=True).stdout
    return process.read()

st=string.printable
count=0
first=0
fisrt_st=""
flag=""

try:
    opts,etc_args= getopt.getopt(sys.argv[1:],"hf:m:l:s:a:",["help","file=","mode=",'len=','start_with=',"argv="])
except getopt.GetoptError:
    print("-f <file> -m <mode> -l <len> -s <start_with> -a <argv for file>")
    exit(1)

for opt,arg in opts:
    if opt=="-f":
	file_name=arg
    elif opt=="-m":
	mode=arg
    elif opt=="-l":
	le=int(arg)
    elif opt=="-s":
	start=arg
    elif opt=="-a":
	argv=arg



if mode=="len":
    for x in xrange(le):
	cmd="/bin/echo '"+"A"*x+"' | ../../pin -t ../../inscount.so -- ./"+file_name
	count=cmdline(cmd)#.read()
	count=int(count)
	if(first<count):
	    first=count
	    print("x = %d count = %d"%(x+1,count))


if mode=="crack":
    for x in xrange(le):
	print("%dcycle"%(x+1))
	for y in xrange(len(st)):
	    if st[y]=="'" or "'" in flag:
		count=cmdline('/bin/echo "'+flag+st[y]+'" | ../../pin -t ../../inscount.so -- ./'+file_name)
	    else:
		count=cmdline("/bin/echo '"+flag+st[y]+"' | ../../pin -t ../../inscount.so -- ./"+file_name)
	    count=int(count)
	    if(first<count):
		first=count
		first_st=st[y]
		print("x = %s count = %lx"%(st[y],count))
	flag+=first_st#in my experience. In this condition st[y] is flag 
	print(flag)
if mode=="check":
    print("check side channel attack")
    real=start
    for x in xrange(int(len(real))):
	count=cmdline('/bin/echo "'+flag+real[x]+'" | ../../pin -t ../../inscount.so -- ./'+file_name)#get target by cmdline
	non=cmdline("/bin/echo '"+"a"*(x+1)+"' | ../../pin -t ../../inscount.so -- ./"+file_name)
	flag+=real[x]
	print "flag = %s  %s"%(flag, count)
	print "NON = %s %s"%("a"*(x+1),non)

 

flag 형식이 "flag"라고 생각하자. check 모드를 통해 side channel attack의 유효성을 예상해보았다. aaaa가 입력되었을 때보다 "flag"가 입력되었을 때 더 많은 코드가 실행되는 것을 볼 수 있다. 이를 통해 side channel attack의 가능성을 짐작할 수 있다. 또한 한 바이트를 더 입력할 때마다 실행되는 코드가 규칙적으로 증가하는 것을 보아 flag 체크로직이 한 바이트씩 검증한다고 짐작할 수 있다. 

 

 

길이에 대한 side channel 어택을 해보니 결과가 제대로 나오지 않는 것을 볼 수 있다. 실제로 prob.c를 보면 길이에 대한 검증로직이 없다. 

 

 

side channel attack의 가능성을 보았으니 crack을 해보았다. 정상적으로 flag를 출력하는 것을 볼 수 있다. 

 

➜  test python ./solve.py -f ./a.out -m crack -l 20

 


pintools을 이용한 side channel attack을 실제 CTF 문제에 적용해보자. 문제는 CODEGATE 2020 예선전에서 출제되었던 simple machine이다. 

 

해당 문제를 리버싱해보면 VM 형식을 가지고 있다는 것을 금방 알 수 있다. 또한 VM 코드 외부에서 당시 flag 형식이었던 "CODEGATE2020"을 검증하거나 flag 길이를 검증하는 코드는 없었다. 

 

__int64 __fastcall VM(__int64 cmd)
{
  __int64 result; // rax
  char v2; // al

  result = *(unsigned __int8 *)(cmd + 48);      // eip
  switch ( (_BYTE)result )
  {
    case 0:
      *(_WORD *)(cmd + 62) = *(_WORD *)(cmd + 52);
      goto LABEL_3;
    case 1:
      *(_WORD *)(cmd + 62) = *(_WORD *)(cmd + 52) + *(_WORD *)(cmd + 54);
      goto LABEL_3;
    case 2:
      *(_WORD *)(cmd + 62) = *(_WORD *)(cmd + 54) * *(_WORD *)(cmd + 52);
      goto LABEL_3;
    case 3:
      *(_WORD *)(cmd + 62) = *(_WORD *)(cmd + 54) ^ *(_WORD *)(cmd + 52);
      goto LABEL_3;
    case 4:
      *(_WORD *)(cmd + 62) = *(_WORD *)(cmd + 52) < *(_WORD *)(cmd + 54);
      goto LABEL_3;
    case 5:
      if ( !*(_WORD *)(cmd + 52) )
        goto LABEL_3;
      result = *(unsigned __int16 *)(cmd + 54);
      *(_BYTE *)(cmd + 46) = 0;
      *(_BYTE *)(cmd + 56) = 0;
      *(_BYTE *)(cmd + 64) = 0;
      *(_WORD *)(cmd + 34) = result;
      return result;
    case 6:
      *(_WORD *)(cmd + 62) = read_((_QWORD *)cmd, *(_WORD *)(cmd + 52), *(_WORD *)(cmd + 54));
      goto LABEL_3;
    case 7:                                     // 7--> write
      *(_WORD *)(cmd + 62) = write(
                               1,
                               (const void *)(*(_QWORD *)cmd + *(unsigned __int16 *)(cmd + 52)),
                               *(unsigned __int16 *)(cmd + 54));
      goto LABEL_3;
    case 8:                                     // have to avoid here
      *(_BYTE *)(cmd + 46) = 0;
      *(_BYTE *)(cmd + 56) = 0;
      *(_BYTE *)(cmd + 64) = 0;
      *(_DWORD *)(cmd + 24) = 0;                // end
      break;
    default:
LABEL_3:
      v2 = *(_BYTE *)(cmd + 49);
      *(_BYTE *)(cmd + 64) = 1;
      *(_BYTE *)(cmd + 58) = v2;
      result = *(unsigned __int16 *)(cmd + 50);
      *(_WORD *)(cmd + 60) = result;
      break;
  }
  return result;
}

 

우선 check 모드를 이용해서 side channel attack이 가능한지 알아보았다. "a"의 수만 증가했을 때와 달리 CODEGATE2020을 입력하면 점점 실행되는 명령어 수가 늘어나는 것을 볼 수 있다. 또한 2바이트가 증가할 때마다 실행되는 코드가 늘어나는 것으로 보아 flag 검증 코드가 2바이트 씩 검증한다고 예측할 수 있다. 

 

 

check 모드에서 알아낸 정보를 이용하여 예측한 VM 내부의 로직은 다음과 같다.  

for x in range(len(flag)/2):
	if input[x*2:x*(2+1)]==flag[2*x:2*(x+1)]:
    	pass
    else:
    	exit(1)

따라서 입력값을 2바이트 씩 바꿔가면서 실행되는 코드 수를 측정하게 템플릿 코드를 바꾸었다.

from subprocess import PIPE, Popen
import string
import sys
import os
import getopt
from threading import Thread


def cmdline(command):
    process = Popen(args=command,stdout=PIPE,shell=True).stdout
#    print(command)
    return process.read()

st=string.printable
st="qwertyuioplkjhgfdsazxcvbnmMNBVCXZASDFGHJKLPOIUYTREWQ1234567890'_{})(*&^%$#@!~`"
count=0
first=0
fisrt_st=""
flag=""

try:
    opts,etc_args= getopt.getopt(sys.argv[1:],"hf:m:l:s:a:",["help","file=","mode=",'len=','start_with=',"argv="])
except getopt.GetoptError:
    print("-f <file> -m <mode> -l <len> -s <start_with> -a <argv for file>")
    exit(1)

i=0
for opt,arg in opts:
    if opt=="-f":
	file_name=arg
    elif opt=="-m":
	mode=arg
    elif opt=="-l":
	le=int(arg)
    elif opt=="-s":
	if(mode=="check"):
	    start=arg
	else:
	    flag+=arg
    elif opt=="-a":
	file_name += " "+arg
	i+=1
if(i==0):
    argv=""


if mode=="len":
    for x in xrange(le):
	cmd="/bin/echo '"+"A"*x+"' | ../../pin -t ../../inscount.so -- ./"+file_name
	count=cmdline(cmd)#.read()
	count=int(count)
	if(first<count):
	    first=count
	    print("x = %d count = %d"%(x+1,count))


if mode=="crack":
    for x in xrange(le):
	for y in xrange(len(st)):
	    print("%dcycle"%y)
	    for z in xrange(len(st)):
		if st[y]=="'" or "'" in flag:
		    count=cmdline('/bin/echo "'+flag+st[y]+st[z]+'" | ../../pin -t ../../inscount.so -- ./'+file_name)
		else:
		    count=cmdline("/bin/echo '"+flag+st[y]+st[z]+"' | ../../pin -t ../../inscount.so -- ./"+file_name)
                if "GOOD!" in count :
                    print(flag + st[y]+st[z])
                    exit(1)
                try:
		    count=int(count)
		except:
		    count=first
		if(first<count):
		    first=count
		    first_st=st[y]+st[z]
		    print("x = %s count = %d"%(st[y]+st[z],count))
	flag+=first_st #in my experience. In this condition st[y] is flag 
        fd=open("flag",'w')
        fd.write(flag)
        fd.close()
	print(flag)
if mode=="check":
    print("check side channel attack")
    for x in xrange(int(len(start))):
	flag+=start[x]
	count=cmdline('/bin/echo "'+flag+'" | ../../pin -t ../../inscount.so -- ./'+file_name)#get target by cmdline
	non=cmdline("/bin/echo '"+"a"*(x+1)+"' | ../../pin -t ../../inscount.so -- ./"+file_name)
        print "NON = %s %s"%("a"*(x+1),non)
        print "flag = %s  %s"%(flag, count)

 

그리고 아래의 커맨드로 flag를 crack 했다. 

 

python solve.py -f ./simple_machine -m crack -l 20 -a ./target -s 'CODEGATE2020'

 

conoha 서버의 제일 싼 서버에서 하루 정도 돌리니 flag가 나왔다. (CPU 3개,RAM 2G) 2바이트 검증이라 시간이 매우 오래 걸린 것 같다.

 

 

 

대회 당시 코드 최적화는 못하고 손으로 프로세스 6씩 돌리고 게싱도 오지게 했었는데, 노가다라도 해서 시간 내에 flag를 얻은 것 같다.


TODO

  1. 쓰레드 이용
  2. 파이썬 코드 구조 수정 
  3. 다양한 경우에 맞출 수 있도록 수정
  4. bash 에러 수정
  5. 전체 코드가 아니라 "cmp"의 숫자만 세게 한다면 실행 시간 단축 가능???

'REVERSING' 카테고리의 다른 글

Trend Micro CTF 2020 / reversing-2 200  (0) 2020.10.06
윈도우 악성코드 분석  (0) 2020.02.29