0%

pwnable.kr——input+leg

pwnable.kr —— input

先看看题目代码

image-20210103152433405

stage1

要求argc值为100,就是除文件名外还需要99个参数,argv['A']="\x00"argv['B']="\x20\x0a\x0d",要满足最后argv[argc]=NULL

stage2

要求从fd:0(stdin)处读出4字节\x00\x0a\x00\xff,从fd:2(stderr)处读出四字节\x00\x0a\x02\xff,这里直接读是读不了的,要使用dup2函数将文件描述符克隆到 0和2

stage3

设置一个环境变量,好说

stage4

打开一个文件名是回车的文件,读四字节\x00\x00\x00\x00

stage5

image-20210103153255873

这里好好学一下网络编程的一些东西,之前一直喜欢忽略

socket函数(返回socke描述符)

1
2
3
#include <sys/socket.h>
int socket(int family,int type,int protocol);
//返回:若成功则为非负描述符,若出错则为-1

family参数指明协议簇,决定了socket的地址类型,如AF_INET决定了要用IPv4地址(32位的)与端口号(16位的)的组合、AF_UNIX要使用一个绝对路径名作为地址

family 说明
AF_INET (一般用) IPv4协议
AF_INET6 IPv6协议
AF_LOCAL Unix域协议
AF_ROUTE 路由套接字
AF_KEY 密钥套接字

type参数指明socket类型

type 说明
SOCK_STREAM 字节流套接字
SOCK_DGRAM 数据报套接字
SOCK_SEGPACKET 有序分组套接字
SOCK_RAW 原始套接字

protocol指协议,一般赋值0让系统自动选择

(这三个参数更详细的内容可以在这里找到)

sockaddr_in结构体

用来设置具体地址给bind函数当做参数,因为一些字节序的原因不直接使用sockaddr,但两者目的等价;设置好协议簇之后,IP地址的sin_addr.s_addr可以用INADDR_ANY表示本机,也可以用具体的IP地址,如inet_addr("192.168.1.1");然后端口号使用hton()封装,转换字节序

这里题目的意思就是将我们argv['C']处的值作为绑定的端口号

bind、listen、accept、recv

设置好结构体后此程序作为服务端程序,bind对应端口,然后监听,listen函数的第一个参数即为监听的socket描述字,第二个参数为相应socket可以排队的最大连接个数。socket()函数创建的socket默认是一个主动类型的,listen函数将socket变为被动类型的,等待客户的连接请求。

这里accept返回的是新的已连接的socket描述字,和前面的监听socket描述字不一样,监听socket描述字一直存在,而当服务器完成某服务后已连接的socket描述字就会被关闭

之后使用recv接受四字节内容

更加具体的描述可以看这篇文章,同时这里还有一张图表示建立连接与TCP三次握手的关系

solution

直接上代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>

char *P="/home/pwner/Desktop/pwnable.kr/input2/input";//写题时换成pwnable.kr那边的路径
int main(){

char *Argv[101];
char *Envp[2];
//argv
int i;
for(i=0;i<100;i++)Argv[i]="\x20";
Argv['A']="\x00";
Argv['B']="\x20\x0a\x0d";
Argv[100]=NULL;//设置参数
//env
Envp[0]="\xde\xad\xbe\xef=\xca\xfe\xba\xbe";
Envp[1]=NULL;//设置环境变量

int fd1=open("./infile" , O_CREAT | O_RDWR , S_IRWXU | S_IRWXO | S_IRWXG);
int fd2=open("./errfile", O_CREAT | O_RDWR , S_IRWXU | S_IRWXO | S_IRWXG);
//这里创建两个文件打开后用于将0,2文件描述符覆盖

write(fd1,"\x00\x0a\x00\xff",4);
write(fd2,"\x00\x0a\x02\xff",4);

dup2(fd1,0);
dup2(fd2,2);
lseek(fd1,0,SEEK_SET);//写文件之后要调用lseek将文件指针重置一下
lseek(fd2,0,SEEK_SET);

close(fd1);close(fd2);

int fd3=open("./\x0a", O_CREAT | O_RDWR , S_IRWXU | S_IRWXO | S_IRWXG);//这里创建需要的“回车”文件
write(fd3,"\x00\x00\x00\x00",4);
close(fd3);

Argv['C']="8888";

execve(P,Argv,Envp);
}

然后我解题的具体做法是进入pwnable服务器之后,cd /tmp ,mkdir ljc ,cd ljc ,touch solu.c , vim solu.c

然后把这段代码写进去,修改一下可执行文件的路径保存,再 ln -s flag /home/input2/flag 为了最后一步cat flag,gcc编译之后运行,此时就会等待stage5,再从一个新的命令行用python输入就行

image-20210104202957472

pwnable.kr —— leg

一道ARM汇编的题目,来入门一下arm的汇编

ARM学习

从这个教程开始

搭环境我创了一个全新的ubuntu 20 虚拟机…装了大概一天的各种依赖等等,把python2装上去了,还有什么re2c,Ninja,meson,编译安装了一个qemu,东西还挺多的,然后又琢磨了一晚上qemu树莓派虚拟机的网络问题….照着这里(这里的helper最后还是没用,琢磨了一个下午也没琢磨出来。。tcl)还是用教程的指导把树莓派虚拟机网络问题解决了,我起qemu的命令是这样的

1
2
3
4
5
6
7
8
9
10
11
stty intr ^]
qemu-system-arm -kernel /home/coldshield/qemu_vms/kernel-qemu-4.4.34-jessie \
-cpu arm1176 \
-m 256 \
-M versatilepb \
-serial stdio \
-net nic \
-nic tap,ifname=tap0,script=no,downscript=no\
-append "root=/dev/sda2 rootfstype=ext4 rw console=ttyAMA0,115200" \
-hda /home/coldshield/qemu_vms/2017-03-02-raspbian-jessie.img \
-no-reboot

进去之后我是设置静态DNS还有网络,然后让虚拟机和host还有外网都能ping通,(参照过这里,后来自己又谷歌了一下静态配置,用了一个静态的IP还有DNS连上我的tap),然后因为镜像的磁盘空间不够又给手动扩了个容….参照这里。还有树莓派虚拟机里面的gdb只能支持python2的gef,那个教程没更新了吧可能,用gef-legacy就行,反正全部弄清楚是啥然后搭起来断断续续搞了两三天的样子….

随手记一些教程上的东西,可能会特别粗略

常见访存指令&后缀

1
2
3
4
5
6
7
8
9
10
11
ldr = Load Word
ldrh = Load unsigned Half Word
ldrsh = Load signed Half Word
ldrb = Load unsigned Byte
ldrsb = Load signed Bytes

str = Store Word
strh = Store unsigned Half Word
strsh = Store signed Half Word
strb = Store unsigned Byte
strsb = Store signed Byte

一些寄存器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

R0 – General purpose
R1 – General purpose
R2 – General purpose
R3 – General purpose
R4 – General purpose
R5 – General purpose
R6 – General purpose
R7 – Holds Syscall Number 系统调用号
R8 – General purpose
R9 – General purpose
R10 – General purpose
R11 FP Frame Pointer 类似EBP
R12 IP Intra Procedural Call
R13 SP Stack Pointer 类似ESP,以字节为单位
R14 LR Link Register 函数返回时存放返回地址
R15 PC Program Counter 类似EIP,但实际内容不是存放next的地址,而是存放当前执行指令+2指令的地址(ARM模式值+8,Thumb模式值+4,这里应该是跟指令流水线有关?)
CPSR – Current Program Status Register

函数调用时前四个参数存在 r0-r3

Thumb

When writing ARM shellcode, we need to get rid of NULL bytes and using 16-bit Thumb instructions instead of 32-bit ARM instructions reduces the chance of having them

可以用BX还有BLX指令设置寄存器最低位为1来切换ARM和Thumb模式,执行的时候最低位会被忽略所以不会产生地址对齐问题

具体的指令格式相关还是看教程吧….这里记一个大概

1
2
3
4
5
6
7
MNEMONIC{S}{condition} {Rd}, Operand1, Operand2
MNEMONIC - 指令助记符
{S} - 可选后缀,如果设置S,flags将根据操作结果进行更新
{condition} - 执行指令所需的条件
{Rd} - 目的寄存器,用于存储指令结果(不过后续提到的ldm不是)
Operand1 - 寄存器或者立即数
Operand2 - 立即数或寄存器(可用于增加、偏移)

几种寻址方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ldr r2, [r0]         最普通的,有效地址就是r0的值(r0)
ldr r1, [pc, #12] 这里有效地址是(PC)+12(arm模式 4单位对齐),不是当前执行指令地址加12

str r2, [r1, #2] 这里有效地址是(r1)+2,r1不发生更改,offset
str r2, [r1, #4]! 这里有效地址是(r1)+4,第一操作数有感叹号,所以r1发生更改 r1=r1+4 ,pre-indexed
ldr r3, [r1], #4 这里有效地址是(r1),r1发生更改 r1=r1+4 ,post-indexed

str r2, [r1, r2]
str r2, [r1, r2]!
ldr r3, [r1], r2
这三种跟上面比较相似,只不过是把立即数换成了r2寄存器的值,更改的依然是r1(用r2的值相加)

str r2, [r1, r2, LSL#2] 有效地址是(r1) + ((r2)<<2),offset
str r2, [r1, r2, LSL#2]! 有效地址是(r1) + ((r2)<<2),r1发生更改 r1= (r1) + ((r2)<<2) ,pre-indexed
ldr r3, [r1], r2, LSL#2 有效地址是(r1),r1发生更改 r1= (r1) + ((r2)<<2) ,post-indexed

ARM中的立即数是最后操作数12中的,前8位(n)和后4位(r)的组合表示, v = n ror 2*r,循环右移得到,用于12位表示32位数

访存指令(Multiple)&后缀

这里有一串类似猝发式传送的指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
adr r0, words+12             /* address of words[3] -> r0 ,adr是取地址的指令*/
ldr r1, array_buff_bridge /* address of array_buff[0] -> r1 */
ldr r2, array_buff_bridge+4 /* address of array_buff[2] -> r2 */

ldm r0, {r4,r5} /* words[3] -> r4 = 0x03; words[4] -> r5 = 0x04 */
//ldm:load多个数据到目的寄存器,这里是将r0指向的连续内存数据分别load到r4 r5,默认情况下是r0增加的连续方向,此时执行后r0的值不会变
stm r1, {r4,r5} /* r4 -> array_buff[0] = 0x03; r5 -> array_buff[1] = 0x04 */
//stm则是将r4 45中的内容存到r1所指的连续内存,默认也是r0增加方向,此时r1值不改变

ldmia r0, {r4-r6} /* words[3] -> r4 = 0x03, words[4] -> r5 = 0x04; words[5] -> r6 = 0x05; */
stmia r1, {r4-r6} /* r4 -> array_buff[0] = 0x03; r5 -> array_buff[1] = 0x04; r6 -> array_buff[2] = 0x05 */
//这里是后缀的介绍还有指令写法可以写成r4-46,这样就是存3个了
//后缀具体代表 -IA (increase after), -IB (increase before), -DA (decrease after), -DB (decrease before)

ldmib r0, {r4-r6} /* words[4] -> r4 = 0x04; words[5] -> r5 = 0x05; words[6] -> r6 = 0x06 */
stmib r1, {r4-r6} /* r4 -> array_buff[1] = 0x04; r5 -> array_buff[2] = 0x05; r6 -> array_buff[3] = 0x06 */
ldmda r0, {r4-r6} /* words[3] -> r6 = 0x03; words[2] -> r5 = 0x02; words[1] -> r4 = 0x01 */
ldmdb r0, {r4-r6} /* words[2] -> r6 = 0x02; words[1] -> r5 = 0x01; words[0] -> r4 = 0x00 */
stmda r2, {r4-r6} /* r6 -> array_buff[2] = 0x02; r5 -> array_buff[1] = 0x01; r4 -> array_buff[0] = 0x00 */
stmdb r2, {r4-r5} /* r5 -> array_buff[1] = 0x01; r4 -> array_buff[0] = 0x00; */
//后缀的不同用法,I(x)时指令后面的寄存器操作顺序是顺着来的,D(x)时指令后面的寄存器操作顺序是逆着来的,要注意差别。这里第一个寄存器都不会发生改变

bx lr

push&pop

ARM中虽然直接写stmdb sp!, {r0, r1} ldmia sp!, {r4, r5} 在汇编会被翻译为 push {r0, r1} pop {r4, r5}
但push , pop本质上是stmdb sp!和ldmia sp!的代名词

所以push操作时是先push r1再push r0,从右到左;pop操作时是先pop r4再pop r5,从左到右;

条件执行相关

下表列出了ARM中可用的条件代码,它们的含义以及测试的标志,条件码跟着指令助记符满足指令格式就行,比如addlt,addsne

Condition Code Meaning (for cmp or subs) Status of Flags
EQ Equal Z==1
NE Not Equal Z==0
GT Signed Greater Than (Z==0) && (N==V)
LT Signed Less Than N!=V
GE Signed Greater Than or Equal N==V
LE Signed Less Than or Equal (Z==1) || (N!=V)
CS or HS Unsigned Higher or Same (or Carry Set) C==1
CC or LO Unsigned Lower (or Carry Clear) C==0
MI Negative (or Minus) N==1
PL Positive (or Plus) N==0
AL Always executed
NV Never executed
VS Signed Overflow V==1
VC No signed Overflow V==0
HI Unsigned Higher (C==1) && (Z==0)
LS Unsigned Lower or same (C==0) || (Z==0)

Thumb模式下有分支条件执行,语法格式是这样的

1
2
3
4
5
6
Syntax: IT{x{y{z}}} cond  ,IT指令之后会跟一个条件执行块,最多四条指令,这四条指令的条件都由这条IT指令指出

cond specifies the condition for the first instruction in the IT block(cond指出第一条指令的执行条件)
x specifies the condition switch for the second instruction in the IT block(x指出第二条指令执行的条件是不是和cond一样,如果x是T,那么第二条指令和cond条件相同;如果是E,那么第二条指令和cond条件相反)
y specifies the condition switch for the third instruction in the IT block(同上)
z specifies the condition switch for the fourth instruction in the IT block(同上)

所以Thumb模式下的条件执行指令块看起来就是这样的(这里直接偷图了):

image-20210111000847458

里面每个指令都是带条件的,并且符合IT指令的声明,常见的条件和相反条件有如下几个

Code Meaning Code Meaning
EQ Equal NE Not Equal
HS (or CS) Unsigned higher or same (or carry set) LO (or CC) Unsigned lower (or carry clear)
MI Negative PL Positive or Zero
VS Signed Overflow VC No Signed Overflow
HI Unsigned Higher LS Unsigned Lower or Same
GE Signed Greater Than or Equal LT Signed Less Than
GT Signed Greater Than LE Signed Less Than or Equal
AL (or omitted) Always Executed (There is no opposite to AL)

这里还有一段比较典型的将ARM切换成Thumb模式的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.syntax unified    @ this is important!
.text
.global _start

_start:
.code 32
add r3, pc, #1 @ increase value of PC by 1 and add it to R3,执行到这里时PC指向cmp指令
bx r3 @ branch + exchange to the address in R3 -> switch to Thumb state because LSB = 1

.code 16 @ Thumb state
cmp r0, #10
ite eq @ if R0 is equal 10...
addeq r1, #2 @ ... then R1 = R1 + 2
addne r1, #3 @ ... else R1 = R1 + 3
bkpt

分支指令(跳转)

三种分支指令:

  • Branch (B)
    • 简单跳转
  • Branch link (BL)
    • 将(当前执行指令地址+4)存入LR寄存器再跳转
  • Branch exchange (BX) and Branch link exchange (BLX)
    • 和B/BL指令一样步骤 然后最低位为1时会切换模式 (ARM <-> Thumb)
    • 必须用寄存器作为操作数

然后就是加上条件执行变成条件跳转了(跳转之后由于PC特性,会多读跳转目的处两条指令,实现自动PC+8?)

函数栈

先来看一个函数代码对应的汇编

main函数

1
2
3
4
5
6
7
8
int main()
{
int res = 0;
int a = 1;
int b = 2;
res = max(a, b);
return res;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Dump of assembler code for function main:
=> 0x000103e8 <+0>: push {r11, lr} @最开始执行到main时,lr存放的是<__libc_start_main+276> bl 0xb6ea4b28 <__GI_exit>,就是说这里将本函数的返回地址先存到了栈上
@然后r11存放的是前一个函数的栈帧地址(作用等于rbp吧)
0x000103ec <+4>: add r11, sp, #4 @新栈帧地址 直接sp+4得到,指向存放的lr
0x000103f0 <+8>: sub sp, sp, #16 @开辟了4*4字节大小的栈帧
0x000103f4 <+12>: mov r3, #0
0x000103f8 <+16>: str r3, [r11, #-8] @r11-8处存放0(res)
0x000103fc <+20>: mov r3, #1
0x00010400 <+24>: str r3, [r11, #-12] @r11-12处存放1(a)
0x00010404 <+28>: mov r3, #2
0x00010408 <+32>: str r3, [r11, #-16] @r11-16处存放2(b)
0x0001040c <+36>: ldr r0, [r11, #-12] @开始给要调用的函数赋值参数,把1给r0
0x00010410 <+40>: ldr r1, [r11, #-16] @把2给r1,这里可以看出来参数传递顺序
0x00010414 <+44>: bl 0x1042c <max> @把下个函数的返回地址存到lr后调用函数
0x00010418 <+48>: str r0, [r11, #-8] @(返回值赋给res,然后再给r0)
0x0001041c <+52>: ldr r3, [r11, #-8]
0x00010420 <+56>: mov r0, r3
0x00010424 <+60>: sub sp, r11, #4
0x00010428 <+64>: pop {r11, pc} @这里由于PC的特性,感觉将之前的lr内容给到PC之后,PC自己又会自动+8多读两条指令

上面代码中执行到+32处时栈帧看起来就是这样的(我这里sp指向栈顶后一个单元,可能有些实现会有不同):

image-20210111163416232

再来看max:

1
2
3
4
5
6
int max(int a,int b)
{
do_nothing();
if(a<b) return b;
else return a;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Dump of assembler code for function max:	@这里就不一行行解释了
=> 0x0001042c <+0>: push {r11, lr}
0x00010430 <+4>: add r11, sp, #4
0x00010434 <+8>: sub sp, sp, #8
0x00010438 <+12>: str r0, [r11, #-8]
0x0001043c <+16>: str r1, [r11, #-12]
0x00010440 <+20>: bl 0x1046c <do_nothing> @lr存入下一条指令地址然后跳转到do_nothing
0x00010444 <+24>: ldr r2, [r11, #-8]
0x00010448 <+28>: ldr r3, [r11, #-12]
0x0001044c <+32>: cmp r2, r3
0x00010450 <+36>: bge 0x1045c <max+48>
0x00010454 <+40>: ldr r3, [r11, #-12]
0x00010458 <+44>: b 0x10460 <max+52>
0x0001045c <+48>: ldr r3, [r11, #-8]
0x00010460 <+52>: mov r0, r3
0x00010464 <+56>: sub sp, r11, #4
0x00010468 <+60>: pop {r11, pc}

do_nothing:

1
2
3
4
int do_nothing()
{
return 0;
}
1
2
3
4
5
6
7
8
Dump of assembler code for function do_nothing:	@由于do_nothing没有再调用其他函数,所以没有push lr,返回时也是直接bx lr
=> 0x0001046c <+0>: push {r11} ; (str r11, [sp, #-4]!)
0x00010470 <+4>: add r11, sp, #0
0x00010474 <+8>: mov r3, #0
0x00010478 <+12>: mov r0, r3
0x0001047c <+16>: sub sp, r11, #0
0x00010480 <+20>: pop {r11} ; (ldr r11, [sp], #4)
0x00010484 <+24>: bx lr

把函数调用看做一个树形结构的话,这里也可以看出叶子函数和非叶子函数的区别

OK到此为止对ARM的一些基础就知道的差不多了,来写题吧

leg

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int main(){                                      
int key=0;
printf("Daddy has very strong arm! : ");
scanf("%d", &key);
if( (key1()+key2()+key3()) == key ){
printf("Congratz!\n");
int fd = open("flag", O_RDONLY);
char buf[100];
int r = read(fd, buf, 100);
write(0, buf, r);
}
else{
printf("I have strong leg :P\n");
}
return 0;
}

从c语言内容可以看出来,主要就是看key1、key2、key3的汇编然后计算和即可,先来看key1

1
2
3
4
5
6
7
8
Dump of assembler code for function key1:                          
0x00008cd4 <+0>: push {r11} ; (str r11, [sp, #-4]!)
0x00008cd8 <+4>: add r11, sp, #0
0x00008cdc <+8>: mov r3, pc
0x00008ce0 <+12>: mov r0, r3
0x00008ce4 <+16>: sub sp, r11, #0
0x00008ce8 <+20>: pop {r11} ; (ldr r11, [sp], #4)
0x00008cec <+24>: bx lr

这里就是考PC的特性,指向后两条指令处,即0x00008ce4,再来看key2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Dump of assembler code for function key2:                      
0x00008cf0 <+0>: push {r11} ; (str r11, [sp, #-4]!)
0x00008cf4 <+4>: add r11, sp, #0
0x00008cf8 <+8>: push {r6} ; (str r6, [sp, #-4]!)
0x00008cfc <+12>: add r6, pc, #1
0x00008d00 <+16>: bx r6
0x00008d04 <+20>: mov r3, pc @跳转到这里的时候切成了Thumb模式,然后将r3赋值0x00008d08
0x00008d06 <+22>: adds r3, #4 @ r3=0x00008d0C
0x00008d08 <+24>: push {r3}
0x00008d0a <+26>: pop {pc} @这里就是类似花指令的操作,接着执行
0x00008d0c <+28>: pop {r6} ; (ldr r6, [sp], #4)
0x00008d10 <+32>: mov r0, r3 @此时r3=0x00008d0C
0x00008d14 <+36>: sub sp, r11, #0
0x00008d18 <+40>: pop {r11} ; (ldr r11, [sp], #4)
0x00008d1c <+44>: bx lr

所以key2返回0x00008d0C,再来看key3

1
2
3
4
5
6
7
8
Dump of assembler code for function key3:                      
0x00008d20 <+0>: push {r11} ; (str r11, [sp, #-4]!)
0x00008d24 <+4>: add r11, sp, #0
0x00008d28 <+8>: mov r3, lr
0x00008d2c <+12>: mov r0, r3
0x00008d30 <+16>: sub sp, r11, #0
0x00008d34 <+20>: pop {r11} ; (ldr r11, [sp], #4)
0x00008d38 <+24>: bx lr

这里lr存的是在main里面的返回地址,去main里面看就知道是0x00008d80

所以加起来就是0x1A770→108400,ssh过去就可以得到flag