개요(수업 복기)
오늘(5/7) 수업에서 기존 쉘 코드들이 왜 실행되지 않았는지 다뤘다.(Segmentation Fault)
처음에 만든 쉘 코드는 주소가 상수로 박혀있어서 실패했다.
수업 중 shellcode 변수는 어디에 저장되는지에 대해 교수님이 질문했다.

그런데 순간 생각이 나지 않았고, “일단 문자열이니까 .rodata에 있겠지” 같은 생각을 하고 있었다.
1주일 후에 또 시험인데 X됐다고 볼 수 있다.

오케이 복습 완료. char shellcode[] 는 전역 배열이기 때문에 data 영역에 초기화 되어 저장되는 것이 맞다.
복습 차원에서 몇가지 예시를 들어보면:
char* shellcode= “asdf” 였다면 asdf는 .rodata에 저장되었을 것이다.- 또한 포인터 변수 shellcode 자체는 .data에 저장되어 있을 것이다.
- char shellcode[] = “asdf”가 로컬 배열이었다면, stack에 저장되었을 것이다.
두번째는 실행은 되었지만 널로 인해 동작이 일관성이 없을 수 있어서 추가적인 정리가 필요했다.
코드 내의 null bytes를 어디 다른데서 얻을 수 있는 null로 대체하는 작업이었다.
이렇게 해서 쉘 코드 집어넣고 실행되었다.
이걸 잘 exploiting하면 특정 파일을 실행했을 때, rm -rf ~ 같은 명령어를 시스템콜로 호출 가능할것이다.
rm -rf / 은 sudo 권한이 없어서 안될까?
어쨌든 시스템 콜은 커널 영역에서 실행될텐데 말이다.
환경 구축하기
그동안 32비트 시스템이 없어서 못해보고, 맨날 고민만 했다.
대 AMD64, ARM64 시대에 어디서 x86 시스템을 구하나?
집에 걸어가다가 fail fast라는 개념이 생각났다.
복구 비용이 적을 때 빨리 실패하고 그 결과에서 빨리 피드백하라는 것이다.
잊고 있었는데 사실 테스트 코드, 테스트 코드를 넘어 ‘테스트 자체’의 목적 중 하나가 fail fast였다.코드를 최대한 프로덕션 환경과 가깝게 하고, 의도대로 잘 수행되는지 fail fast하게 검증하는 것이다.
그래서 나도 failfast 정신에 입각해 AMD64 시스템을 구해(AWS EC2), 일단 실행해보기로 했다.
생각해보면 x86_64 = AMD64 = “IA-32 Extended” 이며, 어쨌든 32비트 하위호환성을 가진다.
자세한 건 모르지만 어쨌든 호환되겠거니 싶었다.
환경 구축(처음부터)
Dockerfile
시스템 콜 호출 어셈 분석하기
먼저 execve 시스템콜을 호출하는 프로그램을 작성하고, 컴파일 한 후 어셈블리를 본다.



sysenter 후 int $0x80을 한다고?
이건 왜 이런지 모르겠다.


인자를 넣고 execve를 호출하면, 그 내부에서 스택으로 넘어온 인자들을 레지스터에 각각 저장하고, vdso를 통해 __kernel_vsyscall을 호출해 최종적으로 시스템콜 인터럽트를 날리고 있다.
어셈블리로 재작성하기
우리는 여기서 필수적인 요소만 뽑아볼 수 있다.
- 인자 문자열
- 레지스터에 인자 문자열 주소 저장
- 시스템콜 인터럽트
위 요소들만 있으면 시스템콜을 호출해 실행중인 프로세스를 쉘로 바꿀 수 있다.


그래서 그 부분만 다시 적은 것이 위 어셈블리다. (읽기 편하라고 일부러 10진수로 쓴 것이 강의자료와 조금 다르다)
그리고 재미있게도 실행되지 않는다.
한 줄씩 실행하면서 원인을 알아보자.

여기서 바로 segmentation fault가 발생했다.
esi에 저장된 주소가 어느 영역인지를 고민해보면 쉽다. 바로 .rodata 영역이다.

쓰기 권한이 없는 영역에 쓰기를 수행했기 때문에 segfault가 발생했다.
해당 영역(.text) 영역에 쓰기 권한을 주면 실행된다.
그러나 우리가 원하는 건 이 쉘코드를 다른 프로그램에 집어넣는것. 다른 프로그램들은 보통 code 영역에 쓰기 권한이 없다.
따라서, 이 쉘 코드를 잘 추출해서 쓰기 가능한 영역 (스택 영역)에 넣고, 실행해보도록 하자.

0x8049755 가 LC0인것 같다.그러면, 0x8049755+0x7(/bin/sh) 이 바로 쉘코드의 끝부분이라고 할 수 있다.
따라서, 프로그램의 시작부터, 끝까지 덤프를 뜨도록 하자.
dump memory shell_dump 0x08049728 0x8049755+0x7xxd -p <덤프파일명> | tr -d '\n' | sed 's/../\\x&/g'\xbe\x55\x97\x04\x08\xc6\x46\x07\x00\x89\x76\x08\xc6\x46\x0c\x00\xb8\x0b\x00\x00\x00\x89\xf3\x8d\x4e\x08\xba\x00\x00\x00\x00\xcd\x80\xb8\x01\x00\x00\x00\xbb\x00\x00\x00\x00\xcd\x80\x2f\x62\x69\x6e\x2f\x73\x68이걸 이제 C 프로그램에 넣고 실행한다.


아까와 터진 부분이 다르다. 이번엔 실행 권한이 없는 것이 문제다.
해당 코드를 스택에 집어넣고, 스택 자체에 실행 권한을 줘본다.


0x8049755 위치는 .text영역이다.익숙한 주소일텐데, 아까 쉘 코드의 LC0 영역에 대한 주소다.
이것이 하드코딩 되어 있기 때문에 segfault가 발생한 것이다.
아무리 쉘코드를 스택 영역으로 옮기고, 스택 영역 자체에 실행 권한을 주었더라도, .text 영역에 write를 하고 있으니 즉시 segfault가 발생할 수 밖에 없다.
하드코딩된 주소를 동적으로 얻어올 수는 없을까?
위 기법을 적용해 컴파일한 프로그램에서 쉘 코드를 뽑는다.

시작은 0x8049728, 끝은 (esi에 저장된 문자열의 주소 + 문자열 길이(7)) = 0x8049758+7 이다.

\xeb\x29\x5e\xc6\x46\x07\x00\x89\x76\x08\xc6\x46\x0c\x00\xb8\x0b\x00\x00\x00\x89\xf3\x8d\x4e\x08\xba\x00\x00\x00\x00\xcd\x80\xb8\x01\x00\x00\x00\xbb\x00\x00\x00\x00\xcd\x80\xe8\xd2\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68이제 이걸 아까 shelltest.c 에 넣고 실행해보자. 구분을 위해 shelltest2.c로 복사해서 수행하겠다.

마침내 쉘코드를 실행할 수 있었다.
여기서 끝이 아니다.
쉘코드를 정말로 어디다 투척하려면, 트리밍이 필요하다.
실행 환경(공격 대상)에 따라 null로 인해 읽기&실행이 끊겨버릴 수 있기 때문이다.
strcpy는 스택버퍼오버플로우 취약점이 존재한다.
그러나 이 함수는 널을 만날 때까지 버퍼를 읽어 복사하기 때문에 쉘 코드에 널이 존재하는 경우 온전히 삽입되지 않는다.



그래서 몇가지 기법을 통해 쉘코드에서 널을 모두 제거해줄 수 있다.
어떻게든 00을 동적으로 생성하고, 위 널들을 그것으로 대체해주면 되는것이 아니겠는가?
예를 들면, xor을 통해 동적으로 0을 레지스터에 저장할 수 있다.


아직도 null이 있는 이유는 인스트럭션 때문이다.
mov $11, %eax 인스트럭션의 경우 좌변에 32비트를 그대로 받는다.
거기에 11(0xb)를 집어넣으니, 0x0000000b가 되어버린다.
그래서 잘 살펴보면 첫째줄 끝과 둘째줄 시작에서 b0000000을 발견할 수 있다. (little-endian)
이건 더 작은 단위를 다루는 instruction, movb 및 %al을 사용해 제거할 수 있다.



또 다른 널은 두번째 시스템콜 exit에서 사용하는 mov $1, %eax 인스트럭션에서 발생한다.
위와 같은 이유이다.
여기서는 조금 다른 방식으로, eax가 이미 0이기 때문에, inc 인스트럭션을 사용해보자.


이제 실행해보자. 구분을 위해 shelltest3.c로 복사해서 사용한다.

실행이 안된건 바로 movb $11, %al 이후에 문제가 있기 때문이다.
그 이후의 eax에는 0이 아니라, 11이 들어있다.
따라서 mov %eax, %edx 가 의도대로 일어나지 않았고, 시스템콜이 정상 호출되지 않았다.
우리는 저 부분 이후를 변경해야 한다.
구분을 위해 shell3.c로 복사해 진행한다.

eax가 오염된 이후의 인스트럭션들을 대체했다이제 이 쉘코드를 뽑아서, 다른 프로그램에 집어넣고 실행하면?

목적 달성.
*%gs:0x10?lea Instruction