System hacking ctf 문제를 풀다보면, 쉘코드가 필요할 때가 있는데요. 직접 어셈블리어로 짤 수 있어요, 하지만 이 방법은 타켓 아키텍처에 익숙하지 않은 이상 까다로운 일이에요.
그 다음으로 웹 상에서 원하는 쉘 코드를 가져올 수 있는데요. 대표적으로 shell-storm와 같은 사이트에서 내가 원하는 쉘 코드를 복사하면 편리하지요. 하지만 쉘코드들은 가지각색으로 등록되어 있고, 원하는 쉘 코드가 존재하지 않을 수도 있어요.
그래서 직접 GCC inline assembly과 objcopy의 조합으로 쉘 코드를 만드는 방법에 대해 알아볼게요. 이미 inline assembly에 능숙하다면 손쉽게 (glibc의 syscall 함수와 비슷하게) 쉘코드를 짤 수 있어요 .
우선 아래와 같은 코드에서 쉘코드를 삽입한다고 생각해볼게요.
이제 인라인 어셈블리를 통해 입맛대로 인자와 원하는 시스템 콜을 호출하는 오브젝트 파일을 생성해볼게요.
이 함수는 "bin/sh" 문자열을 스택에 저장하고, 이 값을 인자로 사용하면서 execve 시스템 콜을 호출해요. 우선 인라인 어셈블리의 포맷을 간단하게 알아보도록 할게요.
asm asm-qualifiers ( AssemblerTemplate
: OutputOperands[ : InputOperands[ : Clobbers ] ])
인라인 어셈블리의 문법 자체는 위와 같이 정의되어 있어요. AssemblerTemplate에 인스트럭션들이 들어가는데요, 위의 경우 "syscall"이 이에 해당해요. 그리고 OutputOperands와 InputOperands를 설정할 수 있는데요, 이 인라인 블록에 입력과 출력을 전달할 수 있는 속성이에요. 위와 같은 경우에는 RAX 레지스터에 SYS_execve의 값을, RDI 레지스터에는 변수 path를, RSI/RDX 레지스터에는 상수 0을 입력으로 전달해요. 이 정도만 설명하고 넘어가도록 할게요 ;)
Inline assembly에 자세한 내용은 이 링크를 통해 확인할 수 있어요
해당 파일을 아래와 같이 컴파일했을 때 나온 오브젝트 파일을 (intel style로) disassemble하면 아래와 같이 나와요.
$ gcc -c -o shellcode shellcode.c
$ objdump -M intel -d shellcode
덤프된 코드를 보면 우리가 shellcode뿐만 아니라, 스택 프레임 설정, 스택 프로텍션에 관련된 instruction들이 함께 있어요.
이것들을 제거하기 위해 몇 가지 컴파일 옵션을 추가해볼게요.
-fomit-frame-pointer: 스택 프레임을 저장하고 복원할 필요 없는 함수의 경우 저장/복원을 생략한다.
-fno-stack-protector: 스택 프로텍션 기능을 비활성화 한다.
위 두 옵션을 추가하고, 컴파일하고 dump하면, 우리가 원하는 깔끔한 shellcode가 보여요.
$ gcc -fno-stack-protector -fomit-frame-pointer -c -o shellcode shellcode.c
$ objdump -M intel -d shellcode
아쉽게도 만들어진 파일은 elf 파일이라 elf 헤더도 포함하고 있어요. 따라서 해당 오브젝트 파일의 text 섹션만 추출해서 binary 파일을 생성할게요. 이 과정은 objcopy 유틸리티를 활용하면 간단하게 할 수 있어요.
$ objcopy -O binary --only-section .text shellcode bin
shellcode 파일의 불필요한 헤더와 다른 섹션들은 제외하고 "bin"이라는 이름의 binary(raw) 형태의 파일을 생성했는데요. 만들어진 binary 파일을 xxd를 통해서 잘 만들어졌는지 확인해볼게요.
$ xxd bin
objdump에서 보이는 instruction들만 있다면 성공적으로 쉘코드를 만든 것이에요. 이제 해당 쉘코드를 위 예제 코드에 입력으로 준다면, 쉘이 실행되는 것을 확인할 수 있어요.
$ (cat bin; cat) | ./run
아래는 만든 쉘 코드를 이용하여 쉘을 얻는 영상이에요
이제 위와 같은 방식을 응용하여, 여러가지 시스템 콜을 호출하는 코드(ex setuid/execve, open/read)들도 작성할 수 있어요.
단, inline assembly에서 input operand를 r8~r10 레지스터로 지정할 수 없다. 따라서, 해당 레지스터를 사용하기 위해서는, Local register variable 형태로 사용해야 해요. 아래에는 그 예제 코드가 있다.
위 예제에서는, syscall 인자인 r10과 r8 레지스터에 값을 전달하기 위해 Local register variable을 선언했으며, 해당 변수 명을 output operand에 넣어요. 이 C 코드를 쉘코드로 만드는 방법은 위에서 했던 것이랑 동일해요!
정리
인라인 어셈블리에 익숙하다면, 여러모로 재밌는 방법이라 생각해요 ;)
Comments