Wargame/HackCTF

[HackCTF] yes_or_no

핏디 2021. 8. 15. 21:12
SMALL

[문제]


[풀이]

바이너리를 실행해서 임의의 숫자를 입력하면 system+1094를 실행해보라는 메시지와 함께 종료된다.

-> do_system+1094는 2.27 glibc 버전에서 system 함수 내에서 발생하는 오류

-> rsp 값이 16바이트로 채워져 있지 않으면 do_system+1094에 존재하는 명령어에서 오류 발생(gadget를 잘 넣어야 한다~~!)

 

gdb 분석 전 보호 기법부터 확인해보면 다음과 같다.

NX bit가 걸려있는 것으로 보아 쉘코드 삽입은 불가해보인다. 또한, RELRO 가 Partial이므로 data, stack, heap 영역이 읽기 권한만 가진다는 것을 알 수 있다. -> Got Overwrite를 떠올릴 수 있다!

 

그리고, ldd 명령을 통해 ASLR이 걸려있음을 확인할 수 있다.(주소들이 실행할 때마다 매번 바뀌고 있기 때문!)

 

즉, ROP을 이용하여 Exploit 해야 한다!

 

gdb를 통해 함수 목록부터 확인해보았다.

main 함수 외에는 특별한 함수가 존재하지 않는다.

 

main 함수를 살펴보면 매우 긴 형태로 되어 있는데, 쉘을 실행할 부분이 보이지 않는다. NX도 걸려 있고, ASLR로 주소도 매번 바뀌기 때문에 RTL 기법을 생각해볼 수 있다. 

 

우선 main 함수의 주요 핵심은 main+235~237이라고 할 수 있다.

 

바이너리는 입력 값과 미리 지정된 값(main+235에서 eax에 담긴 값)을 비교하여(main+237) 분기한다. 이에 main+235에 bp를 걸어 임의의 값을 입력 후 eax레지스터 값을 살펴보면 0x960000임을 알 수 있다. 

 

사용자가 바이너리를 실행하여 입력할 때에는 10진수 형태로 입력해야 프로그램 내부에서 hex형태로 변하므로 9830400을 입력해야 한다.

 

다시 바이너리를 실행하여 해당 값을 입력하면 flag가 아닌 다시 입력을 받고 있다.

 

이에 c코드를 유추해보면 다음과 같다.

main(){
	
    char num;
    int buf
    
    setbuf(stdout, ~, 2, ~);
    
    puts("Show me your number~!");
    fgets(&num, 10, stdin); //사용자 입력
    buf = atoi(&num); //입력 값 형변환(char -> int)
    
    if (buf != 숫자){ //입력 형태가 문자열이 아닌 숫자만 되도록 설정
    	puts("Sorry. You can't come with us");
    }
    
    else{
    
        if(buf == 9830400){ //입력 값이 9830400이면
            puts("That's cool. Follow me"); 
            gets(&s); //입력 값 받음 -> 취약점 발생
        }
        
        else{
            printf("Why are you here?"); //입력 값이 9830400이 아니면
            return 0; //종료
        }
        
        puts("All I can say to you is \"do_system+1094\"."ngood luck");
    }    
    
    return 0;
}

여기서 생각해볼 수 있는 점은 9830400 입력 후 get함수를 실행하여 exploit을 시도해야 한다는 것!

-> gets 함수는 입력 값에 대한 검증을 실시하지 않으므로 BOF 발생할 수 있는 취약점을 가짐.

-> gets 함수를 통해 ret 값을 변경시키면서 exploit 할 수 있음

 

즉, ROP를 이용해 Exploit 해야 한다!

ROP를 다시 remind 해보면,

ROP는 가젯을 이용해서 BOF 공격을 시도하는 공격이고, 그 속에서 ROP를 위해 사용되는 기술들이 RTL, RTL Chaining, GOT Overwrite라는 점!

 

exploit을 위해 생각해볼 수 있는 방법은 크게 2가지가 있다.

1번째 방법 -> rop 이용

1. bss에 /bin/sh 주입

2. put_got(or printf_got) 주소 leak

3. got overwrite

4. puts(or printf) 실행

 

2번째 방법

1. puts 의 plt 주소로 리턴해 puts 의 got 주소 출력

2. puts 의 got 주소로 라이브러리의 베이스주소 leak

3. 라이브러리 베이스주소 + system offset 으로 system 함수 주소 leak

4. main 으로 리턴

5. main 에서 system함수로 리턴 뒤 쉘 실행

 

즉, libc 파일을 이용해 필요한 함수들의 offset을 구하고(ASLR 때문에 정확한 주소를 구할 수 없음. but offset은 항상 고정됨!) 서버의 문제 파일을 이용해서 libc_base 주소를 leak한 뒤(base 주소는 ASLR이 걸려 있어도 고정된 값을 가짐) main 함수로 돌아와서 system 함수를 실행해야 함!!!

 

가장 먼저 ROP gadget을 구해야 하고, cmd에서 ROPgadget --binary [file] 명령을 통해 구할 수 있다.

sik@ubuntu:~/hackctf/yes_or_no$ ROPgadget --binary yes_or_no 
Gadgets information
============================================================
0x000000000040063e : adc byte ptr [rax], ah ; jmp rax
0x0000000000400609 : add ah, dh ; nop dword ptr [rax + rax] ; ret
0x00000000004005c7 : add al, 0 ; add byte ptr [rax], al ; jmp 0x400570
0x00000000004005a7 : add al, byte ptr [rax] ; add byte ptr [rax], al ; jmp 0x400570
0x000000000040060f : add bl, dh ; ret
0x000000000040088d : add byte ptr [rax], al ; add bl, dh ; ret
0x000000000040088b : add byte ptr [rax], al ; add byte ptr [rax], al ; add bl, dh ; ret
0x0000000000400587 : add byte ptr [rax], al ; add byte ptr [rax], al ; jmp 0x400570
0x00000000004007f8 : add byte ptr [rax], al ; add byte ptr [rax], al ; jmp 0x40080f
0x000000000040080b : add byte ptr [rax], al ; add byte ptr [rax], al ; leave ; ret
0x00000000004006bc : add byte ptr [rax], al ; add byte ptr [rax], al ; push rbp ; mov rbp, rsp ; pop rbp ; jmp 0x400650
0x000000000040088c : add byte ptr [rax], al ; add byte ptr [rax], al ; ret
0x00000000004006bd : add byte ptr [rax], al ; add byte ptr [rbp + 0x48], dl ; mov ebp, esp ; pop rbp ; jmp 0x400650
0x000000000040080c : add byte ptr [rax], al ; add cl, cl ; ret
0x0000000000400589 : add byte ptr [rax], al ; jmp 0x400570
0x00000000004007fa : add byte ptr [rax], al ; jmp 0x40080f
0x000000000040080d : add byte ptr [rax], al ; leave ; ret
0x0000000000400646 : add byte ptr [rax], al ; pop rbp ; ret
0x00000000004006be : add byte ptr [rax], al ; push rbp ; mov rbp, rsp ; pop rbp ; jmp 0x400650
0x000000000040060e : add byte ptr [rax], al ; ret
0x0000000000400645 : add byte ptr [rax], r8b ; pop rbp ; ret
0x000000000040060d : add byte ptr [rax], r8b ; ret
0x00000000004006bf : add byte ptr [rbp + 0x48], dl ; mov ebp, esp ; pop rbp ; jmp 0x400650
0x00000000004006a7 : add byte ptr [rcx], al ; pop rbp ; ret
0x000000000040080e : add cl, cl ; ret
0x0000000000400597 : add dword ptr [rax], eax ; add byte ptr [rax], al ; jmp 0x400570
0x00000000004006a8 : add dword ptr [rbp - 0x3d], ebx ; nop dword ptr [rax + rax] ; ret
0x00000000004005b7 : add eax, dword ptr [rax] ; add byte ptr [rax], al ; jmp 0x400570
0x000000000040056b : add esp, 8 ; ret
0x000000000040056a : add rsp, 8 ; ret
0x0000000000400608 : and byte ptr [rax], al ; hlt ; nop dword ptr [rax + rax] ; ret
0x0000000000400584 : and byte ptr [rax], al ; push 0 ; jmp 0x400570
0x0000000000400594 : and byte ptr [rax], al ; push 1 ; jmp 0x400570
0x00000000004005a4 : and byte ptr [rax], al ; push 2 ; jmp 0x400570
0x00000000004005b4 : and byte ptr [rax], al ; push 3 ; jmp 0x400570
0x00000000004005c4 : and byte ptr [rax], al ; push 4 ; jmp 0x400570
0x00000000004005d4 : and byte ptr [rax], al ; push 5 ; jmp 0x400570
0x0000000000400561 : and byte ptr [rax], al ; test rax, rax ; je 0x40056a ; call rax
0x0000000000400568 : call rax
0x0000000000400744 : clc ; sub edx, eax ; mov eax, edx ; jmp 0x400750
0x0000000000400741 : cld ; mov edx, dword ptr [rbp - 8] ; sub edx, eax ; mov eax, edx ; jmp 0x400750
0x000000000040086c : fmul qword ptr [rax - 0x7d] ; ret
0x000000000040060a : hlt ; nop dword ptr [rax + rax] ; ret
0x00000000004006c3 : in eax, 0x5d ; jmp 0x400650
0x00000000004005c2 : jb 0x4005ce ; and byte ptr [rax], al ; push 4 ; jmp 0x400570
0x0000000000400566 : je 0x40056a ; call rax
0x0000000000400639 : je 0x400648 ; pop rbp ; mov edi, 0x601058 ; jmp rax
0x000000000040067b : je 0x400688 ; pop rbp ; mov edi, 0x601058 ; jmp rax
0x000000000040058b : jmp 0x400570
0x00000000004006c5 : jmp 0x400650
0x0000000000400749 : jmp 0x400750
0x00000000004007d6 : jmp 0x40080a
0x00000000004007fc : jmp 0x40080f
0x0000000000400a23 : jmp qword ptr [rbp]
0x0000000000400641 : jmp rax
0x00000000004005b2 : jp 0x4005be ; and byte ptr [rax], al ; push 3 ; jmp 0x400570
0x000000000040080f : leave ; ret
0x00000000004006a2 : mov byte ptr [rip + 0x2009cf], 1 ; pop rbp ; ret
0x0000000000400592 : mov cl, byte ptr [rdx] ; and byte ptr [rax], al ; push 1 ; jmp 0x400570
0x00000000004007f7 : mov eax, 0 ; jmp 0x40080f
0x000000000040080a : mov eax, 0 ; leave ; ret
0x0000000000400747 : mov eax, edx ; jmp 0x400750
0x00000000004006c2 : mov ebp, esp ; pop rbp ; jmp 0x400650
0x000000000040063c : mov edi, 0x601058 ; jmp rax
0x0000000000400742 : mov edx, dword ptr [rbp - 8] ; sub edx, eax ; mov eax, edx ; jmp 0x400750
0x00000000004006c1 : mov rbp, rsp ; pop rbp ; jmp 0x400650
0x0000000000400643 : nop dword ptr [rax + rax] ; pop rbp ; ret
0x000000000040060b : nop dword ptr [rax + rax] ; ret
0x0000000000400685 : nop dword ptr [rax] ; pop rbp ; ret
0x00000000004006a5 : or dword ptr [rax], esp ; add byte ptr [rcx], al ; pop rbp ; ret
0x000000000040067c : or ebx, dword ptr [rbp - 0x41] ; pop rax ; adc byte ptr [rax], ah ; jmp rax
0x000000000040087c : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x000000000040087e : pop r13 ; pop r14 ; pop r15 ; ret
0x0000000000400880 : pop r14 ; pop r15 ; ret
0x0000000000400882 : pop r15 ; ret
0x000000000040063d : pop rax ; adc byte ptr [rax], ah ; jmp rax
0x00000000004006c4 : pop rbp ; jmp 0x400650
0x000000000040063b : pop rbp ; mov edi, 0x601058 ; jmp rax
0x000000000040087b : pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x000000000040087f : pop rbp ; pop r14 ; pop r15 ; ret
0x0000000000400648 : pop rbp ; ret
0x0000000000400883 : pop rdi ; ret
0x0000000000400881 : pop rsi ; pop r15 ; ret
0x000000000040087d : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret
0x0000000000400586 : push 0 ; jmp 0x400570
0x00000000004005d2 : push 0xa ; and byte ptr [rax], al ; push 5 ; jmp 0x400570
0x0000000000400596 : push 1 ; jmp 0x400570
0x00000000004005a6 : push 2 ; jmp 0x400570
0x00000000004005b6 : push 3 ; jmp 0x400570
0x00000000004005c6 : push 4 ; jmp 0x400570
0x00000000004005d6 : push 5 ; jmp 0x400570
0x0000000000400743 : push rbp ; clc ; sub edx, eax ; mov eax, edx ; jmp 0x400750
0x0000000000400740 : push rbp ; cld ; mov edx, dword ptr [rbp - 8] ; sub edx, eax ; mov eax, edx ; jmp 0x400750
0x00000000004006c0 : push rbp ; mov rbp, rsp ; pop rbp ; jmp 0x400650
0x000000000040056e : ret
0x0000000000400746 : ret 0xd089
0x00000000004007aa : retf 0x428d
0x0000000000400638 : sal byte ptr [rbp + rcx + 0x5d], 0xbf ; pop rax ; adc byte ptr [rax], ah ; jmp rax
0x000000000040067a : sal byte ptr [rbx + rcx + 0x5d], 0xbf ; pop rax ; adc byte ptr [rax], ah ; jmp rax
0x0000000000400565 : sal byte ptr [rdx + rax - 1], 0xd0 ; add rsp, 8 ; ret
0x0000000000400745 : sub edx, eax ; mov eax, edx ; jmp 0x400750
0x0000000000400895 : sub esp, 8 ; add rsp, 8 ; ret
0x0000000000400894 : sub rsp, 8 ; add rsp, 8 ; ret
0x000000000040088a : test byte ptr [rax], al ; add byte ptr [rax], al ; add byte ptr [rax], al ; ret
0x0000000000400564 : test eax, eax ; je 0x40056a ; call rax
0x0000000000400563 : test rax, rax ; je 0x40056a ; call rax

Unique gadgets found: 106

* Gadget

pop rdi; ret -> 호출할 함수의 1번째 인자 값을 저장

pop rsi; ret -> 호출할 함수의 2번째 인자 값을 저장

pop rdi; pop rdx; ret -> 호출할 함수의 1번째, 3번째 인자 값을 저장

 

* rdi, rsi, rdx, rcx ... 순서로 인자 값을 레지스터에 저장함!

 

gets 함수는 1개의 인자를 이용하기 때문에 달리 말하면 입력 받는 인자가 1번째 임을 알 수 있다. 따라서 gadget 또한 pop rdi; ret 을 이용해야 한다. 

0x0000000000400883 : pop rdi ; ret 에서 원하는 gadget을 찾을 수 있었다.

 

* x64 환경에서 pr 가젯과 r 가젯은 굳이 ROPgadget으로 찾을 필요가 없고, __libc_csu_init 의 99번째 줄이 pop rdi 이라고 한다..! 99번째 줄이 pr 가젯이니 r 가젯은 100번째 줄!

-> pop pop pop ret gadget을 사용해야 하는데 없을 경우 사용

더보기

포너블 문제를 풀 때, 64Bit 바이너리가 까다로운 이유가 바로 'Gadget' 때문이다.

 

64Bit의 Calling Convention은 Fastcall로 호출된 함수에 인자를 레지스터로 전달한다.

이 때문에 Exploit을 구성할 때도 [POP RDI]와 같은 가젯이 반드시 필요하다.

 

이러한 특이점 때문에 문제에서 가장 흔하게 볼 수있는게 '가젯 제한'이다.

이 상황에서 EDI,RSI,RDX 를 구성할 수 있는 방법이 있는데, 바로 Return_to_csu기법이다.


[ Return to csu란? ]

ELF바이너리를 IDA와 같은 디버깅도구로 열어보면, __libc_csu_init( )를 볼 수 있다.

이 함수가 하는일은 바이너리의 _start( )를 호출하는 함수인 gmon_start( )를 호출하는 역할을 한다.

 

사실 바이너리가 시작되면 main( )가 바로 시작되는 것이 아니다.

__libc_csu_init( ) -> _start( ) -> __libc_start_main( ) 를 거쳐서 main( )가 실행되는 것이다.

 

그럼 어떻게 __libc_csu_init( )에서 인자셋팅을 해줄 수 있는 것일까?

 

[ __libc_csu_init( ) ]

위 사진은 __libc_csu_init( )에서 공격기법에 사용되는 부분이다.

Return_to_csu기법은 2개의 Stage로 나뉘게 된다.

 

[ Attack Flow ]

<Stage1>

1.RET를 0x40075A로 조작한 뒤, 스택에 저장한 데이터들을 각 레지스터에 POP한다.

 

<Stage2>

2.그 다음 RET를 0x400740으로 돌려서 EDI, RSI, RDX를 셋팅한다.

3.R12+RBX*8에 저장된 주소를 Call해서 함수를 호출한다.

이런식으로 인자들을 셋팅하고 원하는 함수를 Call할 수 있게되는 것이다.

인자가 4개 이상인 함수는 호출이 불가능 하다는 점을 유의해야 한다.

 

그럼 상황 하나를 가정하고 각 Stage가 어떻게 돌아가는지 확인해 보도록 하자

현재 상황은 RET를 0x40075A로 핸들링한 상태이며

Return_to_csu를 이용해서 .bss영역에 "/bin/sh\x00" 문자열을 저장하려고 한다.

 

 

[ Stage 1 ]

이렇게 Stage2에서 MOV되는 레지스터를 고려해서 각 레지스터 데이터들을 넣어주고

RET를 Stage2인 0x400740으로 돌려주면 Stage1은 끝난다.

 

 

[ Stage2 ]

Stage2에서는 Stage1에서 설정한 레지스터들을 인자들로 셋팅하고 함수를 Call하는 역할을 한다.

 

READ_GOT에는 __libc_read( )의 주소가 저장되어 있으니

qword ptr로 __libc_read( )주소를 참조해서 Call하는 모습이다.

 

여기서 유의해야 할 점은 qword ptr로 R12공간에 저장된 데이터를 참조한다는 것이다.

내가 main( )를 Call하고 싶다고 R12를 main( )주소로 설정해놓으면 Segmentation Fault가 발생하게 된다.

 

qword ptr로 저장된 데이터를 참조해서 Call하는 것이기 때문에

.bss영역과 같은 저장공간에 main( )주소를 넣어놓고, 해당 .bss주소를 R12로 셋팅해줘야 main( )이 호출된다.

 

자칫하면 실수 할수도 있지만, Call하는 방식이 저렇기 때문에 libc_leak을 하지 않아도 된다는 장점도 있다.

여러모로 쓸모있는 기법이다. :)

 

그리고 RBP값을 1로 설정한 이유는 지속적인 Csu Chaining을 위해서다.

빨간 박스를 C코드로 바꾸면 while( RBX != RBP )를 의미하는데

RBX는 우리가 Call 연산식 때문에 0으로 설정해줬고, ADD로 1이 증가하면 RBX값이 1로 바뀐다.

이 상태에서 JNZ컴페어를 우회하려면 RBX와 RBP가 같도록 해줘야 하기 때문에, 초기에 1로 설정해준 것이다.

 

처음보면 어렵게 느껴질 수 있지만, 한 두번 사용해보면 정말 단비같은 존재가 아닐 수 없다.

해당 기법을 제대로 이해했다고 생각된다면, Pwnable.tw의 'Unexploitable'문제를 풀어보기를 추천한다.

 

여러가지 방법으로도 풀 수 있지만, 위에서 소개한 Return_to_csu기법으로 풀 수 있는 문제 중 하나이다.

 

 

결론적으로 exploit에 넣어야 하는 값과 스택 구조를 표현해보면 다음과 같다.

1번째 dummy부터 2번째 dummy 전까지는 base 주소를 구하기 위한 과정이고, 2번째 dummy 부터가 실제 exploit 하는 과정이라고 생각하면 된다. 

 

해당 값들은 pwntools을 통해 구할 수 있다.

 

-> peda로는 find "/bin/sh"로도 할 수 있다고 한다..!

strings -t x /lib/x86_64-linux-gnu/libc-2.23.so | grep "/bin/sh" 도 가능

 

그리고 dummy를 알기 위해서는 gets 함수의 입력 값이 저장되는 stack과 ret 까지의 거리를 계산해야 하는데, gdb를 통해 쉽게 파악할 수 있다.

 

gets 함수 호출하는 부분에 bp를 걸어 9830400을 입력해주고 임의의 값인 a를 입력해주면,

rax 레지스터를 통해 입력 값이 0x7fffffffdf6e에 저장되는 것을 알 수 있고, ret은 0x7fffffffdf88에 저장되는 것을 알 수 있다. 둘 사이의 offset을 구해보면 1A(26)임을 알 수 있고, SFP(8bytes)까지 포함한 dummy를 26바이트만큼 채워줘야 하는 것을 알 수 있다.

나머지 값들은 pwntools에서 구할 수 있으므로 exploit code를 작성해보면 다음과 같다.

 

 

  • u32 또는 u64를 쓰기 위해서 ljust는 필수이다. 대충 간단하게 설명하자면, u64를 사용하려면 총 8byte로 구성되어 있어야 하는데, leak 주소가 8byte를 모두 차지하고 있지 않으면 빈 공간을 “\x00”으로 채워주는 용도라고 생각하면 된다.

LIST

'Wargame > HackCTF' 카테고리의 다른 글

[HackCTF] 1996  (0) 2021.08.23
[HackCTF] Poet  (0) 2021.08.22
[HackCTF] BOF_PIE  (0) 2021.07.26
[HackCTF] Offset  (0) 2021.07.25
[HackCTF] Simple_Overflow_ver_2  (0) 2021.07.25