블로그 이전했습니다. https://jeongzero.oopy.io/
[Christmas CTF 2020] angrforge
본문 바로가기
워게임/CTF 문제들

[Christmas CTF 2020] angrforge

728x90

1. 문제


히키코모리 생활을 즐기고 있던 A씨… B모 게임사의 W게임을 즐기던 중 게임 몬스터가 현실로 나타나 그의 컴퓨터를 훔쳐갔다… 취미도 일도 모두 빼앗겨버린 그는 특별한 퀘스트를 발행하는데…

퀘스트란 어떤 정확한 문자열을 입력하면 되는것으로 보인다. 여기가 플래그 겠지..

2. 접근방법


__int64 __usercall main@<rax>(char **a1@<rsi>, char **a2@<rdx>, __int64 a3@<rbp>)
{
...

l Angerforge, the Dark Iron responsible for stealing my computer.", a1, a2);
  print("But I'm just a programmer.. so Call me my best warrior friend.", a1, v3);
  print("If you call my friend, I will give you a good reward.", a1, v4);
  fgets_func();
  analysis_func(&input_);
  if ( cmp_byte_input(&input_) == 1 )           // 변환된 input값과 byte_8040에 있는 문자열을 1byte씩 비교
    print("OMG, Thank you for your good works :)", 57LL, v5);
  else
    incorrect(&input_, 57LL);
  result = 0LL;
  if ( __readfsqword(0x28u) != v15 )
    result = sub_10B0();
  return result;
}
  • fgets_func() 함수로 입력을 받는다
  • analysis_func() 함수로 입력값에 대한 뮤테이션이 진행된다
  • cmp_byte_input() 뮤테이션 된 입력값을 가지고 특정 값과 비교한다. 전부 일치한다면 1을 반환한다. 결국 여기를 1로 반환되게 만들어야 한다

analysis_func() 함수가 중요하다.

_BYTE *__fastcall analysis_func(char input[56])
{
  _BYTE *result; // rax

  if ( strlen_func() >= ((name[0] | name[1]) + 5) )// input길이와 0x38과 비교
  {
    sub_11C9(input);
    sub_1396(input);
    sub_1563(input);
    sub_1730(input);
    sub_18FD(input);
    sub_1ABF(input);
    sub_1BBF(input);
    sub_1CCE(input);
    sub_1DCE(input);
    sub_1EEE(input);
    sub_22CB(input);
    sub_2589(input);
    sub_2847(input);
    sub_2B05(input);
    sub_2DC3(input);
    sub_2EF9(input);
    sub_3035(input);
    sub_3171(input);
    result = sub_32AD(input);
  }
  else                                          // 여기로 빠지면 안됨
  {
    sub_33E9(input);
    sub_3481(input);
    sub_3519(input);
    sub_35B1(input);
    sub_3649(input);
    sub_36E1(input);
    sub_3779(input);
    sub_3811(input);
    sub_38A9(input);
    result = sub_3941(input);
  }
  return result;
}

보면 입력값이 56바이트 이상일때와 미만일때에 따라서 뮤테이션 로직이 달라진다. 뮤테이션 로직은 엄청 복잡한데, 각 함수 내부에서도 또 다른 함수를 호출하는 방식이다. 처음 해당 문제를 풀때 모든 로직을 분석해서 풀어야하는건가.. 라고 생각해서 하다가 포기했다.

롸업을 보니까 이름자체에서 힌트를 얻을수 있다고 한다. 바로 angr를 이용해서 쉽게 해당 문제를 푸는게 포인트이다.

Angr란?

파이썬 기반의 바이너리 분석 도구로서 smt-solver 중 하나인 z3-solver를 기반으로 만들어졌다. 주 기능은 정적, 동적으로 symbolic 분석을 진행한다.

symblic 분석이란 퍼징에서 코드 커버리지 개념과 비슷한거 같다. 예를 들어 다음과 같은 코드다 있다고 해보자.

#include <stdio.h>

int main(int argc, char** argv)
{
	if(argv[1]==3)
	{
		if(argv[2]==4)
		{
			...
		}
		else
		{
			...
		}
	}
	else
	{
		...
	}


	return 0;	
}

사용자의 입력값에 따라서 분기가 달라진다. 덤퍼징 같은 경우는 랜덤하게 분기가 달라지겠지만 코드 커버리지 같은경우 커버리지를 자체적 알고리즘으로 넓혀가는 방식으로 퍼징이 진행된다.

이처럼 symbolic 분석도 다양한 분기로 진행될 수 있게끔 path condition 조건을 달면서 수행한다. symbolic 분석을 편하게 진행하기 위해서 path condition을 만들어주는 도구가 필요한데 이 도구를 SMT-solver라고 부르고 대표적으로 z3가 해당 도구이다.

이러한 z3 도구로 path condition을 생성하고 실제 symbolic 분석을 쉽게 진행하게 도와주는 툴이 바로 angr이다.

따라서 angrforge 문제를 풀기 위해서 angr 사용법을 익힌다음 문제를 풀면 된다.

3. 풀이


angr를 처음 써봐서 롸업 코드를 봐도 이해가 안됬었다. 해당 도구의 사용법을 어느정도 익힌 후에 코드가 이해됬다.

코드 출처 : https://hackyboiz.github.io/2020/12/29/idioth/christmasctf2020-angrforge/

import angr
import claripy

p = angr.Project('./angrforge')  // 바이너리 로드

flag_chars = [claripy.BVS('flag_%d' % i, 8) for i in range(56)]
flag = claripy.Concat(*flag_chars + [claripy.BVV(b'\n')])

st = p.factory.full_init_state(
    stdin = flag,
    add_options = angr.options.unicorn,
)

for i in flag_chars:
    st.solver.add(i != 0)
    st.solver.add(i != 10)

sm = p.factory.simulation_manager(st)
sm.run()

for i in sm.deadended:
    if b'OMG' in i.posix.dumps(1):
        print(i.posix.dumps(0))

코드를 이해해보자

flag_chars = [claripy.BVS('flag_%d' % i, 8) for i in range(56)]
flag = claripy.Concat(*flag_chars + [claripy.BVV(b'\n')])
  • 표준입력을 받는 문자열을 세팅하는 부분이다. claripy 모듈은 바이너리 실행시 입력값을 매번 다르게 주기 위한 기능이다. flag_chars에는 flag_%d 형태로 8비트 값이 총 56개 저장된다.
  • 입력값의 마지막에 개행을 추가하기 위해 concat으로 이어 붙인다

st = p.factory.full_init_state(
    stdin = flag,
    add_options = angr.options.unicorn,
)
  • 초기 프로그램을 돌리기 위한 초기화 및 환경 설정을 하는 로직이다. stdin을 위에서 설정한 flag값으로 지정한다.
  • 바이너리가 수행될때 마다 영구적으로 초기화를 진행시키기 위해서 unicorn 옵션을 추가한다

for i in flag_chars:
    st.solver.add(i != 0)
    st.solver.add(i != 10)

sm = p.factory.simulation_manager(st)
sm.run()
  • 입력값에서 NULL 과 개행 문자는 포함되지 않도록 제한한다
  • 위에서 설정한 설정값을 토대로 angr 분석 머신이 동작한다

for i in sm.deadended:
    if b'OMG' in i.posix.dumps(1):
        print(i.posix.dumps(0))
  • 분석머신이 수행되면서 바이너리가 종료될때의 수행한 정보를 가져온다
  • posix.dump(0)은 해당 바이너리가 수행될때의 표준입력값이다.
  • posix.dump(1)은 해당 바이너리가 수행한 표준 출력 값이다.
  • 따라서 for문을 돌면서 표준 출력에 OMG 즉, 성공했을시 그때의 표준입력값을 출력하는 로직이다

결과 화면 )

4. 몰랐던 개념


  • angr 그 자체...
  • 보통 리버싱 문제에서 복잡한 연산이 필요할 때 사용한다고 한다.
  • 포너블 - fuzzing , 리버싱 - angr 고런 느낌인듯..

5. 참고 문헌


728x90

'워게임 > CTF 문제들' 카테고리의 다른 글

[Christmas CTF 2020] lock  (0) 2021.01.17
[Christmas CTF 2020] phantom  (0) 2021.01.15
[Christmas CTF 2020] show me the pcap  (0) 2021.01.14
[star CTF 2019] hack me  (0) 2020.12.17
[0ctf 2019] babykernel2  (0) 2020.12.08