워게임이나 실시간 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
- 쓰레드 이용
- 파이썬 코드 구조 수정
- 다양한 경우에 맞출 수 있도록 수정
- bash 에러 수정
- 전체 코드가 아니라 "cmp"의 숫자만 세게 한다면 실행 시간 단축 가능???
'REVERSING' 카테고리의 다른 글
Trend Micro CTF 2020 / reversing-2 200 (0) | 2020.10.06 |
---|---|
윈도우 악성코드 분석 (0) | 2020.02.29 |